Using IRPs for I/O on device object returned by Io

2019-08-02 18:41发布

问题:

Can one use IoCallDriver() with an IRP created by IoBuildAsynchronousFsdRequest() on a device object returned by IoGetDeviceObjectPointer()? What I have currently fails with blue screen (BSOD) 0x7E (unhandled exception), which when caught shows an Access Violation (0xc0000005). Same code worked when the device was stacked (using the device object returned by IoAttachDeviceToDeviceStack()).

So what I have is about the following:

status = IoGetDeviceObjectPointer(&device_name, FILE_ALL_ACCESS, &FileObject, &windows_device);
if (!NT_SUCCESS(status)) {
    return -1;
}
offset.QuadPart = 0;
newIrp = IoBuildAsynchronousFsdRequest(io, windows_device, buffer, 4096, &offset, &io_stat);
if (newIrp == NULL) {
    return -1;
}
IoSetCompletionRoutine(newIrp, DrbdIoCompletion, bio, TRUE, TRUE, TRUE);

status = ObReferenceObjectByPointer(newIrp->Tail.Overlay.Thread, THREAD_ALL_ACCESS, NULL, KernelMode);
if (!NT_SUCCESS(status)) {
    return -1;
}
status = IoCallDriver(bio->bi_bdev->windows_device, newIrp);
if (!NT_SUCCESS(status)) {
    return -1;
}
return 0;

device_name is \Device\HarddiskVolume7 which exists according to WinObj.exe .

buffer has enough space and is read/writable. offset and io_stat are on stack (also tried with heap, didn't help). When catching the exception (SEH exception) it doesn't blue screen but shows an access violation as reason for the exception. io is IRP_MJ_READ.

Do I miss something obvious? Is it in general better to use IRPs than the ZwCreateFile / ZwReadFile / ZwWriteFile API (which would be an option, but isn't that slower?)? I also tried a ZwCreateFile to have an extra reference, but this also didn't help.

Thanks for any insights.

回答1:

you make in this code how minimum 2 critical errors.

  1. can I ask - from which file you try read (or write) data ? from FileObject you say ? but how file system driver, which will handle this request know this ? you not pass any file object to newIrp. look for IoBuildAsynchronousFsdRequest - it have no file object parameter (and impossible get file object from device object - only visa versa - because on device can be multiple files open). so it and can not be filled by this api in newIrp. you must setup it yourself:

        PIO_STACK_LOCATION irpSp = IoGetNextIrpStackLocation( newIrp );
        irpSp->FileObject = FileObject;
    

    I guess bug was exactly when file system try access FileObject from irp which is 0 in your case. also read docs for IRP_MJ_READ - IrpSp->FileObject - Pointer to the file object that is associated with DeviceObject

  2. you pass I guess local variables io_stat (and offset) to IoBuildAsynchronousFsdRequest. as result io_stat must be valid until newIrp is completed - I/O subsystem write final result to it when operation completed. but you not wait in function until request will be completed (in case STATUS_PENDING returned) but just exit from function. as result later I/O subsystem, if operation completed asynchronous, write data to arbitrary address &io_stat (it became arbitrary just after you exit from function). so you need or check for STATUS_PENDING returned and wait in this case (have actually synchronous io request). but more logical use IoBuildSynchronousFsdRequest in this case. or allocate io_stat not from stack, but say in your object which correspond to file. in this case you can not have more than single io request with this object at time. or if you want exactly asynchronous I/O - you can do next trick - newIrp->UserIosb = &newIrp->IoStatus. as result you iosb always will be valid for newIrp. and actual operation status you check/use in DrbdIoCompletion

also can you explain (not for me - for self) next code line ?:

status = ObReferenceObjectByPointer(newIrp->Tail.Overlay.Thread, THREAD_ALL_ACCESS, NULL, KernelMode);

who and where dereference thread and what sense in this ?

Can one use ...

we can use all, but with condition - we understand what we doing and deep understand system internally.

Is it in general better to use IRPs than the ZwCreateFile / ZwReadFile / ZwWriteFile API

for performance - yes, better. but this require more code and more complex code compare api calls. and require more knowledge. also if you know that previous mode is kernel mode - you can use NtCreateFile, NtWriteFile, NtReadFile - this of course will be bit slow (need every time reference file object by handle) but more faster compare Zw version


Just wanted to add that the ObReferenceObjectByPointer is needed because the IRP references the current thread which may exit before the request is completed. It is dereferenced in the Completion Routine. Also as a hint the completion routine must return STATUS_MORE_PROCESSING_REQUIRED if it frees the IRP (took me several days to figure that out).

here you make again several mistakes. how i understand you in completion routine do next:

IoFreeIrp(Irp);
return StopCompletion;

but call simply call IoFreeIrp here is error - resource leak. i advice you check (DbgPrint) Irp->MdlAddress at this point. if you read data from file system object and request completed asynchronous - file system always allocate Mdl for access user buffer in arbitrary context. now question - who free this Mdl ? IoFreeIrp - simply free Irp memory - nothing more. you do this yourself ? doubt. but Irp is complex object, which internally hold many resources. as result need not only free it memory but call "destructor" for it. this "destructor" is IofCompleteRequest. when you return StopCompletion (=STATUS_MORE_PROCESSING_REQUIRED) you break this destructor at very begin. but you must latter again call IofCompleteRequest for continue Irp (and it resources) correct destroy.

about referencing Tail.Overlay.Thread - what you doing - have no sense:

It is dereferenced in the Completion Routine.

  1. but IofCompleteRequest access Tail.Overlay.Thread after it call your completion routine (and if you not return StopCompletion). as result your reference/dereference thread lost sense - because you deference it too early, before system actually access it.
  2. also if you return StopCompletion and not more call IofCompleteRequest for this Irp - system not access Tail.Overlay.Thread at all. and you not need reference it in this case.
  3. and exist else one reason, why reference thread is senseless. system access Tail.Overlay.Thread only for insert Apc to him - for call final part (IopCompleteRequest) of Irp destruction in original thread context. really this need only for user mode Irp's requests, where buffers and iosb located in user mode and valid only in context of process (original thread ). but if thread is terminated - call of KeInsertQueueApc fail - system not let insert apc to died thread. as result IopCompleteRequest will be not called and resources not freed.

so you or dereference Tail.Overlay.Thread too early or you not need do this at all. and reference for died thread anyway not help. in all case what you doing is error.

you can try do next here:

PETHREAD Thread = Irp->Tail.Overlay.Thread;
IofCompleteRequest(Irp, IO_NO_INCREMENT);// here Thread will be referenced
ObfDereferenceObject(Thread);
return StopCompletion;

A second call to IofCompleteRequest causes the I/O manager to resume calling the IRP's completion. here io manager and access Tail.Overlay.Thread insert Apc to him. and finally you call ObfDereferenceObject(Thread); already after system access it and return StopCompletion for break first call to IofCompleteRequest. look like correct but.. if thread already terminated, how i explain in 3 this will be error, because KeInsertQueueApc fail. for extended test - call IofCallDriver from separate thread and just exit from it. and in completion run next code:

PETHREAD Thread = Irp->Tail.Overlay.Thread;

if (PsIsThreadTerminating(Thread))
{
    DbgPrint("ThreadTerminating\n");

    if (PKAPC Apc = (PKAPC)ExAllocatePool(NonPagedPool, sizeof(KAPC)))
    {
        KeInitializeApc(Apc, Thread, 0, KernelRoutine, 0, 0, KernelMode, 0);

        if (!KeInsertQueueApc(Apc, 0, 0, IO_NO_INCREMENT))
        {
            DbgPrint("!KeInsertQueueApc\n");
            ExFreePool(Apc);
        }
    }
}

PMDL MdlAddress = Irp->MdlAddress;

IofCompleteRequest(Irp, IO_NO_INCREMENT);

ObfDereferenceObject(Thread);

if (MdlAddress == Irp->MdlAddress)
{
    // IopCompleteRequest not called due KeInsertQueueApc fail
    DbgPrint("!!!!!!!!!!!\n");
    IoFreeMdl(MdlAddress);
    IoFreeIrp(Irp);
}

return StopCompletion;

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

VOID KernelRoutine (PKAPC Apc,PKNORMAL_ROUTINE *,PVOID *,PVOID *,PVOID *)
{
    DbgPrint("KernelRoutine(%p)\n", Apc);
    ExFreePool(Apc);
}

and you must got next debug output:

ThreadTerminating
!KeInsertQueueApc
!!!!!!!!!!!

and KernelRoutine will be not called (like and IopCompleteRequest) - no print from it.

so what is correct solution ? this of course not documented anywhere, but based on deep internal understand. you not need reference original thread. you need do next:

  Irp->Tail.Overlay.Thread = KeGetCurrentThread();
  return ContinueCompletion;

you can safe change Tail.Overlay.Thread - if you have no any pointers valid only in original process context. this is true for kernel mode requests - all your buffers in kernel mode and valid in any context. and of course you not need break Irp destruction but continue it. for correct free mdl and all irp resources. and finally system call IoFreeIrp for you.

and again for iosb pointer. how i say pass local variable address, if you exit from function before irp completed (and this iosb accessed) is error. if you break Irp destruction, iosb will be not accessed of course, but in this case much better pass 0 pointer as iosb. (if you latter something change and iosb pointer will be accessed - will be the worst error - arbitrary memory corrupted - with unpredictable effect. and research crash of this will be very-very hard). but if you completion routine - you not need separate iosb at all - you have irp in completion and can direct access it internal iosb - for what you need else one ? so the best solution will be do next:

Irp->UserIosb = &Irp->IoStatus;

full correct example how read file asynchronous:

NTSTATUS DemoCompletion (PDEVICE_OBJECT /*DeviceObject*/, PIRP Irp, BIO* bio)
{
    DbgPrint("DemoCompletion(p=%x mdl=%p)\n", Irp->PendingReturned, Irp->MdlAddress);

    bio->CheckResult(Irp->IoStatus.Status, Irp->IoStatus.Information);
    bio->Release();

    Irp->Tail.Overlay.Thread = KeGetCurrentThread();

    return ContinueCompletion;
}

VOID DoTest (PVOID buf)
{
    PFILE_OBJECT FileObject;
    NTSTATUS status;
    UNICODE_STRING ObjectName = RTL_CONSTANT_STRING(L"\\Device\\HarddiskVolume2");
    OBJECT_ATTRIBUTES oa = { sizeof(oa), 0, &ObjectName, OBJ_CASE_INSENSITIVE };

    if (0 <= (status = GetDeviceObjectPointer(&oa, &FileObject)))
    {
        status = STATUS_INSUFFICIENT_RESOURCES;

        if (BIO* bio = new BIO(FileObject))
        {
            if (buf = bio->AllocBuffer(PAGE_SIZE))
            {
                LARGE_INTEGER ByteOffset = {};

                PDEVICE_OBJECT DeviceObject = IoGetRelatedDeviceObject(FileObject);

                if (PIRP Irp = IoBuildAsynchronousFsdRequest(IRP_MJ_READ, DeviceObject, buf, PAGE_SIZE, &ByteOffset, 0))
                {
                    Irp->UserIosb = &Irp->IoStatus;
                    Irp->Tail.Overlay.Thread = 0;

                    PIO_STACK_LOCATION IrpSp = IoGetNextIrpStackLocation(Irp);

                    IrpSp->FileObject = FileObject;

                    bio->AddRef();

                    IrpSp->CompletionRoutine = (PIO_COMPLETION_ROUTINE)DemoCompletion;
                    IrpSp->Context = bio;
                    IrpSp->Control = SL_INVOKE_ON_CANCEL|SL_INVOKE_ON_ERROR|SL_INVOKE_ON_SUCCESS;

                    status = IofCallDriver(DeviceObject, Irp);
                }
            }

            bio->Release();
        }

        ObfDereferenceObject(FileObject);
    }

    DbgPrint("DoTest=%x\n", status);
}

struct BIO 
{
    PVOID Buffer;
    PFILE_OBJECT FileObject;
    LONG dwRef;

    void AddRef()
    {
        InterlockedIncrement(&dwRef);
    }

    void Release()
    {
        if (!InterlockedDecrement(&dwRef))
        {
            delete this;
        }
    }

    void* operator new(size_t cb)
    {
        return ExAllocatePool(PagedPool, cb);
    }

    void operator delete(void* p)
    {
        ExFreePool(p);
    }

    BIO(PFILE_OBJECT FileObject) : FileObject(FileObject), Buffer(0), dwRef(1)
    {
        DbgPrint("%s<%p>(%p)\n", __FUNCTION__, this, FileObject);

        ObfReferenceObject(FileObject);
    }

    ~BIO()
    {
        if (Buffer)
        {
            ExFreePool(Buffer);
        }

        ObfDereferenceObject(FileObject);

        DbgPrint("%s<%p>(%p)\n", __FUNCTION__, this, FileObject);
    }

    PVOID AllocBuffer(ULONG NumberOfBytes)
    {
        return Buffer = ExAllocatePool(PagedPool, NumberOfBytes);
    }

    void CheckResult(NTSTATUS status, ULONG_PTR Information)
    {
        DbgPrint("CheckResult:status = %x, info = %p\n", status, Information);
        if (0 <= status)
        {
            if (ULONG_PTR cb = min(16, Information))
            {
                char buf[64], *sz = buf;
                PBYTE pb = (PBYTE)Buffer;
                do sz += sprintf(sz, "%02x ", *pb++); while (--cb); sz[-1]= '\n';
                DbgPrint(buf);
            }
        }
    }
};

NTSTATUS GetDeviceObjectPointer(POBJECT_ATTRIBUTES poa, PFILE_OBJECT *FileObject )
{
    HANDLE hFile;
    IO_STATUS_BLOCK iosb;

    NTSTATUS status = IoCreateFile(&hFile, FILE_READ_DATA, poa, &iosb, 0, 0, 
        FILE_SHARE_VALID_FLAGS, FILE_OPEN, FILE_NO_INTERMEDIATE_BUFFERING, 0, 0, CreateFileTypeNone, 0, 0);

    if (0 <= (status))
    {
        status = ObReferenceObjectByHandle(hFile, 0, *IoFileObjectType, KernelMode, (void**)FileObject, 0);
        NtClose(hFile);
    }

    return status;
}

and output:

BIO::BIO<FFFFC000024D4870>(FFFFE00001BAAB70)
DoTest=103
DemoCompletion(p=1 mdl=FFFFE0000200EE70)
CheckResult:status = 0, info = 0000000000001000
eb 52 90 4e 54 46 53 20 20 20 20 00 02 08 00 00
BIO::~BIO<FFFFC000024D4870>(FFFFE00001BAAB70)

the eb 52 90 4e 54 46 53 read ok