mingw: compute the correct size for symlinks in `mingw_lstat()`

POSIX specifies that upon successful return from `lstat()`: "the value of the st_size member shall be set to the length of the pathname contained in the symbolic link not including any terminating null byte". Git typically doesn't trust the `stat.st_size` member of symlinks (e.g. see `strbuf_readlink()`). Therefore, it is tempting to save on the extra overhead of opening and reading the reparse point merely to calculate the exact size of the link target. This is, in fact, what Git for Windows did, from May 2015 to May 2020. At least almost: some functions take shortcuts if `st_size` is 0 (e.g. `diff_populate_filespec()`), hence Git for Windows hard-coded the length of all symlinks to MAX_PATH. This did cause problems, though, specifically in Git repositories that were also accessed by Git for Cygwin or Git for WSL. For example, doing `git reset --hard` using Git for Windows would update the size of symlinks in the index to be MAX_PATH; at a later time Git for Cygwin or Git for WSL would find that symlinks have changed size during `git status` and update the index. And then Git for Windows would think that the index needs to be updated. Even if the symlinks did not, in fact, change. To avoid that, the correct size must be determined. Signed-off-by: Bill Zissimopoulos <billziss@navimatics.com> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Bill Zissimopoulos committed Jan 9, 2026 at 20:05 UTC 543a91ccf9ba551eb563abb44eef0deadf3e38b7
1 file changed +104 -10
compat/mingw.c
+104 -10
@@ -21,6 +21,7 @@
21 #define SECURITY_WIN32
22 #include <sspi.h>
23 #include <wchar.h>
24 +#include <winioctl.h>
25 #include <winternl.h>
26
27 #define STATUS_DELETE_PENDING ((NTSTATUS) 0xC0000056)
@@ -917,10 +918,102 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
918 return 1;
919 }
920
921 +#ifndef _WINNT_H
922 +/*
923 + * The REPARSE_DATA_BUFFER structure is defined in the Windows DDK (in
924 + * ntifs.h) and in MSYS1's winnt.h (which defines _WINNT_H). So define
925 + * it ourselves if we are on MSYS2 (whose winnt.h defines _WINNT_).
926 + */
927 +typedef struct _REPARSE_DATA_BUFFER {
928 + DWORD ReparseTag;
929 + WORD ReparseDataLength;
930 + WORD Reserved;
931 +#ifndef _MSC_VER
932 + _ANONYMOUS_UNION
933 +#endif
934 + union {
935 + struct {
936 + WORD SubstituteNameOffset;
937 + WORD SubstituteNameLength;
938 + WORD PrintNameOffset;
939 + WORD PrintNameLength;
940 + ULONG Flags;
941 + WCHAR PathBuffer[1];
942 + } SymbolicLinkReparseBuffer;
943 + struct {
944 + WORD SubstituteNameOffset;
945 + WORD SubstituteNameLength;
946 + WORD PrintNameOffset;
947 + WORD PrintNameLength;
948 + WCHAR PathBuffer[1];
949 + } MountPointReparseBuffer;
950 + struct {
951 + BYTE DataBuffer[1];
952 + } GenericReparseBuffer;
953 + } DUMMYUNIONNAME;
954 +} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
955 +#endif
956 +
957 +static int read_reparse_point(const WCHAR *wpath, BOOL fail_on_unknown_tag,
958 + char *tmpbuf, int *plen, DWORD *ptag)
959 +{
960 + HANDLE handle;
961 + WCHAR *wbuf;
962 + REPARSE_DATA_BUFFER *b = alloca(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
963 + DWORD dummy;
964 +
965 + /* read reparse point data */
966 + handle = CreateFileW(wpath, 0,
967 + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
968 + OPEN_EXISTING,
969 + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
970 + if (handle == INVALID_HANDLE_VALUE) {
971 + errno = err_win_to_posix(GetLastError());
972 + return -1;
973 + }
974 + if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, b,
975 + MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &dummy, NULL)) {
976 + errno = err_win_to_posix(GetLastError());
977 + CloseHandle(handle);
978 + return -1;
979 + }
980 + CloseHandle(handle);
981 +
982 + /* get target path for symlinks or mount points (aka 'junctions') */
983 + switch ((*ptag = b->ReparseTag)) {
984 + case IO_REPARSE_TAG_SYMLINK:
985 + wbuf = (WCHAR*) (((char*) b->SymbolicLinkReparseBuffer.PathBuffer)
986 + + b->SymbolicLinkReparseBuffer.SubstituteNameOffset);
987 + *(WCHAR*) (((char*) wbuf)
988 + + b->SymbolicLinkReparseBuffer.SubstituteNameLength) = 0;
989 + break;
990 + case IO_REPARSE_TAG_MOUNT_POINT:
991 + wbuf = (WCHAR*) (((char*) b->MountPointReparseBuffer.PathBuffer)
992 + + b->MountPointReparseBuffer.SubstituteNameOffset);
993 + *(WCHAR*) (((char*) wbuf)
994 + + b->MountPointReparseBuffer.SubstituteNameLength) = 0;
995 + break;
996 + default:
997 + if (fail_on_unknown_tag) {
998 + errno = EINVAL;
999 + return -1;
1000 + } else {
1001 + *plen = MAX_PATH;
1002 + return 0;
1003 + }
1004 + }
1005 +
1006 + if ((*plen =
1007 + xwcstoutf(tmpbuf, normalize_ntpath(wbuf), MAX_PATH)) < 0)
1008 + return -1;
1009 + return 0;
1010 +}
1011 +
1012 int mingw_lstat(const char *file_name, struct stat *buf)
1013 {
1014 WIN32_FILE_ATTRIBUTE_DATA fdata;
923 - WIN32_FIND_DATAW findbuf = { 0 };
1015 + DWORD reparse_tag = 0;
1016 + int link_len = 0;
1017 wchar_t wfilename[MAX_PATH];
1018 int wlen = xutftowcs_path(wfilename, file_name);
1019 if (wlen < 0)
@@ -935,28 +1028,29 @@ int mingw_lstat(const char *file_name, struct stat *buf)
1028 }
1029
1030 if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
938 - /* for reparse points, use FindFirstFile to get the reparse tag */
1031 + /* for reparse points, get the link tag and length */
1032 if (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
940 - HANDLE handle = FindFirstFileW(wfilename, &findbuf);
941 - if (handle == INVALID_HANDLE_VALUE)
942 - goto error;
943 - FindClose(handle);
1033 + char tmpbuf[MAX_PATH];
1034 +
1035 + if (read_reparse_point(wfilename, FALSE, tmpbuf,
1036 + &link_len, &reparse_tag) < 0)
1037 + return -1;
1038 }
1039 buf->st_ino = 0;
1040 buf->st_gid = 0;
1041 buf->st_uid = 0;
1042 buf->st_nlink = 1;
1043 buf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes,
950 - findbuf.dwReserved0);
951 - buf->st_size = fdata.nFileSizeLow |
952 - (((off_t)fdata.nFileSizeHigh)<<32);
1044 + reparse_tag);
1045 + buf->st_size = S_ISLNK(buf->st_mode) ? link_len :
1046 + fdata.nFileSizeLow | (((off_t) fdata.nFileSizeHigh) << 32);
1047 buf->st_dev = buf->st_rdev = 0; /* not used by Git */
1048 filetime_to_timespec(&(fdata.ftLastAccessTime), &(buf->st_atim));
1049 filetime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim));
1050 filetime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim));
1051 return 0;
1052 }
959 -error:
1053 +
1054 switch (GetLastError()) {
1055 case ERROR_ACCESS_DENIED:
1056 case ERROR_SHARING_VIOLATION: