From: "Bill Zissimopoulos via GitGitGadget" <gitgitgadget@gmail•com>
To: git@vger•kernel.org
Cc: Johannes Schindelin <johannes.schindelin@gmx•de>,
Bill Zissimopoulos <billziss@navimatics•com>
Subject: [PATCH 06/18] mingw: compute the correct size for symlinks in `mingw_lstat()`
Date: Wed, 17 Dec 2025 14:08:43 +0000 [thread overview]
Message-ID: <4c49a3d9bf57415ea31ac357452b646c75b64d82.1765980535.git.gitgitgadget@gmail.com> (raw)
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
next prev parent reply other threads:[~2025-12-17 14:09 UTC|newest]
Thread overview: 51+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-17 14:08 [PATCH 00/18] Support symbolic links on Windows Johannes Schindelin via GitGitGadget
2025-12-17 14:08 ` [PATCH 01/18] mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()` Karsten Blees via GitGitGadget
2025-12-18 10:34 ` Johannes Sixt
2025-12-17 14:08 ` [PATCH 02/18] mingw: implement `stat()` with symlink support Karsten Blees via GitGitGadget
2025-12-18 10:44 ` Johannes Sixt
2026-01-09 20:04 ` Johannes Schindelin
2025-12-17 14:08 ` [PATCH 03/18] mingw: drop the separate `do_lstat()` function Karsten Blees via GitGitGadget
2025-12-18 10:48 ` Johannes Sixt
2025-12-17 14:08 ` [PATCH 04/18] mingw: let `mingw_lstat()` error early upon problems with reparse points Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 05/18] mingw: teach dirent about symlinks Karsten Blees via GitGitGadget
2025-12-17 14:08 ` Bill Zissimopoulos via GitGitGadget [this message]
2025-12-17 14:08 ` [PATCH 07/18] mingw: factor out the retry logic Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 08/18] mingw: change default of `core.symlinks` to false Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 09/18] mingw: add symlink-specific error codes Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 10/18] mingw: handle symlinks to directories in `mingw_unlink()` Karsten Blees via GitGitGadget
2025-12-18 2:49 ` Ben Knoble
2026-01-09 20:04 ` Johannes Schindelin
2025-12-17 14:08 ` [PATCH 11/18] mingw: support renaming symlinks Karsten Blees via GitGitGadget
2025-12-18 17:44 ` Johannes Sixt
2026-01-09 20:04 ` Johannes Schindelin
2025-12-17 14:08 ` [PATCH 12/18] mingw: allow `mingw_chdir()` to change to symlink-resolved directories Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 13/18] mingw: implement `readlink()` Karsten Blees via GitGitGadget
2025-12-18 18:13 ` Johannes Sixt
2026-01-09 20:04 ` Johannes Schindelin
2025-12-17 14:08 ` [PATCH 14/18] mingw: implement basic `symlink()` functionality (file symlinks only) Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 15/18] mingw: add support for symlinks to directories Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 16/18] mingw: try to create symlinks without elevated permissions Johannes Schindelin via GitGitGadget
2025-12-17 14:08 ` [PATCH 17/18] mingw: emulate `stat()` a little more faithfully Johannes Schindelin via GitGitGadget
2025-12-17 14:08 ` [PATCH 18/18] mingw: special-case index entries for symlinks with buggy size Johannes Schindelin via GitGitGadget
2025-12-18 0:00 ` [PATCH 00/18] Support symbolic links on Windows Junio C Hamano
2025-12-18 18:51 ` Johannes Sixt
2025-12-18 19:33 ` Karsten Blees
2026-01-09 20:04 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
2026-01-09 20:04 ` [PATCH v2 01/18] mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()` Karsten Blees via GitGitGadget
2026-01-09 20:04 ` [PATCH v2 02/18] mingw: implement `stat()` with symlink support Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 03/18] mingw: drop the separate `do_lstat()` function Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 04/18] mingw: let `mingw_lstat()` error early upon problems with reparse points Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 05/18] mingw: teach dirent about symlinks Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 06/18] mingw: compute the correct size for symlinks in `mingw_lstat()` Bill Zissimopoulos via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 07/18] mingw: factor out the retry logic Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 08/18] mingw: change default of `core.symlinks` to false Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 09/18] mingw: add symlink-specific error codes Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 10/18] mingw: handle symlinks to directories in `mingw_unlink()` Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 11/18] mingw: support renaming symlinks Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 12/18] mingw: allow `mingw_chdir()` to change to symlink-resolved directories Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 13/18] mingw: implement `readlink()` Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 14/18] mingw: implement basic `symlink()` functionality (file symlinks only) Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 15/18] mingw: add support for symlinks to directories Karsten Blees via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 16/18] mingw: try to create symlinks without elevated permissions Johannes Schindelin via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 17/18] mingw: emulate `stat()` a little more faithfully Johannes Schindelin via GitGitGadget
2026-01-09 20:05 ` [PATCH v2 18/18] mingw: special-case index entries for symlinks with buggy size Johannes Schindelin via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=4c49a3d9bf57415ea31ac357452b646c75b64d82.1765980535.git.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail$(echo .)com \
--cc=billziss@navimatics$(echo .)com \
--cc=git@vger$(echo .)kernel.org \
--cc=johannes.schindelin@gmx$(echo .)de \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox