git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 06/18] mingw: compute the correct size for symlinks in `mingw_lstat()`

From
BGBill Zissimopoulos via GitGitGadget <gitgitgadget@gmail.com>
Date
Dec 17, 2025, 14:08 UTC
Message-ID
<4c49a3d9bf57415ea31ac357452b646c75b64d82.1765980535.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.2018.git.1765980535.gitgitgadget@gmail.com>
From: Bill Zissimopoulos <billziss@navimatics.com>

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>
---
 compat/mingw.c | 114 ++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 104 insertions(+), 10 deletions(-)
diff --git a/compat/mingw.c b/compat/mingw.c
index a3a48db581..c7571951dc 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -21,6 +21,7 @@
 #define SECURITY_WIN32
 #include <sspi.h>
 #include <wchar.h>
+#include <winioctl.h>
 #include <winternl.h>
 
 #define STATUS_DELETE_PENDING ((NTSTATUS) 0xC0000056)
@@ -917,10 +918,102 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
 	return 1;
 }
 
+#ifndef _WINNT_H
+/*
+ * The REPARSE_DATA_BUFFER structure is defined in the Windows DDK (in
+ * ntifs.h) and in MSYS1's winnt.h (which defines _WINNT_H). So define
+ * it ourselves if we are on MSYS2 (whose winnt.h defines _WINNT_).
+ */
+typedef struct _REPARSE_DATA_BUFFER {
+	DWORD  ReparseTag;
+	WORD   ReparseDataLength;
+	WORD   Reserved;
+#ifndef _MSC_VER
+	_ANONYMOUS_UNION
+#endif
+	union {
+		struct {
+			WORD   SubstituteNameOffset;
+			WORD   SubstituteNameLength;
+			WORD   PrintNameOffset;
+			WORD   PrintNameLength;
+			ULONG  Flags;
+			WCHAR PathBuffer[1];
+		} SymbolicLinkReparseBuffer;
+		struct {
+			WORD   SubstituteNameOffset;
+			WORD   SubstituteNameLength;
+			WORD   PrintNameOffset;
+			WORD   PrintNameLength;
+			WCHAR PathBuffer[1];
+		} MountPointReparseBuffer;
+		struct {
+			BYTE   DataBuffer[1];
+		} GenericReparseBuffer;
+	} DUMMYUNIONNAME;
+} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
+#endif
+
+static int read_reparse_point(const WCHAR *wpath, BOOL fail_on_unknown_tag,
+			      char *tmpbuf, int *plen, DWORD *ptag)
+{
+	HANDLE handle;
+	WCHAR *wbuf;
+	REPARSE_DATA_BUFFER *b = alloca(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
+	DWORD dummy;
+
+	/* read reparse point data */
+	handle = CreateFileW(wpath, 0,
+			FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
+			OPEN_EXISTING,
+			FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
+	if (handle == INVALID_HANDLE_VALUE) {
+		errno = err_win_to_posix(GetLastError());
+		return -1;
+	}
+	if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, b,
+			MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &dummy, NULL)) {
+		errno = err_win_to_posix(GetLastError());
+		CloseHandle(handle);
+		return -1;
+	}
+	CloseHandle(handle);
+
+	/* get target path for symlinks or mount points (aka 'junctions') */
+	switch ((*ptag = b->ReparseTag)) {
+	case IO_REPARSE_TAG_SYMLINK:
+		wbuf = (WCHAR*) (((char*) b->SymbolicLinkReparseBuffer.PathBuffer)
+				+ b->SymbolicLinkReparseBuffer.SubstituteNameOffset);
+		*(WCHAR*) (((char*) wbuf)
+				+ b->SymbolicLinkReparseBuffer.SubstituteNameLength) = 0;
+		break;
+	case IO_REPARSE_TAG_MOUNT_POINT:
+		wbuf = (WCHAR*) (((char*) b->MountPointReparseBuffer.PathBuffer)
+				+ b->MountPointReparseBuffer.SubstituteNameOffset);
+		*(WCHAR*) (((char*) wbuf)
+				+ b->MountPointReparseBuffer.SubstituteNameLength) = 0;
+		break;
+	default:
+		if (fail_on_unknown_tag) {
+			errno = EINVAL;
+			return -1;
+		} else {
+			*plen = MAX_PATH;
+			return 0;
+		}
+	}
+
+	if ((*plen =
+	     xwcstoutf(tmpbuf, normalize_ntpath(wbuf), MAX_PATH)) <  0)
+		return -1;
+	return 0;
+}
+
 int mingw_lstat(const char *file_name, struct stat *buf)
 {
 	WIN32_FILE_ATTRIBUTE_DATA fdata;
-	WIN32_FIND_DATAW findbuf = { 0 };
+	DWORD reparse_tag = 0;
+	int link_len = 0;
 	wchar_t wfilename[MAX_PATH];
 	int wlen = xutftowcs_path(wfilename, file_name);
 	if (wlen < 0)
@@ -935,28 +1028,29 @@ int mingw_lstat(const char *file_name, struct stat *buf)
 	}
 
 	if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
