Skip to content

Commit c99be51

Browse files
committed
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: #4996 (comment) 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 #4996. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent 15e18f2 commit c99be51

File tree

4 files changed

+201
-0
lines changed

4 files changed

+201
-0
lines changed

compat/win32/path-utils.c

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#include "../../git-compat-util.h"
2+
#include "../../wrapper.h"
3+
#include "../../strbuf.h"
4+
#include "../../versioncmp.h"
25

36
int win32_has_dos_drive_prefix(const char *path)
47
{
@@ -50,3 +53,191 @@ int win32_offset_1st_component(const char *path)
5053

5154
return pos + is_dir_sep(*pos) - path;
5255
}
56+
57+
static int read_at(int fd, char *buffer, size_t offset, size_t size)
58+
{
59+
if (lseek(fd, offset, SEEK_SET) < 0) {
60+
fprintf(stderr, "could not seek to 0x%x\n", (unsigned int)offset);
61+
return -1;
62+
}
63+
64+
return read_in_full(fd, buffer, size);
65+
}
66+
67+
static size_t le16(const char *buffer)
68+
{
69+
unsigned char *u = (unsigned char *)buffer;
70+
return u[0] | (u[1] << 8);
71+
}
72+
73+
static size_t le32(const char *buffer)
74+
{
75+
return le16(buffer) | (le16(buffer + 2) << 16);
76+
}
77+
78+
/*
79+
* Determine the Go version of a given executable, if it was built with Go.
80+
*
81+
* This recapitulates the logic from
82+
* https://github.com/golang/go/blob/master/src/cmd/go/internal/version/version.go
83+
* (without requiring the user to install `go.exe` to find out).
84+
*/
85+
static ssize_t get_go_version(const char *path, char *go_version, size_t go_version_size)
86+
{
87+
int fd = open(path, O_RDONLY);
88+
char buffer[1024];
89+
off_t offset;
90+
size_t num_sections, opt_header_size, i;
91+
char *p = NULL, *q;
92+
ssize_t res = -1;
93+
94+
if (fd < 0)
95+
return -1;
96+
97+
if (read_in_full(fd, buffer, 2) < 0)
98+
goto fail;
99+
100+
/*
101+
* Parse the PE file format, for more details, see
102+
* https://en.wikipedia.org/wiki/Portable_Executable#Layout and
103+
* https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
104+
*/
105+
if (buffer[0] != 'M' || buffer[1] != 'Z')
106+
goto fail;
107+
108+
if (read_at(fd, buffer, 0x3c, 4) < 0)
109+
goto fail;
110+
111+
/* Read the `PE\0\0` signature and the COFF file header */
112+
offset = le32(buffer);
113+
if (read_at(fd, buffer, offset, 24) < 0)
114+
goto fail;
115+
116+
if (buffer[0] != 'P' || buffer[1] != 'E' || buffer[2] != '\0' || buffer[3] != '\0')
117+
goto fail;
118+
119+
num_sections = le16(buffer + 6);
120+
opt_header_size = le16(buffer + 20);
121+
offset += 24; /* skip file header */
122+
123+
/*
124+
* Validate magic number 0x10b or 0x20b, for full details see
125+
* https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only
126+
*/
127+
if (read_at(fd, buffer, offset, 2) < 0 ||
128+
((i = le16(buffer)) != 0x10b && i != 0x20b))
129+
goto fail;
130+
131+
offset += opt_header_size;
132+
133+
for (i = 0; i < num_sections; i++) {
134+
if (read_at(fd, buffer, offset + i * 40, 40) < 0)
135+
goto fail;
136+
137+
/*
138+
* For full details about the section headers, see
139+
* https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers
140+
*/
141+
if ((le32(buffer + 36) /* characteristics */ & ~0x600000) /* IMAGE_SCN_ALIGN_32BYTES */ ==
142+
(/* IMAGE_SCN_CNT_INITIALIZED_DATA */ 0x00000040 |
143+
/* IMAGE_SCN_MEM_READ */ 0x40000000 |
144+
/* IMAGE_SCN_MEM_WRITE */ 0x80000000)) {
145+
size_t size = le32(buffer + 16); /* "SizeOfRawData " */
146+
size_t pointer = le32(buffer + 20); /* "PointerToRawData " */
147+
148+
/*
149+
* Skip the section if either size or pointer is 0, see
150+
* https://github.com/golang/go/blob/go1.21.0/src/debug/buildinfo/buildinfo.go#L333
151+
* for full details.
152+
*
153+
* The size actually must be at least `buildInfoSize`,
154+
* i.e. 32.
155+
*/
156+
if (size < 32 || !pointer)
157+
continue;
158+
159+
p = malloc(size);
160+
161+
if (!p || read_at(fd, p, pointer, size) < 0)
162+
goto fail;
163+
164+
/*
165+
* Look for the build information embedded by Go, see
166+
* https://github.com/golang/go/blob/go1.21.0/src/debug/buildinfo/buildinfo.go#L165-L175
167+
* for full details.
168+
*
169+
* Note: Go contains code to enforce alignment along a
170+
* 16-byte boundary. In practice, no `.exe` has been
171+
* observed that required any adjustment, therefore
172+
* this here code skips that logic for simplicity.
173+
*/
174+
q = memmem(p, size - 18, "\xff Go buildinf:", 14);
175+
if (!q)
176+
goto fail;
177+
/*
178+
* Decode the build blob. For full details, see
179+
* https://github.com/golang/go/blob/go1.21.0/src/debug/buildinfo/buildinfo.go#L177-L191
180+
*
181+
* Note: The `endianness` values observed in practice
182+
* were always 2, therefore the complex logic to handle
183+
* any other value is skipped for simplicty.
184+
*/
185+
if ((q[14] == 8 || q[14] == 4) && q[15] == 2) {
186+
/*
187+
* Only handle a Go version string with fewer
188+
* than 128 characters, so the Go UVarint at
189+
* q[32] that indicates the string's length must
190+
* be only one byte (without the high bit set).
191+
*/
192+
if ((q[32] & 0x80) ||
193+
!q[32] ||
194+
(q + 33 + q[32] - p) > size ||
195+
q[32] + 1 > go_version_size)
196+
goto fail;
197+
res = q[32];
198+
memcpy(go_version, q + 33, res);
199+
go_version[res] = '\0';
200+
break;
201+
}
202+
}
203+
}
204+
205+
fail:
206+
free(p);
207+
close(fd);
208+
return res;
209+
}
210+
211+
void win32_warn_about_git_lfs_on_windows7(int exit_code, const char *argv0)
212+
{
213+
char buffer[128], *git_lfs = NULL;
214+
const char *p;
215+
216+
/*
217+
* Git LFS v3.5.1 fails with an Access Violation on Windows 7; That
218+
* would usually show up as an exit code 0xc0000005. For some reason
219+
* (probably because at this point, we no longer have the _original_
220+
* HANDLE that was returned by `CreateProcess()`) we get 0xb00 instead.
221+
*/
222+
if (exit_code != 0x0b00)
223+
return;
224+
if (GetVersion() >> 16 > 7601)
225+
return; /* Warn only on Windows 7 or older */
226+
if (!starts_with(argv0, "git-lfs ") ||
227+
!(git_lfs = locate_in_PATH("git-lfs")))
228+
return;
229+
if (get_go_version(git_lfs, buffer, sizeof(buffer)) > 0 &&
230+
skip_prefix(buffer, "go", &p) &&
231+
versioncmp("1.21.0", p) <= 0)
232+
warning("This program was built with Go v%s\n"
233+
"i.e. without support for this Windows version:\n"
234+
"\n\t%s\n"
235+
"\n"
236+
"To work around this, you can download and install a "
237+
"working version from\n"
238+
"\n"
239+
"\thttps://github.com/git-lfs/git-lfs/releases/tag/"
240+
"v3.4.1\n",
241+
p, git_lfs);
242+
free(git_lfs);
243+
}

compat/win32/path-utils.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ static inline int win32_has_dir_sep(const char *path)
3030
int win32_offset_1st_component(const char *path);
3131
#define offset_1st_component win32_offset_1st_component
3232

33+
void win32_warn_about_git_lfs_on_windows7(int exit_code, const char *argv0);
34+
#define warn_about_git_lfs_on_windows7 win32_warn_about_git_lfs_on_windows7
35+
3336
#endif

git-compat-util.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,12 @@ static inline int git_offset_1st_component(const char *path)
520520
#define offset_1st_component git_offset_1st_component
521521
#endif
522522

523+
#ifndef warn_about_git_lfs_on_windows7
524+
static inline void warn_about_git_lfs_on_windows7(int exit_code, const char *argv0)
525+
{
526+
}
527+
#endif
528+
523529
#ifndef is_valid_path
524530
#define is_valid_path(path) 1
525531
#endif

run-command.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ static int wait_or_whine(pid_t pid, const char *argv0, int in_signal)
566566
*/
567567
code += 128;
568568
} else if (WIFEXITED(status)) {
569+
warn_about_git_lfs_on_windows7(status, argv0);
569570
code = WEXITSTATUS(status);
570571
} else {
571572
if (!in_signal)

0 commit comments

Comments
 (0)