13

As described here, using SetFileInformationByHandle with FILE_DISPOSITION_INFO allows one to set a file with an open handle to be deleted upon all handles being closed.

However, I am trying to delete a file based on its file index (disk ID) retrieved by FILE_DISPOSITION_INFO and OpenFileById in order to safely delete files/directories in a directory which differ only in case. This is safe to do in my use case, as on an NTFS system, file indexes are persistent until deletion, negating the use of ReplaceFile, which the current codebase handles.

However, when attempting to delete the handle, I get error 87 (ERROR_INVALID_PARAMETER). If I delete using a handle created with CreateFileW, I run into no problems. I can't do this, though, as Windows will not be able to distinguish between two file/folders of the same case, even though NTFS can.

I am also aware that there is an ambiguity with hardlinked files opened with OpenFileById, as hardlinked files share the same disk ID. The issue of hardlinked files can be considered irrelevant for this scenario. I will only be deleting directories by ID, which cannot be hardlinked.

Is there a parameter or setting I am missing in my OpenFileById call? Somehow, in my SetFileInformationByHandle call?

Additional methods I have tried:

  • Calling DuplicateHandle with the OpenFileById handle, providing DELETE for dwDesiredAccess, and using that. Same ERROR_INVALID_PARAMETER result.
  • Using ReOpenFile with the OpenFileById handle, providing DELETE for dwDesiredAccess, and using that. Same ERROR_INVALID_PARAMETER result.
  • Using ReOpenFile with the OpenFileById handle, providing DELETE for dwDesiredAccess, and providing the FILE_FLAG_DELETE_ON_CLOSE flag. No error is given, but the file remains after all handles are closed.

Here is a minimal, yet complete, example which reproduces the problem:

#include <stdio.h>
#include <sys/stat.h>
#include <Windows.h>

DWORD getFileID(LPCWSTR path, LARGE_INTEGER *id)
{
    HANDLE h = CreateFileW(path, 0, 0, 0, OPEN_EXISTING,
        FILE_FLAG_OPEN_REPARSE_POINT |
        FILE_FLAG_BACKUP_SEMANTICS |
        FILE_FLAG_POSIX_SEMANTICS,
        0);
    if (h == INVALID_HANDLE_VALUE)
        return GetLastError();

    BY_HANDLE_FILE_INFORMATION info;
    if (!GetFileInformationByHandle(h, &info))
    {
        DWORD err = GetLastError();
        CloseHandle(h);
        return err;
    }
    id->HighPart = info.nFileIndexHigh;
    id->LowPart = info.nFileIndexLow;
    CloseHandle(h);
    return ERROR_SUCCESS;
}

DWORD deleteFileHandle(HANDLE fileHandle)
{
    FILE_DISPOSITION_INFO info;
    info.DeleteFileW = TRUE;
    if (!SetFileInformationByHandle(
        fileHandle, FileDispositionInfo, &info, sizeof(info)))
    {
        return GetLastError();
    }
    return ERROR_SUCCESS;
}

int wmain(DWORD argc, LPWSTR argv[])
{
    if (argc != 3)
    {
        fwprintf(stderr, L"Arguments: <rootpath> <path>\n");
        return 1;
    }

    DWORD err;
    HANDLE rootHandle = CreateFileW(
        argv[1], 0, 0, 0, OPEN_EXISTING,
        FILE_FLAG_OPEN_REPARSE_POINT |
        FILE_FLAG_BACKUP_SEMANTICS |
        FILE_FLAG_POSIX_SEMANTICS,
        0);
    if (rootHandle == INVALID_HANDLE_VALUE)
    {
        err = GetLastError();
        fwprintf(stderr,
            L"Could not open root directory '%s', error code %d\n",
            argv[1], err);
        return err;
    }

    LARGE_INTEGER fileID;
    err = getFileID(argv[2], &fileID);
    if (err != ERROR_SUCCESS)
    {
        fwprintf(stderr,
            L"Could not get file ID of file/directory '%s', error code %d\n",
            argv[2], err);
        CloseHandle(rootHandle);
        return err;
    }
    fwprintf(stdout,
        L"The file ID of '%s' is %lld\n",
        argv[2], fileID.QuadPart);

    FILE_ID_DESCRIPTOR idStruct;
    idStruct.Type = FileIdType;
    idStruct.FileId = fileID;
    HANDLE fileHandle = OpenFileById(
        rootHandle, &idStruct, DELETE, FILE_SHARE_DELETE, 0,
        FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS);
    if (fileHandle == INVALID_HANDLE_VALUE)
    {
        err = GetLastError();
        CloseHandle(rootHandle);
        fwprintf(stderr,
            L"Could not open file by ID %lld, error code %d\n",
            fileID.QuadPart, err);
        return err;
    }

    err = deleteFileHandle(fileHandle);
    if (err != ERROR_SUCCESS)
    {
        fwprintf(stderr,
            L"Could not delete file by ID '%lld', error code %d\n",
            fileID.QuadPart, err);
    }

    CloseHandle(fileHandle);
    struct _stat _tmp;
    fwprintf(stdout,
        L"File was %ssuccessfully deleted\n",
        (_wstat(argv[2], &_tmp) == 0) ? L"not " : L"");
    CloseHandle(rootHandle);
    return err;
}