-		/* for reparse points, use FindFirstFile to get the reparse tag */
+		/* for reparse points, get the link tag and length */
 		if (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
-			HANDLE handle = FindFirstFileW(wfilename, &findbuf);
-			if (handle == INVALID_HANDLE_VALUE)
-				goto error;
-			FindClose(handle);
+			char tmpbuf[MAX_PATH];
+
+			if (read_reparse_point(wfilename, FALSE, tmpbuf,
+					       &link_len, &reparse_tag) < 0)
+				return -1;
 		}
 		buf->st_ino = 0;
 		buf->st_gid = 0;
 		buf->st_uid = 0;
 		buf->st_nlink = 1;
 		buf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes,
-				findbuf.dwReserved0);
-		buf->st_size = fdata.nFileSizeLow |
-			(((off_t)fdata.nFileSizeHigh)<<32);
+				reparse_tag);
+		buf->st_size = S_ISLNK(buf->st_mode) ? link_len :
+			fdata.nFileSizeLow | (((off_t) fdata.nFileSizeHigh) << 32);
 		buf->st_dev = buf->st_rdev = 0; /* not used by Git */
 		filetime_to_timespec(&(fdata.ftLastAccessTime), &(buf->st_atim));
 		filetime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim));
 		filetime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim));
 		return 0;
 	}
