PreRead: how to read a different file during Paging I/O

In my minifilter PreRead callback, I'd like to replace the content of some files with different content. The different content is stored in a different file.

Now my main question is: What's the best way to read from a different file within the PreRead callback, especially when the PreRead is Paging I/O?

After some research, I've found 2 promising approaches:

  1. FltPerformSynchronousIo, possibly using paging I/O flag.
  2. Pending the Paging I/O, then doing the work in my own PASSIVE_LEVEL thread.

I think 1) looks more promising, but I've seen Don say that it's risky to use because it's not fully documented. However, his comment was in 2007, and now looking at e.g. the MS NameChanger minifilter driver, it does use FltPerformSynchronousIo (see nccompat.c), and it looks fairly simple, and it seems somewhat documented now?

For 2), I'm a bit worried because it seems "bad" to pend a paging I/O, and I can't use FltQueueDeferredIoWorkItem.

Thoughts?

Maybe does anybody have an already tried & proven APC_LEVEL compatible FltReadFile replacement function available which internally uses FltPerformSynchronousIo?

(P.S: For PASSIVE_LEVEL PreRead events I'll use either FltRead or ZwReadFile directly.)

FWIW, I'm now using this code in PreRead, and it seems to work fine. Does this look ok to you guys?

if (KeGetCurrentIrql() < APC_LEVEL)
{
  // we're at PASSIVE_LEVEL, which is easy, we can simply use FltReadFile
  ULONG bytesRead = 0;
  Data->IoStatus.Status = FltReadFile(GlobalData.FilterInstance, GlobalData.VirtualFileObject, &byteOffset, bytesToRead, buf, 0, &bytesRead, NULL, NULL);
  Data->IoStatus.Information = NT_SUCCESS(Data->IoStatus.Status) ? bytesRead : 0;
}
else
{
  // ouch, we're at APC_LEVEL, so we're not allowed to use FtlReadFile, instead we use FltPerformSynchronousIo
  PFLT_CALLBACK_DATA callbackData = NULL;
  Data->IoStatus.Status = FltAllocateCallbackData(GlobalData.FilterInstance, GlobalData.VirtualFileObject, &callbackData);
  if ((NT_SUCCESS(Data->IoStatus.Status)) && (callbackData))
  {
    callbackData->Iopb->MajorFunction = IRP_MJ_READ;
    callbackData->Iopb->MinorFunction = IRP_MN_NORMAL;
    callbackData->Iopb->Parameters.Read.Length     = bytesToRead;
    callbackData->Iopb->Parameters.Read.Key        = 0;
    callbackData->Iopb->Parameters.Read.ByteOffset = byteOffset;
    callbackData->Iopb->Parameters.Read.ReadBuffer = buf;
    callbackData->Iopb->Parameters.Read.MdlAddress = NULL;
    callbackData->Iopb->IrpFlags = IRP_READ_OPERATION | IRP_SYNCHRONOUS_API | (Data->Iopb->IrpFlags & IRP_NOCACHE) |
                                   ((Data->Iopb->IrpFlags & IRP_PAGING_IO) ? (IRP_PAGING_IO | IRP_SYNCHRONOUS_PAGING_IO) : 0);
    FltPerformSynchronousIo(callbackData);
    Data->IoStatus.Status      = callbackData->IoStatus.Status;
    Data->IoStatus.Information = callbackData->IoStatus.Information;
    FltFreeCallbackData(callbackData);
  }
}

The other file can be only a page/swap file. Only a pagefile can be accessed when processing a regular file IO (paging or non paging). Otherwise, this is a deadlock prone scenario. A regular file can be opened as a page file with an appropriate set of flags for NtCretaeFile or IoCreateFile, if file system supports it. Retrieving data by a such file object requires a caller to synchronise access to the file. IIRC only paging IO is possible. This is not what is supposed to be done by 3rd party drivers, but it was done in the past by a number of products.

Thank you very much for your reply! I've a couple follow-up questions, if you don't mind:

  1. How/why would a deadlock occur in this situation?

  2. Checking the documentation of both NtCreateFile and IoCreateFile(Ex), I can't seem to find a flag that opens a file as a pagefile. Could you help me find the appropriate set of flags?

  3. This will always be NTFS on a local SSD. Does it support opening a file as a pagefile?

  4. Can I simply use a mutex to synchronize access? But isn't that "bad" when I receive paging I/O requests at APC_LEVEL?

  5. What happens if I don't synchronize access to the other file (opened as a pagefile)?

P.S: FWIW, I only allow read requests to the original file and only do read requests on "the other" file. Attempts to open the original file with write access are blocked by my minifilter. Also, both files are always on the same volume.

Had to dig into 16 years old project.

  1. This is defined by Windows memory subsystem and file system design. The lock and IO hierarchy can be only as follows
- Regular File IO and resources first.
- Paging File IO and resources second.

The Windows memory subsystem and file systems are designed for this hierarchy. That is why there are these callbacks masqueraded as FastIo, like AcquireFileForNtCreateSection and AcquireForCcFlush, as there was no other good place to add them. Reversing this hierarchy, recursively acquiring resources, or doing IO for two regular files will undoubtedly result in a deadlock. Even if it passed your tests, it will deadlock on some client's system with some 3rd party filters.

  1. The flag might not be defined for 3rd party drivers, but it can be defined and used.
//
// IO_OPEN_PAGING_FILE is not defined in the old WDKs
//
#ifndef IO_OPEN_PAGING_FILE
#define IO_OPEN_PAGING_FILE             0x0002
#endif//IO_OPEN_PAGING_FILE

            //
            // create a backup file as a paging one, this helps to tackle with many
            // synchronization issues between ordinary files and the backup file
            //
            status = IoCreateFile( &shadowFile,
                                   FILE_READ_DATA | FILE_WRITE_DATA | WRITE_DAC | SYNCHRONIZE,
                                   &objAttr,
                                   &ioStatusBlock,
                                   NULL,
                                   FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM,
                                   FILE_SHARE_WRITE,
                                   FILE_SUPERSEDE,
                                   FILE_SYNCHRONOUS_IO_NONALERT | FILE_DELETE_ON_CLOSE | FILE_NON_DIRECTORY_FILE |
                                   FILE_RANDOM_ACCESS | FILE_NO_COMPRESSION | FILE_NO_INTERMEDIATE_BUFFERING,
                                   NULL,
                                   0L,
                                   CreateFileTypeNone,
                                   NULL,
                                   IO_OPEN_PAGING_FILE | IO_NO_PARAMETER_CHECKING );
  1. NTFS supports paging files.
  2. Mutex can be used. KeWaitForSingleObject can be called at IRQL <= APC_LEVEL . Just be careful about recursive acquisitions, this must be processed carefully regarding structures consistency.
  3. Memory manager and IO manager do not provide the same level of serialisation for IO in case of paging file IO, so this might result in data integrity issues if IO is not properly serialised.

FYI, some code I used to read files opened as paging files

            //
            // Prepare the MDL for reuse, for synchronous Paging IO 
            // requests the MDL doesn't freed( as in the case of the ordinary requests )
            //
            MmPrepareMdlForReuse( ReaderThread->LocalBuffer.PartialMdl );

            IoBuildPartialMdl( ReaderThread->LocalBuffer.Mdl,
                               ReaderThread->LocalBuffer.PartialMdl,
                               (PCHAR)ReaderThread->LocalBuffer.Address,
                               BytesToRead );

            //
            // the buffer described by the MDL must be aligned to avoid any nasty surprises for file systems
            //
            DLD_ASSERT_BUGCHECK( 0x0 == MmGetMdlByteOffset( ReaderThread->LocalBuffer.PartialMdl ) );

            //
            // MDL must be for the system space!
            //
            DLD_ASSERT_BUGCHECK( (ULONG_PTR)MmGetMdlBaseVa( ReaderThread->LocalBuffer.PartialMdl ) > MAXIMUM_USER_SPACE_ADDRESS );

            DldAcquireBuffersFileForRead( FileHandle );
            { // start of the lock
                RC = DldIoSynchronousPageReadWrite( FileHandle->FileObject, 
                                                    ReaderThread->LocalBuffer.PartialMdl, 
                                                    &StartingOffset, 
                                                    &IoStatusBlock,
                                                    FALSE );
            } // end of the lock
            DldReleaseBuffersFileFromRead( FileHandle );

            //
            // Prepare the MDL for reuse, for synchronous Paging IO 
            // requests the MDL doesn't freed( as in the case of the ordinary requests )
            //
            MmPrepareMdlForReuse( ReaderThread->LocalBuffer.PartialMdl );

The IO was implemented as

NTSTATUS
DldIoSynchronousPageReadWriteSpecifyDeviceObjectHint(
    __in PFILE_OBJECT FileObject,
    __in PMDL MemoryDescriptorList,
    __in ULONG BufferSize, // if 0x0 the size is inferred from the MDL
    __in PLARGE_INTEGER StartingOffset,
    __out PIO_STATUS_BLOCK IoStatusBlock,
    __in BOOLEAN  IsWrite,
    __in_opt PDEVICE_OBJECT FsdStackObject,
    __in BOOLEAN       DataStreamCanBeCached
    )

/*++

Routine Description:

    The caller must free the MemoryDescriptorList!

Arguments:

    FileObject - A pointer to a referenced file object describing which file
        the write should be performed on.

    MemoryDescriptorList - An MDL which describes the physical pages that the
        pages should be written to the disk.  All of the pages have been locked
        in memory.  The MDL also describes the length of the write operation.
        ATTENTION! This MDL will not be freed during the IRP completion the caller 
        is resposible for the MDL freeing.

    StartingOffset - Pointer to the offset in the file from which the write
        should take place.

    IoStatusBlock - A pointer to the I/O status block in which the final status
        and information should be stored.

    FsdStackObject - if not NULL the request will be sent to the specified device else
                     the upper device on the stack will be used

    DataStreamCanBeCached - TRUE if the caller understands that the data can be cached and
                            the data in the cache differs from the data on disk

Return Value:

    The function value is the final status of the queue request to the I/O
    system subcomponents.


--*/

{
    NTSTATUS             RC;
    PIRP                 irp;
    PIO_STACK_LOCATION   irpSp;
    PDEVICE_OBJECT       deviceObject;
    KEVENT               Event;

    DLD_ASSERT( !( FALSE == DataStreamCanBeCached && CcIsFileCached(FileObject) ) );
    DLD_ASSERT( KeGetCurrentIrql() <= APC_LEVEL );
    DLD_ASSERT( IoStatusBlock );
    DLD_ASSERT( MemoryDescriptorList );
    DLD_ASSERT( StartingOffset );
    DLD_ASSERT( 0x0 != MmGetMdlByteCount( MemoryDescriptorList ) );

    KeInitializeEvent( &Event, SynchronizationEvent, FALSE );

    if( 0x0 == BufferSize )
        BufferSize = MmGetMdlByteCount( MemoryDescriptorList );

    DLD_ASSERT( BufferSize <= MmGetMdlByteCount( MemoryDescriptorList ) );

    //
    // Begin by getting a pointer to the device object that the file resides
    // on.
    //

    if( NULL == FsdStackObject )
        deviceObject = IoGetRelatedDeviceObject( FileObject );
    else
        deviceObject = FsdStackObject;

    //
    // Allocate an I/O Request Packet (IRP) for this out-page operation.
    //

    irp = IoAllocateIrp( deviceObject->StackSize, FALSE );
    if (!irp) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    //
    // Get a pointer to the first stack location in the packet.  This location
    // will be used to pass the function codes and parameters to the first
    // driver.
    //

    irpSp = IoGetNextIrpStackLocation( irp );

    //
    // Fill in the IRP according to this request.
    //

    irp->MdlAddress = MemoryDescriptorList;
    irp->Flags = IRP_PAGING_IO | IRP_NOCACHE | IRP_SYNCHRONOUS_PAGING_IO;

    irp->RequestorMode = KernelMode;
    irp->UserIosb = IoStatusBlock;
    irp->UserEvent = &Event;
    irp->UserBuffer = (PVOID) ((PCHAR) MemoryDescriptorList->StartVa + MemoryDescriptorList->ByteOffset);
    irp->Tail.Overlay.OriginalFileObject = FileObject;
    irp->Tail.Overlay.Thread = PsGetCurrentThread();

    //
    // Fill in the normal write parameters.
    //

    if( IsWrite ){

        irpSp->MajorFunction = IRP_MJ_WRITE;
        irpSp->Parameters.Write.Length = BufferSize;
        irpSp->Parameters.Write.ByteOffset = *StartingOffset;
        irpSp->FileObject = FileObject;

    } else {

        irpSp->MajorFunction = IRP_MJ_READ;
        irpSp->Parameters.Read.Length = BufferSize;
        irpSp->Parameters.Read.ByteOffset = *StartingOffset;
        irpSp->FileObject = FileObject;

    }

    //
    // set the completion routine which will zero
    // the MDL pointer on completion
    //
    IoSetCompletionRoutine( irp, 
                            DldPaginReadWriteCompletion,
                            NULL,
                            TRUE, TRUE, TRUE );

    //
    // Queue the packet to the appropriate driver based on whether or not there
    // is a VPB associated with the device.
    //

    RC = IoCallDriver( deviceObject, irp );

    //
    // Wait for the IRP completion
    //
    if( STATUS_PENDING == RC ){

        KeWaitForSingleObject( &Event, 
                               Executive, 
                               KernelMode, 
                               FALSE, 
                               NULL );

        RC = IoStatusBlock->Status;
    }
    DLD_ASSERT( NT_SUCCESS( RC ) || STATUS_END_OF_FILE == RC );

    return RC;
}

//---------------------------------------------------------------------

NTSTATUS 
DldPaginReadWriteCompletion( 
    IN PDEVICE_OBJECT DeviceObject, 
    IN PIRP Irp,
    IN PVOID Context 
    )
{
    //
    // We must not mark Irp as pending, because
    // this routine is set by the request's initiator
    // so while this routine is called
    // the current stack will be invalid.
    //
    DLD_ASSERT( Irp->CurrentLocation == ( Irp->StackCount + 0x1 ) );

    //
    // But for safety check, may be I forgot and 
    // set this routine for the Irp which has 
    // been created not me.
    //
    if( Irp->PendingReturned && Irp->CurrentLocation <= Irp->StackCount ){

        IoMarkIrpPending( Irp );
    }

    //
    // all known kernels do not free Irp's MDL
    // for the paging IO, but in the future this
    // may change, so set pointer to NULL to 
    // avert the kernel from the MDL freeing
    //
    Irp->MdlAddress = NULL;

    return STATUS_SUCCESS;
}
2 Likes

I used a resource object for synchronisation , i.e. ExAcquireResourceSharedLite and ExAcquireResourceExclusiveLite. But this was 16 years ago, since then there might have been better ways to synchronise access to a paging file.

Do not forget to enter a critical region before acquiring a resource object , i.e. KeEnterCriticalRegion .

Thank you so much, that's an awesome reply !! :grinning: :heart_eyes:

I'm a bit scared of allocating IRPs in the minifilter PreRead callback. Could I simply use a combo of KeWaitForSingleObject + FltRead/FltPerformSynchronousIo + KeReleaseMutex instead? It would also be shorter/simpler code. Or is it problematic to use FltRead/FltPerformSynchronousIo in this situation, for whatever reason?

If FltPerformSynchronousIo allows to perform paging IO, then you can use it. I haven't used FilterIo for this particular task so cannot tell exactly. All you need is paging IO, i.e. MDL for Irp and IRP_PAGING_IO flags set.

Thanks again!!

I provide a full FLT_CALLBACK_DATA structure to FltPerformSynchronousIo, so I can easily set "CallbackData->Iopb->IrpFlags = IRP_PAGING_IO". However, I can't really tell if the flag reaches the file system driver.

Though, it seems likely since FltReadFile has the documented flag "FLTFL_IO_OPERATION_PAGING". So I would assume that setting "IrpFlags = IRP_PAGING_IO" for FltPerformSynchronousIo will probably also work.

Not sure I understand the "MDL for Irp" thing you mentioned? FltReadFile doesn't allow me to specify an MDL, it instead wants to have a direct system buffer pointer. For FltPerformSynchronousIo, I'm currently setting "CallbackData->Iopb->Parameters.Read.ReadBuffer = SomeSystemBuffer", to make it similar to my FltReadFile call. Do you think using a direct system buffer pointer instead of an MDL could be problematic?

I think FltReadFile will initialise an IRP as required for paging IO. Otherwise, FLTFL_IO_OPERATION_PAGING doesn't make sense. For paging IO the buffer must be aligned on a page boundary.

Thank you very much, once more! I think I'm all set now.

Are there any tricks how I can stress test my final driver for potential deadlocks or similar issues?

There should be some Microsoft or 3rd party test frameworks, other people might advise on this.
I would suggest

  • Test on a system with a lot of CPUs/cores, at least 20.
  • Test on a uniprocessor system (If Windows supports it).
  • Test with slow storage (old SATA) and the latest super fast NVMe.
  • Set a limit on system physical memory to the minimum that allows it to carry on.
  • Test with 3rd party products that have filesystem minifilters.
  • Use DriverVerifier with I/O verification, Deadlock detection, Force pending I/O requests, Kernel synchronization delay fuzzing
  • Test with instances running in virtual environment (VMWare, Azure, AWS)
1 Like

Thank you - appreciated!!

FltReadFile will, IIRC, fail inside a paging IO path or fail when under Driver Verifier.
FltPerformSync/AsyncIo is needed. You need to keep the synchronisity of the original read!

Thanks for your comment!

FWIW, FltReadFile works for me for paging IO requests, but I'm only using it in PASSIVE_LEVEL. In APC_LEVEL, I'm using FltPerformSyncIo. Will have to double check Driver Verifier.

Hmmmm... I need to keep the synchronisity? That's a good hint. What happens if I don't?

Just checking the IRQL isn't enough, you need to use KeAreAllApcsDisabled to account for guarded regions.

Making synchronous paging I/Os asynchronous can cause deadlocks. It depends on the circumstances and your design.

Also note that opening a file as a paging file has other unknown impacts in the lower file system (which aside from a few rules are going to be subject to implementation...e.g. FAT avoids some cache coherency checks on open because it doesn't think they're necessary). Whether or not this matters would again be a matter of design.

In any case start running the HLK's early and often...

Thank you for your comment!

I'll switch to KeAreAllApcsDisabled.

I understand that I shouldn't convert synchronous paging I/Os to asynchronous, which suits me just fine, because I prefer synchronous.

But how about the other way round? Do I need to keep asynchronous requests (both paging and not) asynchronous? Or is it ok for me to fulfill async requests in my PreRead callback synchronously?

There is no requirement to honour asynchronous IO as a true asynchronous. But ignoring asynchronous IO and delaying returning might slow down system or even result in a transitory low free physical pages condition. The Memory Manager might perform asynchronous IO to multiple targets in anticipation they are performed concurrently, for example, to free physical pages by flushing modified pages. Converting asynchronous IO to synchronous blocks unsubmitted IO requests in a queue waiting for IRP_MJ_WRITE dispatch return.

Thank you very much!

@Slava, unfortunately I'm having trouble getting your code to work.

  1. If I remove the "IO_OPEN_PAGING_FILE" flag (when calling IoCreateFile) everything works perfectly fine, even under heavy stress testing with hundreds of threads.

  2. If I use the "IO_OPEN_PAGING_FILE", everything's still fine as long as only 1 thread is doing stress testing. But as soon as I have 2 threads doing stress testing, there's an "NTFS_FILE_SYSTEM" BSOD happening instantly.

  3. I have all the "IoAllocateIrp ... KeWaitForSingleObject" code in a global synchronization, using FltAcquireResourceExclusive, but it doesn't seem to help.

  4. As an alternative to your "IoAllocateIrp" approach, I've also tried FltReadFile and FltPerformSynchronousIo. FltReadFile doesn't work because I can't provide an MDL, and it seems "IO_OPEN_PAGING_FILE" requires the IRP to have an MDL assigned. FltPerformSynchronousIo produces the exact same results as your "IoAllocateIrp" approach.

  5. Driver Verifier doesn't find anything to complain about, even with most options activated.

  6. I only ever do 16KB (16 * 1024) reads that are completely aligned to 16KB (all: file pointer, read length and buffer), and buffer is NonPagedPool, of course.

Do you have any good ideas for how I could figure this out?

It's a bit confusing to me that problems only occur in multi-thread situations, although I have the offending code in global synchronization (double checked).

STACK_TEXT:
ffffc4036de12f98 fffff8045014d618 : 0000000000000024 000000b0000c0bc8 0000000000000ff6 0000000000ff6000 : nt!KeBugCheckEx
ffffc4036de12fa0 fffff80450145b29 : ffff8d8f00000000 ffff8d8fa7e383b0 0000000000ff4000 0000000000000001 : Ntfs!NtfsPagingFileIo+0xe278
ffffc4036de130b0 fffff8052282a7ca : ffff8d8fa9ec6b40 ffff8d8fa9ec6b40 ffff8d8fa08cb030 ffff8d8fa878ee50 : Ntfs!NtfsFsdRead+0x2c559
ffffc4036de13170 fffff80522fddf49 : ffff8d8fa9ec6b40 ffff8d8fa08cb030 0000000000000000 fffff80522feea27 : nt!IopfCallDriver+0x56
ffffc4036de131b0 fffff8052289e5c7 : ffff8d8fa5256270 0000000000000000 ffff8d8fa9ec6b40 ffff8d8fa878ee50 : nt!IovCallDriver+0x275
ffffc4036de131f0 fffff8044ed977ce : 0000000000000006 0000000000000000 ffff8d8fa9ec6b40 ffff8d8faa2f00d0 : nt!IofCallDriver+0x1b7417
ffffc4036de13230 fffff8044ed96146 : ffffc4036de132c0 0000000000ff5800 0000000000000000 ffff8b45a2d88068 : FLTMGR!FltpLegacyProcessingAfterPreCallbacksCompleted+0x28e
ffffc4036de132a0 fffff8052282a7ca : ffff8d8fa9ec6b40 fffff80522fea1e0 ffff8d8f00000001 ffff8d8f00000000 : FLTMGR!FltpDispatch+0xb6
ffffc4036de13300 fffff80522fddf49 : ffff8d8fa9ec6b40 ffff8d8fa06848d0 ffffffffffffffb5 fffff80522fec27f : nt!IopfCallDriver+0x56
ffffc4036de13340 fffff80522fec278 : 0000000000000000 ffffc4036de13539 ffff8d8fa9ec6fb8 ffff8d8fa42c04a0 : nt!IovCallDriver+0x275
ffffc4036de13380 fffff804507a5746 : 0000000000000000 ffff8d8fa9ec6fb8 ffff8d8fa928c010 0000000000000000 : nt!VerifierIofCallDriver+0x18
ffffc4036de133b0 fffff8044edeeefe : ffff8d8f9fa23138 ffff8d8f9fa23138 0000000000000000 00000000000003fe : MyDriver!PreReadCallback+0x2b6
ffffc4036de13480 fffff8044ed9fca1 : ffffc4036de13649 fffff80500000000 0000000000000000 0000000000000000 : FLTMGR!FltvPreOperation+0xde
ffffc4036de135a0 fffff8044ed97389 : ffffc4036de137e0 0000000000000000 0000000000000003 fffff8044ed98700 : FLTMGR!FltpPerformPreCallbacksWorker+0x4d1
ffffc4036de136b0 fffff8044ed96f6c : ffff8d8fa06848d0 ffffc4036de14000 ffffc4036de0e000 ffffc4036de137e0 : FLTMGR!FltpPerformPreCallbacks+0x79
ffffc4036de13700 fffff8044ed966f0 : ffff8d8fa9d90b40 0000000040060900 ffff8d8fa9d90b40 ffffc4036de137f0 : FLTMGR!FltpPassThroughInternal+0x8c
ffffc4036de13730 fffff8044ed9612e : 0000000000000000 fffff805229c3844 0000000000000000 ffffc4036de13838 : FLTMGR!FltpPassThrough+0x510
ffffc4036de137c0 fffff8052282a7ca : ffff8d8fa9d90b40 fffff80522fea1e0 ffff8d8f00000001 ffff8d8f00000000 : FLTMGR!FltpDispatch+0x9e
ffffc4036de13820 fffff80522fddf49 : ffff8d8fa9d90b40 ffff8d8fa06848d0 00000000000004c0 fffff80522797ca2 : nt!IopfCallDriver+0x56
ffffc4036de13860 fffff8052289e5c7 : ffff8d8fa9d90b40 ffff8d8fa9a6c560 0000000000000001 ffff8d8fa878e630 : nt!IovCallDriver+0x275
ffffc4036de138a0 fffff80522c95841 : ffffc4036de13b80 ffff8d8fa9a6c560 0000000000000000 ffff8d8fa9a6c560 : nt!IofCallDriver+0x1b7417
ffffc4036de138e0 fffff80522c588e8 : ffff8d8f00000000 ffff8d8fa9a6c560 ffffc4036de13b80 ffffc4036de13b80 : nt!IopSynchronousServiceTail+0x1b1
ffffc4036de13990 fffff805228860f8 : 0000000000000000 0000000000000000 0000000000000000 0000000000000000 : nt!NtReadFile+0x688
[...]

NTFS_FILE_SYSTEM (24)
If you see NtfsExceptionFilter on the stack then the 2nd and 3rd
parameters are the exception record and context record. Do a .cxr
on the 3rd parameter and then kb to obtain a more informative stack
trace.
Arguments:
Arg1: 000000b0000c0bc8
Arg2: 0000000000000ff6
Arg3: 0000000000ff6000
Arg4: 0000000000ff5800

Unfortunately, .cxr fails, although it's a "full" dump.