Any solution must work with Vista and above. Suggestions for code improvement are also welcome.

Community
  • 1
  • 1
Alyssa Haroldsen
  • 3,652
  • 1
  • 20
  • 35
  • Try using DuplicateHandle on the handle you get from OpenFileById, with `dwDesiredAccess` set to `DELETE`. – Harry Johnston Mar 26 '16 at 00:13
  • @HarryJohnston Good idea, unfortunately didn't work. I've edited the question to include what I've tried so far. – Alyssa Haroldsen Mar 27 '16 at 12:55
  • Presumably the system kernel is configured to be case sensitive, or you wouldn't have such files in the first place; I take it using NtCreateFile isn't an option? – Harry Johnston Mar 27 '16 at 22:37
  • @HarryJohnston The files were created in Linux. Theoretically, the program should also be able to work with files with invalid names if it can delete by disk ID. – Alyssa Haroldsen Mar 27 '16 at 22:40
  • It doesn't look like it can, though. The problem presumably being that a disk ID identifies a file rather than a directory entry, and you can only delete a directory entry. Granted removing a directory by disk ID would be possible in principle since in NTFS a directory can't be hard linked, it doesn't look like Windows actually supports doing so. I think you'll need to [set obcaseinsensitive to zero](https://technet.microsoft.com/en-nz/library/cc725747.aspx) and use the kernel API. – Harry Johnston Mar 28 '16 at 02:52
  • Alternatively, if you don't need to keep the conflicting names, it should be possible to rename them one at a time - presumably if you open a file by name when there are multiple matches you get a handle to one of them at random? – Harry Johnston Mar 28 '16 at 02:55
  • @HarryJohnston Unfortunately, this will be used on machines where setting kernel settings and restarting would be impossible. As far as I can tell in my experimentation, opening a file by name when there are multiple matches opens the one with the first filename when sorted, so capitals first. It's confusing that using [GetFinalPathNameByHandle](https://msdn.microsoft.com/en-us/library/windows/desktop/aa364962(v=vs.85).aspx) gives the first hardlink path of a file. I have absolutely no experience in Windows kernel development. If I could use the POSIX subsystem simply, this would be easy. – Alyssa Haroldsen Mar 28 '16 at 03:24
  • I gather the POSIX subsystem no longer exists in the newest Windows releases, so no help there. There's still an NFS server so that's a potential option, but not available on all editions of Windows. What about renaming them so they don't conflict? If there are only two directories with matching names, and you want to remove one of them anyway, that should work perfectly - you don't need to be able to predict which one will wind up with which name, as you can check the file IDs. Another thought: could you mount the file system in question in a VM? – Harry Johnston Mar 28 '16 at 21:22
  • @HarryJohnston The primary restriction is that is must work on Vista+ with default configurations, and be capable of running in the background. So, no NFS, no installations, and no reboots. A problem I see with renaming is that when a child of a directory is locked, the directory cannot be renamed (IIRC). The strange thing is I'd expect `SetFileInformationByHandle` to return `ERROR_INVALID_HANDLE` if the handle was incapable. It's also possible to find the first file path of a `OpenFileById` handle. Lastly, renaming simply wouldn't work for paths with "invalid" characters, like ``\`` or `*`. – Alyssa Haroldsen Mar 29 '16 at 07:32
  • OK, so POSIX wouldn't have helped in the first place, never mind then. I'm not sure why a Linux directory would be in use while Windows has the file system mounted, but if it is you won't be able to rename it. You hadn't mentioned invalid characters before, I'm not sure you can do anything with such a file in Windows even via the kernel, I think you would have to dismount the file system and manipulate it directly. Given all your requirements, I think the answer is simply that this isn't possible. Someone else might have a clever idea though, so good luck. – Harry Johnston Mar 29 '16 at 20:23
  • (Forgot to mention, `ERROR_INVALID_HANDLE` is typically only used if the handle you pass isn't actually a handle or is a handle to the completely wrong kind of object, e.g., calling SetEvent on a file handle. Using a handle of the right sort but that lacks the necessary properties typically produces `ERROR_INVALID_PARAMETER` or `ERROR_ACCESS_DENIED`. For example, calling DeleteFile on a directory produces `ERROR_ACCESS_DENIED`.) – Harry Johnston Mar 29 '16 at 21:05
  • @HarryJohnston In a dual-booted system with Linux, mounting an NTFS partition opens it in "POSIX mode", where the only restrictions are that a path cannot contain `NUL` or `/`. A music manager that edits paths would add `:` to some song files. The paths allowed by Windows are a subset. Using a POSIX namespace (without installation) would fix this when files created in Linux are attempted to be deleted in Windows, but I don't see how. The peculiar thing is that you can use `GetFinalPathNameByHandle` and still get the name of the first hardlink of a file opened by ID, even the path is invalid. – Alyssa Haroldsen Mar 31 '16 at 07:57
  • Hmmm. It might (or might not) be possible to manipulate the files by passing the relevant control codes directly to the NTFS driver, bypassing the kernel's file system support. But it would probably have to be done from a device driver, not an application. (It might not need to be a kernel-mode device driver necessarily.) It would definitely be challenging for anyone without device driver (and preferable file system driver) experience. – Harry Johnston Mar 31 '16 at 21:44
  • 1
    SWAG: As dumb as it sounds, might you not need to include POSIX_SEMANTICS on the OpenFileById call to get a "compatible" handle? Yeah - I know the POSIX_SEMANTICS nominally only has to do with file names...but there are dumber things in the world. @HarryJohnston mentioned the ERROR_INVALID_PARAMETER - maybe internally, it's "grossly" comparing flags. – Clay Apr 01 '16 at 13:40
  • If you're not *too* concerned with performance, you might try `NTQueryInformationFile` querying for `FILE_NAME_INFORMATION`, which takes the file handle and returns a full path to the file, which you can turn around and open with `CreateFile()`. In such a case, you might still want to use POSIX_SEMANTICS to ensure you're getting compatible behavior by the underlying subsystem when you open the file w/ OpenFileById() – Clay Apr 01 '16 at 14:03
  • @Clay `OpenFileById` does not take the `FILE_FLAG_POSIX_SEMANTICS` flag. Also, querying for the filename will still fail if the filename is invalid/differs only in case. – Alyssa Haroldsen Apr 03 '16 at 02:17
  • 1
    For the record, the [documentation here: File System Behavior Overview (PDF)](http://download.microsoft.com/download/4/3/8/43889780-8d45-4b2e-9d3a-c696a890309f/File%20System%20Behavior%20Overview.pdf) confirms (section 4.3.2) that you can't set the delete-on-close flag for a handle that was opened by ID. – Harry Johnston Jul 05 '16 at 00:37
  • @HarryJohnston Thank you for (as far as I can tell) *officially* confirming this is unsupported by Microsoft. Is there any other option you know to guarantee the deletion of a file with incredibly unusual names? Is there everywhere-installed access to the POSIX subsystem hanging around somewhere I can't find? – Alyssa Haroldsen Jul 05 '16 at 04:08
  • I wouldn't necessarily know if there was, but it seems unlikely. Windows doesn't generally support that sort of edge case out of the box. – Harry Johnston Jul 05 '16 at 21:04

4 Answers4

2

There's a user mode version of the kernel mode ZwCreateFile called NTCreteFile which, among other things will give you all of the access rights you can't get with OpenFileById (but you can get with CreateFile). It can do everything CreateFile can do and more. For example, it can even create directories.

The good part is, there's an immensely hacky (but entertaining) way of specifying a file ID in the POBJECT_ATTRIBUTES argument as well, so you get the best of all worlds...except that it's an even more awkward API to call than your run-of-the-mill awkward Windows APIs.

There are two versions of the documentation. One at:

https://msdn.microsoft.com/en-us/library/bb432380(v=vs.85).aspx

and one at:

https://msdn.microsoft.com/en-us/library/windows/hardware/ff556465(v=vs.85).aspx

...which links to the ZwCreateFile documentation at:

https://msdn.microsoft.com/en-us/library/windows/hardware/ff566424(v=vs.85).aspx

The reason I point this out is that the first article omits some of the goodies (like opening files by ID) that are documented in the last article. I have found this to be common and have also found that most of the documented Zwxxx functionality actually does exists in the equivalent, but incompletely documented NTxxx functions. So you gotta hold your mouth just right to get the requisite functionality.

Clay
  • 4,999
  • 1
  • 28
  • 45
1

In order to make FILE_DISPOSITION_INFO work you need to specify the DELETE access in the CreateFile function as reported in https://msdn.microsoft.com/en-us/library/windows/desktop/aa365539(v=VS.85).aspx:

You must specify appropriate access flags when creating the file handle for use with SetFileInformationByHandle. For example, if the application is using FILE_DISPOSITION_INFO with the DeleteFile member set to TRUE, the file would need DELETE access requested in the call to the CreateFile function. To see an example of this, see the Example Code section. For more information about file permissions, see File Security and Access Rights. I.e.

//...
  HANDLE hFile = CreateFile( TEXT("tempfile"), 
                             GENERIC_READ | GENERIC_WRITE | DELETE,  //Specify DELETE access!
                             0 /* exclusive access */,
                             NULL, 
                             CREATE_ALWAYS,
                             0, 
                             NULL);

But it seems that an handle created with OpenFileById() cannot be used because the function cannot accept the DELETE flag.
From https://msdn.microsoft.com/en-us/library/windows/desktop/aa365432(v=vs.85).aspx on OpenFileById() it can be read: dwDesired

Access [in]
The access to the object. Access can be read, write, or both.

Even setting DELETE or GENERIC_ALL the function fails.
If you replace the handle passed to SetFileInformationByHandle with one created with the CreateFile function having the DELETE flag set, as above, it works.

Frankie_C
  • 4,764
  • 1
  • 13
  • 30
  • `CreateFile` is not being used to create the handle that `FILE_DISPOSITION_INFO` is being used on, `OpenFileById` is. `GENERIC_ALL` was used for access, which includes delete. Replacing it with `GENERIC_READ | GENERIC_WRITE | DELETE` makes no difference. – Alyssa Haroldsen Mar 25 '16 at 10:21
  • I already know using `CreateFile` works, as described. This still has the problem of file paths not being unique with `CreateFile`. Is it possible to delete a file by disk ID in general, or reliably delete two files which differ only in case, if one is a file and the other a directory? – Alyssa Haroldsen Mar 25 '16 at 11:46
  • MS file system is not case sensitive, so in general the answer is no. I have checked what for me is the last chance: using `ReOpenFile` to reopen the flie created with `OpenFileById` adding the `DELETE` flag, but also this solution doesn't work :( – Frankie_C Mar 25 '16 at 12:32
  • The problem is NTFS is case sensitive but Windows is not. Good idea on `ReOpenFile`, sad it didn't work. FindNextFile and similar methods still see them as separate files, however. – Alyssa Haroldsen Mar 25 '16 at 13:08
  • It seems that `OpenFileById` is crafted for internetwork files, so have a limited access to the real file. Maybe there is a function to change something on the opened file. Not one of the most used, nor one that I could recall. Maybe you want search in that direction something like `ReOpenFile`... – Frankie_C Mar 25 '16 at 14:28
0

Have you looked into FILE_FLAG_POSIX_SEMANTICS? It will allow you to open files that differ only in case using CreateFile.

Edit: I guess I should have read your code first as I see you are using said flag.

Red Bug
  • 33
  • 5
  • Doesn't work. Have you tried it yourself? Also fails with invalid names. – Alyssa Haroldsen Apr 04 '16 at 01:17
  • I'm trying your sample right now and I can reproduce the behavior. I guess my question to you now is which part do you want to get working, deleting files that differ only in case or deleting files opened by ID? – Red Bug Apr 04 '16 at 01:36
  • Deleting files opened by ID, one benefit being able to delete files which differ only by case, as well as invalid filenames, such as those that contain `:`. `FILE_FLAG_POSIX_SEMANTICS` also does not work as described if the kernel is not currently case insensitive, which is the default. If you can form a code sample which you verify works, that might be useful. – Alyssa Haroldsen Apr 04 '16 at 01:45
-1

Assume the files are XXX and xxx and you want to delete XXX.

  1. MoveFile("XXX", "I think it's XXX")
  2. If XXX got renamed, then DeleteFile("I think it's XXX")
  3. Otherwise, DeleteFile("XXX"); MoveFile("I think it's XXX", "xxx")

As to OpenFileById, as you noted, there is a potential ambiguity with a file with multiple names (aka hard links). Allowing DELETE access could cause havoc with this, with an unexpected name being deleted (if it were left to the file system to select which one). I suspect they opted for the simple case of never letting DELETE access be granted.

A similar argument could be made for allowing hard links to directories. Sure, you could do it some of the time correctly, but once you created a cycle, things get a lot tougher...

MJZ
  • 1,074
  • 6
  • 12