From 1fe2fa9150a7528ef81ab86bd8923e12a7790fed Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 4 Jul 2024 22:41:56 +0200 Subject: [PATCH] run-command: be helpful with Git LFS fails on Windows 7 Git LFS is now built with Go 1.21 which no longer supports Windows 7. However, Git for Windows still wants to support Windows 7. Ideally, Git LFS would re-introduce Windows 7 support until Git for Windows drops support for Windows 7, but that's not going to happen: https://github.com/git-for-windows/git/issues/4996#issuecomment-2176152565 The next best thing we can do is to let the users know what is happening, and how to get out of their fix, at least. This is not quite as easy as it would first seem because programs compiled with Go 1.21 or newer will simply throw an exception and fail with an Access Violation on Windows 7. The only way I found to address this is to replicate the logic from Go's very own `version` command (which can determine the Go version with which a given executable was built) to detect the situation, and in that case offer a helpful error message. This addresses https://github.com/git-for-windows/git/issues/4996. Signed-off-by: Johannes Schindelin --- compat/win32/path-utils.c | 139 ++++++++++++++++++++++++++++++++++++++ compat/win32/path-utils.h | 3 + git-compat-util.h | 6 ++ run-command.c | 1 + 4 files changed, 149 insertions(+) diff --git a/compat/win32/path-utils.c b/compat/win32/path-utils.c index ebf2f12eb66691..ba3d3a01c29446 100644 --- a/compat/win32/path-utils.c +++ b/compat/win32/path-utils.c @@ -1,4 +1,7 @@ #include "../../git-compat-util.h" +#include "../../wrapper.h" +#include "../../strbuf.h" +#include "../../versioncmp.h" int win32_has_dos_drive_prefix(const char *path) { @@ -50,3 +53,139 @@ int win32_offset_1st_component(const char *path) return pos + is_dir_sep(*pos) - path; } + +static int read_at(int fd, char *buffer, size_t offset, size_t size) +{ + if (lseek(fd, offset, SEEK_SET) < 0) { + fprintf(stderr, "could not seek to 0x%x\n", (unsigned int)offset); + return -1; + } + + return read_in_full(fd, buffer, size); +} + +static size_t le16(const char *buffer) +{ + unsigned char *u = (unsigned char *)buffer; + return u[0] | (u[1] << 8); +} + +static size_t le32(const char *buffer) +{ + return le16(buffer) | (le16(buffer + 2) << 16); +} + +/* + * Determine the Go version of a given executable, if it was built with Go. + * + * This recapitulates the logic from + * https://github.com/golang/go/blob/master/src/cmd/go/internal/version/version.go + * (without requiring the user to install `go.exe` to find out). + */ +static ssize_t get_go_version(const char *path, char *go_version, size_t go_version_size) +{ + int fd = open(path, O_RDONLY); + char buffer[1024]; + off_t offset; + size_t num_sections, opt_header_size, i; + char *p = NULL, *q; + ssize_t res = -1; + + if (fd < 0) + return -1; + + if (read_in_full(fd, buffer, 2) < 0) + goto fail; + + if (buffer[0] != 'M' || buffer[1] != 'Z') + goto fail; + + if (read_at(fd, buffer, 0x3c, 4) < 0) + goto fail; + + offset = le32(buffer); + if (read_at(fd, buffer, offset, 24) < 0) + goto fail; + + if (buffer[0] != 'P' || buffer[1] != 'E' || buffer[2] != '\0' || buffer[3] != '\0') + goto fail; + + num_sections = le16(buffer + 6); + opt_header_size = le16(buffer + 20); + offset += 24; /* skip file header */ + + if (read_at(fd, buffer, offset, 2) < 0) + goto fail; + + offset += opt_header_size; + + for (i = 0; i < num_sections; i++) { + if (read_at(fd, buffer, offset + i * 40, 40) < 0) + goto fail; + + if ((le32(buffer + 36) /* characteristics */ & ~0x600000) /* IMAGE_SCN_ALIGN_32BYTES */ == + (/* IMAGE_SCN_CNT_INITIALIZED_DATA */ 0x00000040 | + /* IMAGE_SCN_MEM_READ */ 0x40000000 | + /* IMAGE_SCN_MEM_WRITE */ 0x80000000)) { + size_t size = le32(buffer + 16); + + p = malloc(size); + + if (!p || read_at(fd, p, le32(buffer + 20), size) < 0) + goto fail; + + q = memmem(p, size, "\xff Go buildinf:", 14); + if (!q) + goto fail; + if (q[14] == 8 && q[15] == 2) { + if ((q[32] & 0x80) || + !q[32] || + q[32] + 1 > go_version_size) + goto fail; + res = q[32]; + memcpy(go_version, q + 33, res); + go_version[res] = '\0'; + break; + } + } + } + +fail: + free(p); + close(fd); + return res; +} + +void win32_warn_about_git_lfs_on_windows7(int exit_code, const char *argv0) +{ + char buffer[128], *git_lfs = NULL; + const char *p; + + /* + * Git LFS v3.5.1 fails with an Access Violation on Windows 7; That + * would usually show up as an exit code 0xc0000005. For some reason + * (probably because at this point, we no longer have the _original_ + * HANDLE that was returned by `CreateProcess()`) we get 0xb00 instead. + */ + if (exit_code != 0x0b00) + return; + if (GetVersion() >> 16 > 7601) + return; /* Warn only on Windows 7 or older */ + if (!starts_with(argv0, "git-lfs ") || + !(git_lfs = locate_in_PATH("git-lfs"))) + return; + if (get_go_version(git_lfs, buffer, sizeof(buffer)) > 0 && + skip_prefix(buffer, "go", &p) && + versioncmp("1.21.0", p) <= 0) + warning("This program was built with Go v%s\n" + "i.e. without support for this Windows version:\n" + "\n\t%s\n" + "\n" + "To work around this, you can download and install a" + "working version from\n" + "\n" + "\thttps://github.com/git-lfs/git-lfs/releases/tag/" + "v3.4.1\n", + p, git_lfs); + free(git_lfs); +} diff --git a/compat/win32/path-utils.h b/compat/win32/path-utils.h index 65fa3b9263a47c..792ebebaeedfe8 100644 --- a/compat/win32/path-utils.h +++ b/compat/win32/path-utils.h @@ -30,4 +30,7 @@ static inline int win32_has_dir_sep(const char *path) int win32_offset_1st_component(const char *path); #define offset_1st_component win32_offset_1st_component +void win32_warn_about_git_lfs_on_windows7(int exit_code, const char *argv0); +#define warn_about_git_lfs_on_windows7 win32_warn_about_git_lfs_on_windows7 + #endif diff --git a/git-compat-util.h b/git-compat-util.h index 504e2787dc04d5..d3f9695c6b5c9b 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -520,6 +520,12 @@ static inline int git_offset_1st_component(const char *path) #define offset_1st_component git_offset_1st_component #endif +#ifndef warn_about_git_lfs_on_windows7 +static inline void warn_about_git_lfs_on_windows7(int exit_code, const char *argv0) +{ +} +#endif + #ifndef is_valid_path #define is_valid_path(path) 1 #endif diff --git a/run-command.c b/run-command.c index 1b821042b4e067..9f19638bdb4cb0 100644 --- a/run-command.c +++ b/run-command.c @@ -566,6 +566,7 @@ static int wait_or_whine(pid_t pid, const char *argv0, int in_signal) */ code += 128; } else if (WIFEXITED(status)) { + warn_about_git_lfs_on_windows7(status, argv0); code = WEXITSTATUS(status); } else { if (!in_signal)