Skip to content

Commit

Permalink
PROOF-OF-CONCEPT: allow deleting files that are in use
Browse files Browse the repository at this point in the history
This should not be merged as-is, but is intended to act as a
discussion-starter.

Applying the insights of https://github.com/LloydLabs/delete-self-poc,
this commit implements support for deleting files that are in-use, even
if that is theoretically impossible on Windows.

The ramifications are somewhat non-trivial, though:

- Many Windows applications are totally unhappy when the contract "files
  that are in use cannot be deleted" is broken.

- Even `git.exe` itself can be used to demonstrate the dire downsides of
  this here patch: when switching branches in `git-sdk-64` where, say,
  `mingw64/bin/libintl-8.dll` needs to be updated (which is a dependency
  of `mingw64/bin/git.exe`), a `checkout.workers` value greater than 1
  will let `git.exe` attempt to spawn `git checkout--worker` which will
  fail to load because of the missing dependency (which is missing
  because `git.exe` _just_ deleted it).

Signed-off-by: Johannes Schindelin <[email protected]>
  • Loading branch information
dscho committed Nov 29, 2023
1 parent 4b968f3 commit eb321e2
Showing 1 changed file with 56 additions and 0 deletions.
56 changes: 56 additions & 0 deletions compat/mingw.c
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,59 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
return wbuf;
}

/*
* This is quite the bit of an NTFS hack: It is well-known that a file cannot
* be deleted while it is in use. It is much less well-known that a file that
* is in use can be renamed via `SetFileInformationByHandle()`, and the target
* file name can be a so-called "Alternate Data Stream" (for details, see
* https://learn.microsoft.com/en-us/windows/win32/fileio/file-streams). And
* once the in-use file has been renamed thusly, a handle to the original path
* can be opened and the file can be deleted via yet another call to
* `SetFileInformationByHandle()`.
*/
static int rename_in_use_file_to_alternate_data_stream(wchar_t *wpath)
{
HANDLE handle;
BOOL ret;
const wchar_t dest[] = L":delete-me";
char buffer[sizeof(FILE_RENAME_INFO) + sizeof(dest) + 10];
FILE_RENAME_INFO *rename_info = (void *)buffer;
FILE_DISPOSITION_INFO disposition_info = { .DeleteFile = TRUE };

if (!is_file_in_use_error(GetLastError()))
return 0;

handle = CreateFileW(wpath, DELETE,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (handle == INVALID_HANDLE_VALUE)
return 0;

rename_info->FileNameLength = wcslen(dest);
wcscpy(rename_info->FileName, dest);

ret = SetFileInformationByHandle(handle, FileRenameInfo, rename_info,
sizeof(buffer));
CloseHandle(handle);
if (!ret)
return 0;

handle = CreateFileW(wpath, DELETE,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (handle == INVALID_HANDLE_VALUE)
return 0;

ret = SetFileInformationByHandle(handle, FileDispositionInfo,
&disposition_info,
sizeof(disposition_info));

CloseHandle(handle);
return !!ret;
}

int mingw_unlink(const char *pathname)
{
int tries = 0;
Expand All @@ -543,6 +596,9 @@ int mingw_unlink(const char *pathname)
if (DeleteFileW(wpathname))
return 0;

if (rename_in_use_file_to_alternate_data_stream(wpathname))
return 0;

do {
/* read-only files cannot be removed */
_wchmod(wpathname, 0666);
Expand Down

0 comments on commit eb321e2

Please sign in to comment.