mingw: optionally use legacy (non-POSIX) delete semantics

At some point between Windows 10 Build 17134.1304 and Build 18363.657, the default behavior of `DeleteFileW()` was changed to use POSIX semantics (https://stackoverflow.com/a/60512798). Under those semantics, a file can be deleted even when another process holds an active `MapViewOfFile` view on it: the directory entry is removed immediately, but the underlying data persists until the last handle is closed. On older Windows versions (and Windows 10 builds before that change), `DeleteFileW()` uses legacy semantics where deletion fails outright if any process holds a file mapping. To allow testing code paths that depend on the legacy behavior, introduce a `GIT_TEST_LEGACY_DELETE` environment variable. When set, `mingw_unlink()` uses `SetFileInformationByHandle()` with `FileDispositionInfo` (the non-POSIX variant) instead of `DeleteFileW()`, forcing legacy delete semantics regardless of the Windows version. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed May 7, 2026 at 12:51 UTC 69ed0e35a7548a17a0fcd79b265a6872bceb2d5d
1 file changed +45 -2
compat/mingw.c
+45 -2
@@ -449,20 +449,63 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
449 return wbuf;
450 }
451
452 +/*
453 + * Use SetFileInformationByHandle(FileDispositionInfo) to force legacy
454 + * (non-POSIX) delete semantics. On Windows 11, DeleteFileW() uses POSIX
455 + * delete semantics internally, allowing deletion even with active
456 + * MapViewOfFile views. This helper simulates Windows 10 behavior where
457 + * deletion fails if a file mapping exists.
458 + *
459 + * Returns nonzero on success (like DeleteFileW), 0 on failure.
460 + */
461 +static int legacy_delete_file(const wchar_t *wpathname)
462 +{
463 + FILE_DISPOSITION_INFO fdi = { TRUE };
464 + DWORD gle;
465 + HANDLE h = CreateFileW(wpathname, DELETE,
466 + FILE_SHARE_READ | FILE_SHARE_WRITE |
467 + FILE_SHARE_DELETE,
468 + NULL, OPEN_EXISTING,
469 + FILE_FLAG_OPEN_REPARSE_POINT, NULL);
470 + if (h == INVALID_HANDLE_VALUE)
471 + return 0;
472 +
473 + if (SetFileInformationByHandle(h, FileDispositionInfo,
474 + &fdi, sizeof(fdi))) {
475 + CloseHandle(h);
476 + return 1;
477 + }
478 + gle = GetLastError();
479 + CloseHandle(h);
480 + SetLastError(gle);
481 + return 0;
482 +}
483 +
484 +static int try_delete_file(const wchar_t *wpathname, int use_legacy)
485 +{
486 + if (use_legacy)
487 + return legacy_delete_file(wpathname);
488 + return DeleteFileW(wpathname);
489 +}
490 +
491 int mingw_unlink(const char *pathname, int handle_in_use_error)
492 {
493 + static int use_legacy_delete = -1;
494 int tries = 0;
495 wchar_t wpathname[MAX_PATH];
496 if (xutftowcs_path(wpathname, pathname) < 0)
497 return -1;
498
459 - if (DeleteFileW(wpathname))
499 + if (use_legacy_delete < 0)
500 + use_legacy_delete = git_env_bool("GIT_TEST_LEGACY_DELETE", 0);
501 +
502 + if (try_delete_file(wpathname, use_legacy_delete))
503 return 0;
504
505 do {
506 /* read-only files cannot be removed */
507 _wchmod(wpathname, 0666);
465 - if (!_wunlink(wpathname))
508 + if (try_delete_file(wpathname, use_legacy_delete))
509 return 0;
510 if (!is_file_in_use_error(GetLastError()))
511 break;