-error:
+
 	switch (GetLastError()) {
 	case ERROR_ACCESS_DENIED:
 	case ERROR_SHARING_VIOLATION:
-- 
gitgitgadget
Previous: Karsten Blees via GitGitGadgetNext: Karsten Blees via GitGitGadget
Message 11 of 51 in “Support symbolic links on Windows”
  1. 00/18 Support symbolic links on WindowsJohannes Schindelin via GitGitGadget, Dec 17, 2025
  2. 01/18 mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`Karsten Blees via GitGitGadget, Dec 17, 2025
  3. Johannes SixtDec 18, 2025
  4. 02/18 mingw: implement `stat()` with symlink supportKarsten Blees via GitGitGadget, Dec 17, 2025
  5. Johannes SixtDec 18, 2025
  6. Johannes SchindelinJan 9, 2026
  7. 03/18 mingw: drop the separate `do_lstat()` functionKarsten Blees via GitGitGadget, Dec 17, 2025
  8. Johannes SixtDec 18, 2025
  9. 04/18 mingw: let `mingw_lstat()` error early upon problems with reparse pointsKarsten Blees via GitGitGadget, Dec 17, 2025
  10. 05/18 mingw: teach dirent about symlinksKarsten Blees via GitGitGadget, Dec 17, 2025
  11. 06/18 mingw: compute the correct size for symlinks in `mingw_lstat()`Bill Zissimopoulos via GitGitGadget, Dec 17, 2025
  12. 07/18 mingw: factor out the retry logicKarsten Blees via GitGitGadget, Dec 17, 2025
  13. 08/18 mingw: change default of `core.symlinks` to falseKarsten Blees via GitGitGadget, Dec 17, 2025
  14. 09/18 mingw: add symlink-specific error codesKarsten Blees via GitGitGadget, Dec 17, 2025
  15. 10/18 mingw: handle symlinks to directories in `mingw_unlink()`Karsten Blees via GitGitGadget, Dec 17, 2025
  16. Ben KnobleDec 18, 2025
  17. Johannes SchindelinJan 9, 2026
  18. 11/18 mingw: support renaming symlinksKarsten Blees via GitGitGadget, Dec 17, 2025
  19. Johannes SixtDec 18, 2025
  20. Johannes SchindelinJan 9, 2026
  21. 12/18 mingw: allow `mingw_chdir()` to change to symlink-resolved directoriesKarsten Blees via GitGitGadget, Dec 17, 2025
  22. 13/18 mingw: implement `readlink()`Karsten Blees via GitGitGadget, Dec 17, 2025
  23. Johannes SixtDec 18, 2025
  24. Johannes SchindelinJan 9, 2026
  25. 14/18 mingw: implement basic `symlink()` functionality (file symlinks only)Karsten Blees via GitGitGadget, Dec 17, 2025
  26. 15/18 mingw: add support for symlinks to directoriesKarsten Blees via GitGitGadget, Dec 17, 2025
  27. 16/18 mingw: try to create symlinks without elevated permissionsJohannes Schindelin via GitGitGadget, Dec 17, 2025
  28. 17/18 mingw: emulate `stat()` a little more faithfullyJohannes Schindelin via GitGitGadget, Dec 17, 2025
  29. 18/18 mingw: special-case index entries for symlinks with buggy sizeJohannes Schindelin via GitGitGadget, Dec 17, 2025
  30. Junio C HamanoDec 18, 2025
  31. Johannes SixtDec 18, 2025
  32. Karsten BleesDec 18, 2025
  33. 00/18 Support symbolic links on WindowsJohannes Schindelin via GitGitGadget, Jan 9, 2026
  34. 01/18 mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`Karsten Blees via GitGitGadget, Jan 9, 2026
  35. 02/18 mingw: implement `stat()` with symlink supportKarsten Blees via GitGitGadget, Jan 9, 2026
  36. 03/18 mingw: drop the separate `do_lstat()` functionKarsten Blees via GitGitGadget, Jan 9, 2026
  37. 04/18 mingw: let `mingw_lstat()` error early upon problems with reparse pointsKarsten Blees via GitGitGadget, Jan 9, 2026
  38. 05/18 mingw: teach dirent about symlinksKarsten Blees via GitGitGadget, Jan 9, 2026
  39. 06/18 mingw: compute the correct size for symlinks in `mingw_lstat()`Bill Zissimopoulos via GitGitGadget, Jan 9, 2026
  40. 07/18 mingw: factor out the retry logicKarsten Blees via GitGitGadget, Jan 9, 2026
  41. 08/18 mingw: change default of `core.symlinks` to falseKarsten Blees via GitGitGadget, Jan 9, 2026
  42. 09/18 mingw: add symlink-specific error codesKarsten Blees via GitGitGadget, Jan 9, 2026
  43. 11/18 mingw: support renaming symlinksKarsten Blees via GitGitGadget, Jan 9, 2026
  44. 10/18 mingw: handle symlinks to directories in `mingw_unlink()`Karsten Blees via GitGitGadget, Jan 9, 2026
  45. 12/18 mingw: allow `mingw_chdir()` to change to symlink-resolved directoriesKarsten Blees via GitGitGadget, Jan 9, 2026
  46. 13/18 mingw: implement `readlink()`Karsten Blees via GitGitGadget, Jan 9, 2026
  47. 14/18 mingw: implement basic `symlink()` functionality (file symlinks only)Karsten Blees via GitGitGadget, Jan 9, 2026
  48. 15/18 mingw: add support for symlinks to directoriesKarsten Blees via GitGitGadget, Jan 9, 2026
  49. 16/18 mingw: try to create symlinks without elevated permissionsJohannes Schindelin via GitGitGadget, Jan 9, 2026
  50. 17/18 mingw: emulate `stat()` a little more faithfullyJohannes Schindelin via GitGitGadget, Jan 9, 2026
  51. 18/18 mingw: special-case index entries for symlinks with buggy sizeJohannes Schindelin via GitGitGadget, Jan 9, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.