0

Has anyone managed to figure out how asynchronous calls to NtQueryDirectoryFile work?

By an asynchronous call, I mean calling NtQueryDirectoryFile on directories not opened with FILE_SYNCHRONOUS_IO_ALERT or with FILE_SYNCHRONOUS_IO_NONALERT.

For me, it seems to return STATUS_PENDING just like a normal NtReadFile request does on a normal file, but when I tried using NtWaitForSingleObject on the directory, it didn't end properly, and I still don't get all the data... why does this happen?

user541686
  • 205,094
  • 128
  • 528
  • 886
  • Why do you feel you need to use async I/O for a query directory? – Larry Osterman Mar 12 '11 at 16:46
  • @Larry: Because synchronous I/O on `\Windows\WinSxS` and its subfolders is painfully slow on a hard disk, and I could use some parallelism when enumerating the disk. – user541686 Mar 12 '11 at 18:43
  • I'd be astonished if you were blocked on I/O reading the WinSxS directory (there's not a lot of I/O needed to read the directory content). If you want parallelism, why not do the I/O synchronously on a separate thread? – Larry Osterman Mar 13 '11 at 02:31
  • @Larry: Really? Try rebooting, then listing the contents of that directory on Windows 7. You'd be surprised... it literally takes ~5 seconds for it to read the entire directory before it even *starts* enumerating (assuming a defragmented and an otherwise idle drive, that is)... have you tried it? I would use another thread, but it's not something I want to do if I can avoid it. – user541686 Mar 13 '11 at 02:38
  • You shouldn't be seeing a 5 second delay in reading the WinSxS directory (neither I or the NTFS developer see this - on my machine (Win7 x64) it took .5s to start enumerating the 13K directories in WinSxS). Is it possible that something is interfering with your reads (possibly an antivirus application)? – Larry Osterman Mar 17 '11 at 15:03
  • @Larry: Nope, nothing is interfering with my reads. Do you have a hard disk or an SSD? And did you reboot? I can reproduce this every time on my laptop with a 5400 RPM SATA drive. – user541686 Mar 17 '11 at 16:20
  • Hard disk, 7200 RPM sata drive. – Larry Osterman Mar 17 '11 at 23:00
  • @Larry: Did you do this on a *cold* reboot, with *no* prefetching or anything starting the process for you before you actually start enumerating? It's certainly not what happens on my computer... – user541686 Mar 17 '11 at 23:33

2 Answers2

5

As far as I know, none of the Windows filesystems support asynchronous query directory calls. The Win32 APIs never call NtQueryDirectoryFile asnchronously, so support for it is hit-or-miss.

NTFS theoretically supports asynchronous NtQueryDirectoryFile but (as I mentioned) it is not extensively tested so it may not work.

You response indicated that you called WaitForSingleObject on the directory - that's not how the async pattern works in NT - you need to call WaitForSingleObject on the event handle provided as a parameter to NtQueryDirectoryFile.

This update is a result of asking the NTFS developer for more information, he tested this scenario on his machine and it worked for him (on Windows 7).

Larry Osterman
  • 16,086
  • 32
  • 60
  • Hm... then why do they return `STATUS_PENDING` along with partial data? – user541686 Mar 12 '11 at 18:44
  • I don't know. If I were to speculate, I'd guess that the status_pending is generated by the I/O subsystem (above the filesystems) and the partial results are whatever could be satisfied by the cache at the time of the read. But I'm not a filesystem guy so I don't know. – Larry Osterman Mar 13 '11 at 02:32
  • May I ask why you believe the async calls aren't supported? Is it just a guess, or did you happen to read it somewhere? – user541686 Mar 13 '11 at 02:40
  • It's a guess - thats why the "as far as I know" comment. I do know that Win32 always issues the query directory calls synchronously. – Larry Osterman Mar 13 '11 at 15:42
  • 1
    Please note that I've massively rewritten my answer with information from the deveoper who owns the NTFS filesystem. – Larry Osterman Mar 17 '11 at 15:01
  • @Larry: +1 Wow, thanks for the further research!! Just another small question: How come I can't wait on the file handle parameter? The documentation itself says that the `EventHandle`"must be NULL if the caller will wait for the FileHandle to be set to the Signaled state.", and I've tried this with regular files and it works -- why not directories? – user541686 Mar 17 '11 at 15:44
  • @Larry: Actually, using the event parameter is the same: I get `STATUS_PENDING`, and when I wait on the event handle, I get `STATUS_SUCCESS` with around 100 bytes of transferred data; however, there is no data actually transferred. Would you mind providing a test case? I have a feeling I might be doing something wrong, but I can't figure out what. – user541686 Mar 17 '11 at 16:18
  • You can't wait on the file handle because that's not the way that asynchronous I/O works at the NT API level. You only wait on the file handle when you're using synchronous I/O. The STATUS_IO_PENDING result will always occur when the file handle's opened for async access. The MSDN documentation is correct. – Larry Osterman Mar 17 '11 at 23:00
  • @Larry: From what I know, every file object has an associated event with itself. When you wait on the file handle, the system automatically waits on the event handle -- I don't understand what's wrong. This is how ReactOS works, and it works *completely fine* on asynchronous files; it's only not working for directories. I am almost 100% sure that doing this is correct -- if you specify an event handle, you can't wait for the file handle (it deadlocks), but if you don't, the wait is successful. I'm confused at why you're saying that's not valid; it seems to be correct... – user541686 Mar 17 '11 at 23:32
  • @Larry: By the way, another question: You mentioned that `You only wait on the file handle when you're using synchronous I/O.`. However, what exactly would you be waiting for if it's already doing synchronous I/O? – user541686 Mar 18 '11 at 00:20
  • @Mehrdad: The only way to know if an async I/O is completed is when the eventHandle is set to the signalled state. You should *only* wait for the file handle if the file is opened for synchronous I/O. – Larry Osterman Mar 18 '11 at 03:52
  • @Larry: Would you mind taking a look [at this program](http://processhacker.sourceforge.net/doc/iosup_8c_source.html#l00578)? It *clearly* states that we wait on an **asynchronous** file handle. Could you please explain **why** we can't wait on the file handle, rather than just saying we shouldn't do that? [The MS documentation for the `Event` field of `FILE_OBJECT`](http://msdn.microsoft.com/en-us/library/ff545834.aspx) *clearly* states: "The event object is used to signal the completion of an I/O request on the file object **if no user event was supplied or a synchronous API was called**.".. – user541686 Mar 18 '11 at 04:08
  • @Larry: ... The last part clearly means that it's *completely possible* to have an **asynchronous file read** followed by a *wait on the **file handle***. You don't need an event handle because the system already creates and manages it for you, and internally uses the event handle when you wait on the object. – user541686 Mar 18 '11 at 04:14
  • The reason you can't wait on the file handle is that it's not how the API works. The file handle is never set to the not signalled state, so the wait returns immediately. The contract for all the Nt* I/O APIs is that if the file handle isn't opened for synchronous access, you can't wait on the file handle. Instead you wait on the event handle you pass into the API. – Larry Osterman Mar 18 '11 at 14:06
  • @Larry: Did you *actually read* what I wrote in the last comment? It says *if no user event was supplied **OR** a synchronous API was called*. – user541686 Mar 18 '11 at 14:32
  • @Mehrdad: Did you read my response? If you don't specify syncrhonous I/O, the only way to make the Nt* I/O APIs work reliably is if you provide an event or an APC object. There's a reason that the Nt* APIs aren't generally available - they're extremely hard to use when compared to the Win2 APIs. – Larry Osterman Mar 18 '11 at 15:05
  • @Larry: I read your response, but you're not citing a single reference for it *or explaining why my method **always** works for files*, and *why other people use this method too*. Is everybody (including MS) wrong here? I've shown you quite a few links demonstrating the use of waiting on a file handle, but you just keep on saying it's not the right thing to do, without providing a single link supporting what you're saying -- and I've shown you that MS's *own* documentation states that an async API can be called without a user event. – user541686 Mar 18 '11 at 15:20
  • The references I have are the email sitting in my inbox from the developer at Microsoft who owns the NTFS filesystem (he's the one who said you needed to provide an event to make this work reliably) and my 20+ years of experience working with the low level NT API (I wrote the first network filesystem for NT starting in 1989). If you read the MSDN article on async I/O (http://msdn.microsoft.com/en-us/library/aa365683%28VS.85%29.aspx) it says that if you don't provide an event, the results are unreliable. – Larry Osterman Mar 18 '11 at 15:41
  • @Larry: Notice what your own link says: `An application can also wait on the file handle to synchronize the completion of an I/O operation, but doing so requires extreme caution. Each time an I/O operation is completed, the operating system sets the file handle to the signaled state. Therefore, if an application starts two I/O operations and waits on the file handle, there is no way to determine which operation is finished when the handle is set to the signaled state.` However, notice that *if you only do a single operation on the handle* (which I always do), there is **nothing** wrong here. – user541686 Mar 18 '11 at 16:15
  • @Mehrdad: I told you what the developer of the NTFS filesystem said about this. I told you what my 20+ years of experience in working with the low level NT APIs say about this. But you continue to insist that the NTFS developer and I are incorrect. I don't know why you continue to insist that *the developer who owns the code for the NTFS filesystem* doesn't know what he's talking about but for me, I'm done with this discussion. – Larry Osterman Mar 18 '11 at 18:21
  • @Larry: Well, it's just the lack of support (i.e. sample code) that has left me unconvinced: you've just mentioned it's "unreliable" and that I shouldn't use it because you say so, without saying why. If you could provide *an example showing a **single** file operation in which waiting on event handles works but waiting on a file handle doesn't*, that would be very convincing for anyone, including me. Otherwise, as much as I respect your experiences at Microsoft, I just don't find "take my word for it" to be a very useful or convincing argument, as true as it may be. All I need is an example.. – user541686 Mar 18 '11 at 18:44
0

NtQueryDirectoryFile works well in asynchronous!

pass callback in ApcRoutine, and callback data in ApcContext

asynchronous procedure calls only call when the thread is in alertable state(for example: calling SleepEx(INFINITE, TRUE), WSAaccept)

this program shows how asynchronous NtQueryDirectoryFile work.

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <stdio.h>
#include <winternl.h>
#include <winnt.h>

#define LIST_DIR_SIZE 2000
#define STATUS_NO_MORE_FILES ((NTSTATUS)80000006)

typedef struct _FILE_NAMES_INFORMATION {
    ULONG NextEntryOffset;
    ULONG FileIndex;
    ULONG FileNameLength;
    WCHAR FileName[1];
} FILE_NAMES_INFORMATION, * PFILE_NAMES_INFORMATION;

typedef struct {
    HANDLE hFile;
    OVERLAPPED ol;
    DECLSPEC_ALIGN(4) FILE_NAMES_INFORMATION buf[LIST_DIR_SIZE];
    IO_STATUS_BLOCK iob;
    bool finished;
} LIST_DIR_DATA, * PLIST_DIR_DATA; // my private data


__kernel_entry NTSYSCALLAPI
NTSTATUS
NTAPI
NtQueryDirectoryFile(
    _In_ HANDLE FileHandle,
    _In_opt_ HANDLE Event,
    _In_opt_ PIO_APC_ROUTINE ApcRoutine,
    _In_opt_ PVOID ApcContext,
    _Out_ PIO_STATUS_BLOCK IoStatusBlock,
    _Out_writes_bytes_(Length) PVOID FileInformation,
    _In_ ULONG Length,
    _In_ FILE_INFORMATION_CLASS FileInformationClass,
    _In_ BOOLEAN ReturnSingleEntry,
    _In_opt_ PUNICODE_STRING FileName,
    _In_ BOOLEAN RestartScan
);

#define NTDLL_extern(s) typedef decltype(&s) s##T;s##T s##F;
#define NTDLL_import(s) s##F = (s##T)GetProcAddress(ntdll, #s);

NTDLL_extern(NtOpenFile);
NTDLL_extern(NtQueryDirectoryFile);
NTDLL_extern(NtClose);
NTDLL_extern(RtlInitUnicodeString);

HMODULE ntdll;

VOID NTAPI callback(
    IN PVOID ApcContext,
    IN PIO_STATUS_BLOCK IoStatusBlock,
    IN ULONG Reserved) {

    UNREFERENCED_PARAMETER(Reserved);
    PFILE_NAMES_INFORMATION file_info = ((PLIST_DIR_DATA)ApcContext)->buf;
    do {
        fputws(file_info->FileName, stdout);
        putwchar(L'\t');
        file_info = (PFILE_NAMES_INFORMATION)((char*)file_info + file_info->NextEntryOffset);
    } while (file_info->NextEntryOffset);
    fputws(file_info->FileName, stdout);
    putwchar(L'\t');
    PLIST_DIR_DATA c = (PLIST_DIR_DATA)ApcContext;
    if (IoStatusBlock->Information != 0) {
        NTSTATUS status = NtQueryDirectoryFileF(
            c->hFile,
            NULL,
            callback,
            ApcContext,
            &c->iob,
            c->buf,
            sizeof(c->buf),
            FILE_INFORMATION_CLASS(12),
            FALSE, NULL, FALSE);
        switch (status) {
        case STATUS_PENDING:
            break;
        default:
            fputs("warning: status != STATUS_PENDING", stderr);
        }
    }
    else {
        c->finished = true;
    }
}
BOOL init() {
    ntdll = LoadLibraryW(L"NtDLL.dll");
    if (ntdll == NULL) {
        fputs("LoadLibraryW", stderr);
        return FALSE;
    }
    NTDLL_import(NtQueryDirectoryFile);
    NTDLL_import(NtOpenFile);
    NTDLL_import(NtClose);
    NTDLL_import(RtlInitUnicodeString);
    if (NtCloseF != NULL && NtOpenFileF != NULL && NtCloseF != NULL) {
        return TRUE;
    }
    else {
        fputs("GetProcAddress", stderr);
        return FALSE;
    }
}

int main() {
    if (init() == FALSE) {
        fputs("error: init() failed!", stderr);
        return -1;
    }
    NTSTATUS status;
    PLIST_DIR_DATA data = new LIST_DIR_DATA{};
    {
        OBJECT_ATTRIBUTES ObjectAttributes;
        UNICODE_STRING s;
        RtlInitUnicodeStringF(&s, L"\\??\\c:\\Windows\\System32");
        InitializeObjectAttributes(
            &ObjectAttributes,
            &s,
            OBJ_CASE_INSENSITIVE,
            NULL,
            NULL);
        status = NtOpenFileF(
            &data->hFile,
            FILE_READ_DATA | FILE_LIST_DIRECTORY, // | FILE_TRAVERSE | SYNCHRONIZE
            &ObjectAttributes,
            &data->iob,
            FILE_SHARE_READ,
            FILE_DIRECTORY_FILE); // | FILE_SYNCHRONOUS_IO_NONALERT
    }
    if (status < 0 || data->hFile == NULL) {
        fputs("error: NtOpenFile failed", stderr);
        return -2;
    }
    status = NtQueryDirectoryFileF(
        data->hFile,
        NULL,
        callback,
        data,
        &data->iob,
        data->buf,
        sizeof(data->buf),
        FILE_INFORMATION_CLASS(12),
        FALSE, NULL, FALSE);
    switch (status) {
        case STATUS_PENDING: 
            break;
        default:
            fputs("warning: status != STATUS_PENDING", stderr);
    }
    for (;data->finished==false;) SleepEx(INFINITE, TRUE); // put main thread into alertable wait

    NtCloseF(data->hFile);
    FreeLibrary(ntdll);
    return 0;
}

enter image description here

if you want UTF-8 output, try this (note: recommand use support UTF-8 terminal)

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <winternl.h>
#include <winnt.h>
#include <crtdbg.h>
#include <cstdio>

#define LIST_DIR_SIZE 200
#define STATUS_NO_MORE_FILES ((NTSTATUS)80000006)

typedef struct _FILE_NAMES_INFORMATION {
    ULONG NextEntryOffset;
    ULONG FileIndex;
    ULONG FileNameLength;
    WCHAR FileName[1];
} FILE_NAMES_INFORMATION, * PFILE_NAMES_INFORMATION;

typedef struct {
    HANDLE hFile;
    OVERLAPPED ol;
    DECLSPEC_ALIGN(4) FILE_NAMES_INFORMATION buf[LIST_DIR_SIZE];
    IO_STATUS_BLOCK iob;
    bool finished;
} LIST_DIR_DATA, * PLIST_DIR_DATA; // my private data


__kernel_entry NTSYSCALLAPI
NTSTATUS
NTAPI
NtQueryDirectoryFile(
    _In_ HANDLE FileHandle,
    _In_opt_ HANDLE Event,
    _In_opt_ PIO_APC_ROUTINE ApcRoutine,
    _In_opt_ PVOID ApcContext,
    _Out_ PIO_STATUS_BLOCK IoStatusBlock,
    _Out_writes_bytes_(Length) PVOID FileInformation,
    _In_ ULONG Length,
    _In_ FILE_INFORMATION_CLASS FileInformationClass,
    _In_ BOOLEAN ReturnSingleEntry,
    _In_opt_ PUNICODE_STRING FileName,
    _In_ BOOLEAN RestartScan
);

#define NTDLL_extern(s) typedef decltype(&s) s##T;s##T s##F;
#define NTDLL_init(s) s##F = (s##T)GetProcAddress(ntdll, #s);

NTDLL_extern(NtOpenFile);
NTDLL_extern(NtQueryDirectoryFile);
NTDLL_extern(NtClose);
NTDLL_extern(RtlInitUnicodeString);

HMODULE ntdll;
HANDLE heap;

VOID NTAPI callback(
    IN PVOID ApcContext,
    IN PIO_STATUS_BLOCK IoStatusBlock,
    IN ULONG Reserved) {
    UNREFERENCED_PARAMETER(Reserved);
    PLIST_DIR_DATA c = (PLIST_DIR_DATA)ApcContext;
    if (IoStatusBlock->Information){
        PFILE_NAMES_INFORMATION file_info = c->buf;
        ULONG_PTR length = 0;
        ULONG last;
        do {
            last = file_info->NextEntryOffset;
            file_info->FileNameLength /= 2; // wide char length always base of 2 in bytes
            length += (
                file_info->FileIndex=WideCharToMultiByte(
                    CP_UTF8, WC_ERR_INVALID_CHARS, 
                    file_info->FileName, file_info->FileNameLength, 
                    NULL, 0, 
                    NULL, NULL)
                )+1;
            if (file_info->FileIndex == 0) { // FileIndex is how many byte is the UTF-8 string
                _RPTF0(_CRT_WARN, "WideCharToMultiByte failed!");
            }
            file_info = (PFILE_NAMES_INFORMATION)((char*)file_info + file_info->NextEntryOffset);
        } while (last);
        LPSTR pData = (LPSTR)HeapAlloc(heap, HEAP_NO_SERIALIZE, length), ptr=pData;
        if (ptr == NULL) {
            _RPTF0(_CRT_ERROR, "HeapAlloc failed!");
            return;
        }
        file_info = c->buf;
        do {
            last = file_info->NextEntryOffset;
            if (WideCharToMultiByte(
                CP_UTF8, WC_ERR_INVALID_CHARS,
                file_info->FileName, file_info->FileNameLength, 
                pData, file_info->FileIndex, 
                NULL, NULL)==0) {
                _RPTF0(_CRT_WARN, "WideCharToMultiByte failed!");
            }
            pData += file_info->FileIndex;
            *pData++ = '\n';
            file_info = (PFILE_NAMES_INFORMATION)((char*)file_info + file_info->NextEntryOffset);
        } while (last);

        // use data here
        fwrite(ptr, length, 1, stdout);
        // use data here

        HeapFree(heap, HEAP_NO_SERIALIZE, ptr);
        NTSTATUS status = NtQueryDirectoryFileF(
            c->hFile,
            NULL,
            callback,
            ApcContext,
            &c->iob,
            c->buf,
            sizeof(c->buf),
            FILE_INFORMATION_CLASS(12),
            FALSE, NULL, FALSE);

        switch (status) {
        case STATUS_PENDING:
            break;
        default:
            _RPTF0(_CRT_WARN, "status != STATUS_PENDING");
        }
    }else{
        c->finished = true;
    }
}
BOOL init() {
    ntdll = LoadLibraryW(L"NtDLL.dll");
    if (ntdll == NULL) {
        _RPTF0(_CRT_ERROR, "fail to load NtDLL.dll");
        return FALSE;
    }
    NTDLL_init(NtQueryDirectoryFile);
    NTDLL_init(NtOpenFile);
    NTDLL_init(NtClose);
    NTDLL_init(RtlInitUnicodeString);
    if (NtCloseF != NULL && 
        NtOpenFileF != NULL && 
        NtCloseF != NULL && 
        (heap = HeapCreate(HEAP_NO_SERIALIZE, 4096,0))!=NULL
        ){
        return TRUE;
    }
    else {
        _RPTF0(_CRT_ERROR, "failed to load function and create heap");
        return FALSE;
    }
}

int main() {
    if (init() == FALSE) {
        _RPTF0(_CRT_ERROR, "init failed");
        return -1;
    }
    SetConsoleCP(CP_UTF8);
    NTSTATUS status;
    PLIST_DIR_DATA data = new LIST_DIR_DATA{};
    {
        OBJECT_ATTRIBUTES ObjectAttributes;
        UNICODE_STRING s;
        RtlInitUnicodeStringF(&s, L"\\??\\c:\\Users");
        InitializeObjectAttributes(
            &ObjectAttributes,
            &s,
            OBJ_CASE_INSENSITIVE,
            NULL,
            NULL);
        status = NtOpenFileF(
            &data->hFile,
            FILE_READ_DATA | FILE_LIST_DIRECTORY, // | FILE_TRAVERSE | SYNCHRONIZE
            &ObjectAttributes,
            &data->iob,
            FILE_SHARE_READ,
            FILE_DIRECTORY_FILE); // | FILE_SYNCHRONOUS_IO_NONALERT
    }
    if (status < 0 || data->hFile == NULL) {
        _RPTF0(_CRT_ERROR, "NtOpenFile failed!");
        return -2;
    }
    status = NtQueryDirectoryFileF(
        data->hFile,
        NULL,
        callback,
        data,
        &data->iob,
        data->buf,
        sizeof(data->buf),
        FILE_INFORMATION_CLASS(12),
        FALSE, NULL, FALSE);
    switch (status) {
        case STATUS_PENDING: 
            break;
        default:
            _RPTF0(_CRT_WARN, "status != STATUS_PENDING");
    }
    for (;data->finished==false;) SleepEx(INFINITE, TRUE); // put main thread into alertable wait

    if (NtCloseF(data->hFile)<0) {
        _RPTF0(_CRT_ERROR, "NtClose failed!");
    }
    if (FreeLibrary(ntdll) == FALSE) {
        _RPTF0(_CRT_WARN, "failed to Free libary");
    }
    if (HeapDestroy(heap) == FALSE) {
        _RPTF0(_CRT_WARN, "fail to destroy heap");
    }
}

enter image description here

ianfun
  • 411
  • 5
  • 10