Kernel Mode Drivers: Part 15: The IRP Life Cycle



In this article and the next, we will look at the principles of filtering (intercepting) I / O request packets (IRP). Why do you need to intercept other people's IRPs? There are many applications for this. For example, we wanted to see what files this or that program accesses. What will we do first? That's right - run FileMon (sysinternals.com), which will install the filter driver on the file system. And since access to files is actually the formation of the corresponding IRP (fast I / O, in which the formation of the IRP does not occur, does not count) and sending them to the file system drivers, then before reaching the addressee, the IRP will get into the filter and FileMon will fix it the appeal, after which it will send it to the addressee. At the same time, FileMon cannot affect the intercepted packets. Its task is only to register the fact of sending an IRP. Another example. Let's say you need to hide, for example, from your immediate family or work colleagues, the presence of some frivolous files. Without thinking twice, you google something like "Hide Files And Folders" and immediately find a bunch of programs that allow you to hide individual files and directories. This is possible thanks to the same IRP filtering mechanism. By accessing a packet, the filter driver has the ability to modify the data transmitted in it, both on the path to the file system and back. Of course, you can filter not only IRPs transferred to the file system, but also any others. IRP filtering is a general and versatile mechanism. Anti-virus monitors, firewalls, on-the-fly compressors / decompressors, cryptors / decryptors, etc. etc. use the IRP filtering mechanism. The filter that we will write next time

Filtering I / O request packets is a complex topic. Therefore, before moving on to practical implementation, at least minimal theoretical training is required. At a minimum, you need to be clear about the life cycle of an IRP from birth to death. In this article, we will mainly research this issue. Since the drivers serving the keyboard fully support the Plug And Play mechanism, it will be necessary, to a minimum, to cover this issue as well. However, our filter will not be a Plug And Play driver. This will still be the legacy driver, in Microsoft terminology, but we will connect it to the Plug And Play driver.

Due to the complexity of the topic, I will hardly be able to shed light on this issue from all sides. A lot of additional information can be found in the DDK "Handling IRPs" section. The Installable File System Kit (IFS KIT), which is a superset of the regular DDK, also has an "OSR Technical Articles" section that includes articles prepared by the Open System Resources team ( http://www.osr.com/ ). If you only have a regular DDK at your disposal, then most, if not all, of these articles, as well as a lot of additional information, can be found in the online magazine "The NT Insider" ( http://www.osronline.com/ ).



General classification of WDM drivers

All Plug And Play drivers must match the Windows Driver Model (WDM). According to this model, drivers are classified into three types:

As you know, each driver must create at least one device object that it will control. WDM also divides device objects into types:



Device tree

With the aforesaid classification, start with the fact that determine how the system, or more precisely, the dispatcher the PnP (the PnP Manager) - operating system component for automatic recognition of installed devices, knows what drivers are required for a particular device. The recognition process involves enumerating devices at boot and detecting whether they have been added or removed while the system is running.

At boot time, the PnP Manager starts enumerating devices from the virtual bus named Root. The system itself acts as a virtual driver serving this bus. Logically, all devices (physical and virtual) are connected to this bus. The virtual root bus driver (and other bus drivers too) retrieves the necessary information from the registry. Hardware information is entered into the registry at the stage of operating system installation. The installer detects installed devices and using information files(INF Files), fills in the appropriate registry keys. By enumerating devices on the root bus, its virtual driver detects other buses (physical and virtual), for example, the physical PCI bus. The PnP Manager uses the registry information to determine if a driver is installed on the system that can control the detected device. If such a driver is installed, the PnP manager tells the I / O manager(I / O Manager) download it. If a suitable driver is not installed, PnP Manager tries to install it. In this case, if the corresponding information file or other necessary files are not found, the PnP manager interacts with the user, who must indicate the location of the necessary components. Once loaded, the driver serving the detected bus lists the devices connected to it. In doing so, it can detect other additional buses. If a driver is required for a device detected on the bus to function, it is loaded. This recursive process - enumerating devices, loading a driver, and further enumeration - continues until all the devices in the system are found and configured. The PnP manager is able to detect the addition / removal of a new device while the system is running.Device Tree (Device Tree), which reflects the hierarchical relationship between all the devices installed in the system.

The tree view can be devices via device manager (Device Manager). What the device tree looks like on my computer (in the "View" menu, I selected "Devices by connection" and checked "Show hidden devices".) Is shown in Fig. 15.1.

Rice. 15-1. Device tree.

In the figure, you can find some of the previously created virtual devices, for example, ProcessMon (Process creation / destruction monitor), connected (also virtually) to the root bus. In Windows 2000, Device Manager shows all previously installed virtual devices, and in Windows XP (and in Windows 2003 Server, probably, too), only currently active ones. Information about virtual devices is retrieved by Device Manager from registry keys \ HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Enum \ Root \ LEGACY_XXX.

The nodes in the device tree are called device nodes (devnodes). Each node is served by one or more drivers. How does the system know which drivers are serving which node?

All devices discovered during system installation (and also installed later) are registered in the registry subkeys \ HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Enum \ < enumerator > \ < deviceID > \ < instanceID >. Where enumerator is a bus driver that enumerates devices on the bus, deviceID is a unique identifier for devices of this type, instanceID is a unique identifier for an instance of a device of this type (it can be used to distinguish several identical devices).

During the enumeration process, the bus driver informs the PnP manager of the device ID and instanceID of the discovered devices . Using this information, the PnP manager finds in the registry the drivers needed for the node of this device.

An example of an Enum subkey for a keyboard is shown in Figure 15-2.

Rice. 15-2. The keyboard subkey of the Enum branch.

As you can see in the figure, the enumerator is ACPI, the device ID is PNP0303, and the device instance ID is 3 & 13c0b0c5 & 0. If you look into% SystemRoot% \ inf \ keyboard.inf, you will find that the information in the registry comes from this information file. Of course, a different keyboard type may be connected to your machine.

The functional driver is specified by the Service parameter. In this case, it is i8042prt. The ClassGUID (Globally Unique Identifier of Class) parameter defines the device class subkey \ HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Control \ Class. This subsection contains information about the device class driver. A class driver defines common functionality for all devices of a given type. It knows nothing about how to control a specific device, but using standardized services, it interacts with a functional driver, which, in turn, knows how to control a specific type of device. In this case, the class driver is kbdclass. It acts as a kind of buffer between the i8042prt functional driver and the Win32 subsystem (more details in the next article). An example of a Class subkey for a keyboard is shown in Figure 15-3.

Rice. 15-3. The keyboard subkey of the Class branch.

The contents of these two sections give the PnP manager all the information it needs to load the drivers for the node of a given device. The driver names point to registry subkeys \ HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Services \ < drivername >.

The drivers for the device node are loaded in the following order:



Device object stack

All of the above is not directly related to the material of the article. Used therein is not the PnP driver-driver and still refers to the legacy drivers (legacy drivers). A general understanding of the enumeration mechanism and knowledge of what a device tree is is necessary to introduce the next concept that is already directly important to us.

When loading each PnP driver, the PnP manager calls the AddDevice standard driver routine. The PhysicalDeviceObject parameter is passed a pointer to the physical device object created by the bus driver. The loaded driver, in turn, creates its own device object and connects it to the physical device object by calling the IoAttachDeviceToDeviceStack function. It passes two pointers to this function: a pointer to the "physical device" object passed to it by the Pnp manager and a pointer to the "device" object it created. In this case, the new object is always connected to the topmost object in this chain, regardless of whether there are other objects above the PDO or not. Pointer to the object "physical device", when connecting a new object, is used as a pointer to the object chain to connect to, not a pointer to a specific device object. The IoAttachDeviceToDeviceStack function finds the topmost object itself.

The resulting construction consists of at least two objects: a physical device object created by the bus driver and a functional device object created by a functional driver, called the device stack or simply a stack. That. each node in the device tree is represented by its own stack.

Considering all of the above, and having the contents of the Enum and Class registry keys, we can predict what objects the stack for the keyboard device node will consist of (objects are listed from bottom to top):

Both device-filter objects are created by the high-level filter drivers, and there are no bus filter drivers or low-level filter drivers in this case.

You can view device stacks using the Devide Tree software (osr.com or osronline.com). But I avoid using this utility, because its work on my three machines with different versions of the system inevitably leads to a "blue screen of death" (at least in PnP mode). It's amazing that this utility is included in the DDK. We'll use the more robust! Devstack command of the Kernel Debugger.

Rice. 15-4. Stacks of device objects for the keyboard.

This machine has a Terminal Server system active and the keyboard has not one but two stacks. As you can see, our assumptions about the composition of the devices were confirmed. On your car, its composition, of course, may differ. Next, we will consider the classic composition of the keyboard stack, namely: Kbdclass at the top, i8042prt in the middle, ACPI at the bottom.

In general, a stack of "device" objects might look like this (see the classification of drivers and objects in WDM above):

Rice. 15-5. The device object stack for a device node (general diagram).

Since each device object in the stack is controlled by a driver, it is very common to use a driver stack along with the concept of a device stack. This is not entirely true, but what is at stake, I hope, is clear. Further in the course of the article, I will also sometimes say "device stack", and sometimes "driver stack".

The IRP is generated by an I / O manager or non-stack driver and is directed to the top of the stack. If a driver requires the help of a downstream driver to process a request, it redirects the IRP down the stack, and so on. The IRP always goes down the stack from top to bottom. The decision to end IRP processing can be made at any level. Moreover, any driver in the stack can form additional IRPs (for example, split a read request from a file into several requests) and send it to the necessary drivers. Any driver can reject the request or can modify the data transmitted in it. In general, if a driver has received an IRP, it can do whatever it wants with it.



Language in three minutes

I will have to use the source codes for some of the system functions, since it’s impossible to really understand IRP processing without analyzing the source code. These fragments, of course, will not be the true code of the operating system and will be truncated, sometimes quite significantly. Also, all error handling is omitted: checks of pointers, input data and values ​​returned by functions, SEH handlers are removed. Only the very essence is left. To simplify the analysis of the code, I will use a c-like pseudo-language (almost pure c). I fully admit that you may not know this language, tk. we are still engaged in the development of drivers in assembler. Therefore, I will summarize the basic constructions, which you cannot do without.

In assembler, space for an initialized variable is allocated as follows:

 dw DWORD 0

In the c language, global and local initialized variables are defined as follows:

 DWORD dw = 0;

If we need to pass the address of a variable to a function (using the invoke macro), we do it like this:

 invoke SimeFunc, addr dw

The C programmer does it like this:

 SomeFunc( &dw );

The reverse operation is to write a value into a variable by a pointer to a variable - in assembler it looks like this (pwd is a pointer to a double word variable):

 pdw PTR DWORD ?

 mov eax, pdw
 mov dword ptr [eax], 0

In c it is somewhat simpler:

 PDWORD pdw;

 *pdw = 0;

If we have a FILE_OBJECT structure, then we can write a pointer to the "device" object in its DeviceObject field as follows:

 FileObject     FILE_OBJECT    <>
 pDeviceObject  PDEVICE_OBJECT ?

 mov eax, pDeviceObject
 mov FileObject.DeviceObject, eax

The C programmer does it like this:

 FILE_OBJECT     FileObject;
 PDEVICE_OBJECT  pDeviceObject;

 FileObject.DeviceObject = pDeviceObject;

If, instead of a structure, we have a pointer to it, then we will have to do the above operation like this:

 pFileObject PFILE_OBJECT ?
 pDeviceObject  PDEVICE_OBJECT ?

 mov ecx, pFileObject
 mov eax, pDeviceObject  
 mov (FILE_OBJECT PTR [ecx]).DeviceObject, eax

As always, it is a little easier for a C programmer:

 PFILE_OBJECT    pFileObject;
 PDEVICE_OBJECT  pDeviceObject;

 pFileObject->DeviceObject = pDeviceObject;

If the c-programmer needs to increase the value of a variable by one, then he can do it in at least three ways, the most obvious of which is the following:

 dw = dw + 1;

To reduce the number of IRP circulating in the system, you can do it like this:

 dw += 1;

The same trick can be done with the other three mathematical operations. Logical operations can also be written in this form. For example:

 dw |= <some flag>

Equivalently

 or dw, <some flag>
 

But, I'll tell you a secret, there is another way that usually only gurus or the laziest c-programmers use to increment variables by one:

 dw++;

The same trick can be done with the subtraction operation.

We will not go into further details, this minimum should be enough. Also keep in mind that the DDK contains the complete source code for the kbdclass and i8042prt drivers. However, it differs slightly in different DDKs. Accordingly, these drivers are also different in different versions of the system.



IRP life cycle

We have received an IRP many times, but have never created one ourselves. Since we are going to look at the entire life cycle of an I / O request packet, we cannot do without creating one. Let's start with this.

Let's say we have a device object name, say \ Device \ KeyboardClass0. As the name suggests, this object has something to do with the maintenance of the physical "keyboard" device. Why this object is needed and what is its role, we will talk in more detail in the next article. So far, we are only interested in one thing: we have a device name and we want to send some IRP to it. This can be done by calling the IoCallDriver function, whose prototype looks like this:

 NTSTATUS 
   IoCallDriver(
     IN PDEVICE_OBJECT  DeviceObject,
     IN OUT PIRP        Irp
     );

Despite the name of the function, the first argument is a pointer to the device object, not the driver to which the IRP is addressed. The IRP will be processed, of course, by the driver that created this device. The second parameter is a pointer to the I / O request packet itself.

 ;@echo off
 ;goto make

 .386
 .model flat, stdcall
 option casemap:none

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                  I N C L U D E   F I L E S                                        
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 include \masm32\include\w2k\ntstatus.inc
 include \masm32\include\w2k\ntddk.inc

 include \masm32\include\w2k\ntoskrnl.inc

 includelib \masm32\lib\w2k\ntoskrnl.lib

 include \masm32\Macros\Strings.mac

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                     C O N S T A N T S                                             
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .const

 CCOUNTED_UNICODE_STRING "\\Device\\KeyboardClass0", g_usTargetDeviceName, 4

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                              D I S C A R D A B L E   C O D E                                      
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 .code INIT

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                    IrpComplete                                            
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 IrpComplete proc uses esi edi pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP, pContext:PVOID

     mov esi, pIrp
     assume esi:ptr _IRP

     mov edi, [esi].UserIosb
     assume edi:ptr IO_STATUS_BLOCK

     mov eax, [esi].IoStatus.Status
     mov [edi].Status, eax

     mov eax, [esi].IoStatus.Information
     mov [edi].Information, eax

     assume edi:nothing
     assume edi:nothing

     .if [esi].PendingReturned
         invoke KeSetEvent, pContext, 0, FALSE
     .endif

     invoke IoFreeIrp, pIrp

     mov eax, STATUS_MORE_PROCESSING_REQUIRED
     ret
 
 IrpComplete endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                     QueryPnpDeviceState                                           
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 QueryPnpDeviceState proc uses esi edi ebx pDeviceObject:PDEVICE_OBJECT

 local status:NTSTATUS
 local keEvent:KEVENT
 local iosb:IO_STATUS_BLOCK

     mov status, STATUS_NOT_SUPPORTED

     mov esi, pDeviceObject
     assume esi:ptr DEVICE_OBJECT

     .if ( esi != NULL  &&  [esi]._Type == IO_TYPE_DEVICE )

         movzx eax, [esi].StackSize
         invoke IoAllocateIrp, eax, FALSE

         assume esi:nothing
                
         .if eax != NULL

             mov edi, eax
             assume edi:ptr _IRP

             mov [edi].IoStatus.Status, STATUS_NOT_SUPPORTED
             and [edi].IoStatus.Information, 0

             mov iosb.Status, STATUS_NOT_SUPPORTED
             and iosb.Information, 0

             lea eax, iosb
             mov [edi].UserIosb, eax

             assume edi:nothing

             IoGetNextIrpStackLocation edi
             mov ebx, eax
             assume ebx:ptr IO_STACK_LOCATION

             mov [ebx].MajorFunction, IRP_MJ_PNP
             mov [ebx].MinorFunction, IRP_MN_QUERY_PNP_DEVICE_STATE

             assume ebx:nothing

             invoke KeInitializeEvent, addr keEvent, NotificationEvent, FALSE

             IoSetCompletionRoutine edi, IrpComplete, addr keEvent, TRUE, TRUE, TRUE

             invoke IoCallDriver, esi, edi
             mov status, eax

             .if eax == STATUS_PENDING

                 invoke DbgPrint, $CTA0("QueryPnpDeviceState: Request pended. Waiting...\n")
    
                 invoke KeWaitForSingleObject, addr keEvent, Executive, KernelMode, FALSE, NULL

                 mov eax, iosb.Status
                 mov status, eax

             .endif

             .if status == STATUS_SUCCESS

                 invoke DbgPrint, $CTA0("QueryPnpDeviceState: Device State: %08X\n"), iosb.Information

             .endif

         .else
             mov status, STATUS_INSUFFICIENT_RESOURCES
         .endif

     .endif

     mov eax, status
     ret
    
 QueryPnpDeviceState endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                       DriverEntry                                                 
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING

 local pTargetDeviceObject:PDEVICE_OBJECT
 local pTargetFileObject:PFILE_OBJECT

     invoke IoGetDeviceObjectPointer, addr g_usTargetDeviceName, FILE_READ_DATA, \
                                      addr pTargetFileObject, addr pTargetDeviceObject
     .if eax == STATUS_SUCCESS

         invoke QueryPnpDeviceState, pTargetDeviceObject
        
         invoke ObDereferenceObject, pTargetFileObject

     .endif

     mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
     ret

 DriverEntry endp

 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 ;                                                                                                   
 ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

 end DriverEntry

 :make

 set drv=QueryPnpDeviceState

 \masm32\bin\ml /nologo /c /coff %drv%.bat
 \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj

 del %drv%.obj

 echo.
 pause

We can get a pointer to the device we need by its name using IoGetDeviceObjectPointer. If successful, this function will even return two pointers: one is the actual pointer to the device we need in the pTargetDeviceObject variable, and the second is a pointer to the "file" object associated with this device in the pTargetFileObject variable. Where did the file object come from? Let's take a look inside the IoGetDeviceObjectPointer function, as well as the other two that it calls.

 PDEVICE_OBJECT
   IoGetAttachedDevice(
     IN PDEVICE_OBJECT pDeviceObject
     )
 {
     while pDeviceObject->AttachedDevice
         pDeviceObject = pDeviceObject->AttachedDevice

     return pDeviceObject;
 }



 PDEVICE_OBJECT
   IoGetRelatedDeviceObject(
     IN PFILE_OBJECT pFileObject
     )
 {
     PDEVICE_OBJECT pDeviceObject

     pDeviceObject = pFileObject->Vpb->DeviceObject
     ... or ...
     pDeviceObject = pFileObject->DeviceObject->Vpb->DeviceObject
     ... or ...
     pDeviceObject = pFileObject->DeviceObject

     if pDeviceObject->AttachedDevice != NULL
         pDeviceObject = IoGetAttachedDevice( pDeviceObject )

     return pDeviceObject
 }



 NTSTATUS
   IoGetDeviceObjectPointer(
     IN PUNICODE_STRING  pusObjectName,
     IN ACCESS_MASK      DesiredAccess,
     OUT PFILE_OBJECT    *out_pFileObject,
     OUT PDEVICE_OBJECT  *out_pDeviceObject
     )
 {
     PFILE_OBJECT        pFileObject
     OBJECT_ATTRIBUTES   oa
     HANDLE              hFile

     InitializeObjectAttributes( &oa, pusObjectName, ... )

     ZwOpenFile( &hFile, DesiredAccess, &oa, ... )

     ObReferenceObjectByHandle( hFile, 0, IoFileObjectType, KernelMode, &pFileObject, NULL )

     *out_pFileObject   = pFileObject
     *out_pDeviceObject = IoGetRelatedDeviceObject( pFileObject )

     ZwClose( hFile )

     return STATUS_XXX
 }

First, the IoGetDeviceObjectPointer function gets a handle to the file object (represented by the FILE_OBJECT structure).

Remember how in the driver control program we get a handle to interact with its device. We call the CreateFile function, which creates a file object that does not represent the actual file on disk, but a virtual device (DEVICE_OBJECT structure) created by the driver. Those. in fact, the file descriptor is used for I / O to the device. Such a scheme is needed, firstly, to differentiate access rights, since the DEVICE_OBJECT structure does not have, for example, the WriteAccess and SharedRead fields, while the FILE_OBJECT contains such fields, and secondly, the "file" object can store some other attributes of the I / O operation. The address of the true recipient of the I / O request packet, in our case, is in the FILE_OBJECT.DeviceObject field. So, calling ZwOpenFile, just like CreateFile, results in the creation of a "file" object, which means the formation of an IRP of the IRP_MJ_CREATE type and sending it to the target device (in our case, the device \ Device \ KeyboardClass0). This package, as you understand, gets into the driver serving this device (the device \ Device \ KeyboardClass0 is served by the kbdclass driver). Those. the decision to satisfy the request - calling IoCompleteRequest with the STATUS_SUCCESS status - is made by the serving driver.

Here is a snippet of the KeyboardClassCreate function of the kbdclass driver:

    PIO_STACK_LOCATION   pStack;

    pStack = IoGetCurrentIrpStackLocation( pIrp )

    if  pIrp->RequestorMode == UserMode
            &&
        pStack->Parameters.Create.SecurityContext->DesiredAccess & FILE_READ_DATA  {

        status = STATUS_ACCESS_DENIED
        goto KeyboardClassCreateEnd
    }

As you can see, kbdclass rejects user mode's attempt to access its devices for reading.

By the way, since we are going to understand everything in such detail, let's look at the insides of the IoGetCurrentIrpStackLocation macro, which we ourselves have already used many times (full version in ntddk.inc).

 IoGetCurrentIrpStackLocation MACRO pIrp:REQ
     mov eax, pIrp
     mov eax, (_IRP PTR [eax]).Tail.Overlay.CurrentStackLocation
 ENDM

Maros IoGetCurrentIrpStackLocation simply fetches the pointer to the current stack block from the CurrentStackLocation field.

After receiving a handle to the file object, the IoGetDeviceObjectPointer function further increments the reference count in the file object by calling ObReferenceObjectByHandle. The IoGetDeviceObjectPointer then tries to get the target device pointer associated with the file object by calling IoGetRelatedDeviceObject. Depending on the belonging of the "file" object to one or another type of device, IoGetRelatedDeviceObject can retrieve the necessary pointer from different places (in our case, from the pFileObject-> DeviceObject field). Further, pay special attention to this, if another device is attached to the target device (this is indicated by a nonzero value in the pDeviceObject-> AttachedDevice field), the IoGetAttachedDevice function is "raised" down the device stack to the top and returns a pointer to the device at the top of the stack. If there are no attached devices, then a pointer to the target device itself is returned, i.e. the one whose name was passed to the IoGetDeviceObjectPointer. Remember:The IoGetAttachedDevice function always returns a pointer to the device object at the top of the stack.

After receiving the pointer, IoGetDeviceObjectPointer closes the file object descriptor and at this moment the descriptor counter becomes equal to zero, which results in the formation and sending of an IRP of the IRP_MJ_CLEANUP type to the kbdclass driver. That. the IoGetDeviceObjectPointer function will return pointers to two objects: "file" and "device". And in the object "device" the value of the counters of pointers and descriptors does not change, and in the object "file" it is equal to 1 and 0, respectively. A single pointer count is achieved through an additional call to ObReferenceObjectByHandle. As long as the "file" object exists, the "device" object with which it is associated will not be deleted and, accordingly, the driver that controls the device cannot be unloaded either. in the object he controls "

That. in the case of IoGetDeviceObjectPointer, the schema is exactly the same as that used by user mode, getting a handle to the file object and thus locking the associated object. In this case, the "file" object itself refers to any source or destination of I / O (actually a file or directory, named pipe, mailbox, etc.), which is considered as a file. With this mechanism, all data read or written is represented as simple byte streams directed to virtual files. When finished, the user-mode program closes the file descriptor, and we will have to remove the link by calling ObDereferenceObject. The counter of pointers in the "file" object will be reset to zero, and this will result in the formation and sending of an IRP of the IRP_MJ_CLOSE type to the kbdclass driver. Only after that the object "

Let's go back to the source code of our driver.

         invoke QueryPnpDeviceState, pTargetDeviceObject

We now have a destination to send the IRP. It remains only to form the package itself.

An IRP consists of a body or header (the actual structure of the IRP) and one or more stack blocks(stack locations). The body of the IRP stores general information about an I / O request: pointers to buffers, status data, etc. Stack blocks contain information specific to a particular stage of IRP processing. By passing the IRP to the driver for processing, the I / O dispatcher (or the driver that creates the IRP itself, as we do in this example) fills the top block of the stack. If the driver that received the IRP decides to send it to the downstream driver for further processing, it fills the next block of the stack (since this is a stack, then the next block of the stack is located in memory at a lower address - more on this later) and passes the IRP below and etc. That. stack blocks — one for each called driver — store the information each driver needs to process its part of the request.

     mov esi, pDeviceObject
     assume esi:ptr DEVICE_OBJECT
    
     .if ( esi != NULL  &&  [esi]._Type == IO_TYPE_DEVICE )

         movzx eax, [esi].StackSize
         invoke IoAllocateIrp, eax, FALSE

You can create an IRP using one of four functions: IoBuildSynchronousFsdRequest, IoBuildDeviceIoControlRequest, IoBuildAsynchronousFsdRequest, and IoAllocateIrp. To be absolutely precise, you can do the IRP manually altogether by allocating memory from a pool or an associative list, but then you will have to fill in all its fields yourself. We'll use the most versatile of the four functions above, IoAllocateIrp. Unlike the other three, it can be used to create any type of IRP.

For better performance reasons, the memory for the IRP is allocated in one of two associative lists, individual for each processor (the structures for managing the lists are stored in a processor-specific KPRCB structure). If an IRP with one stack block is needed, then a small IRP associative list is used. If the IRP must contain more than one stack block, the large IRP associative list is used. These IRPs contain 8 stack blocks (this number is stored in the IopLargeIrpStackLocations kernel variable). In Windows NT4, this figure was 4, but with the advent of PnP, stack depths have increased. If the IRP requires more than 8 stack blocks or the associative list is empty, then the I / O manager has no choice but to allocate memory for the IRP from the non-paged pool. Before returning control,

    Irp.Type                              = IO_TYPE_IRP
    Irp.Size                              = sizeof(IRP) + StackSize * sizeof(IO_STACK_LOCATION)
    Irp.AllocationFlags                   = 
    Irp.StackCount                        = StackSize
    Irp.CurrentLocation                   = StackSize + 1
    Irp.Tail.Overlay.CurrentStackLocation = &Irp + sizeof(IRP) + StackSize * sizeof(IO_STACK_LOCATION)

The most important fields for us at the moment are:

Upon returning from IoAllocateIrp, our IRP looks like this (I used the SoftICE debugger irp command with the -f switch):

 :irp -f 83887008
 MdlAddress *         : 00000000
 Flags                : 00000000
 AssociatedIrp        : 00000000
 &ThreadListEntry     : 83887018
 IoStatus.Status      : 00000000
 IoStatus.Information : 00000000
 RequestorMode        : 00
 PendingReturned      : False
 StackCount           : 05
 CurrentLocation      : 06
 Cancel               : False
 CancelIrql           : 00
 ApcEnvironment       : 00
 UserIosb *           : 00000000
 UserEvent *          : 00000000
 Overlay              : 00000000 00000000
 CancelRoutine *      : 00000000
 UserBuffer *         : 00000000
 Tail.Overlay
        &DeviceQueueEntry : 83887048
        Thread *          : 00000000
        AuxiliaryBuffer * : 00000000
        &ListEntry        : 83887060
        CurrentStackLoc * : 8388712C
        OrigFileObject *  : 00000000
 Tail.Apc *           : 83887048
 Tail.ComplKey        : 00000000

 StackLocation 1 at 83887078:
 <заполнен нулями>

 StackLocation 2 at 8388709C:
 <заполнен нулями>

 StackLocation 3 at 838870C0:
 <заполнен нулями>

 StackLocation 4 at 838870E4:
 <заполнен нулями>

 StackLocation 5 at 83887108:
 <заполнен нулями>

 CurrentStackLocation at 8388712C:
 <заполнен нулями>                      <- недействительный блок стека

IoAllocateIrp only does the stub of the future IRP. Some fields need to be filled in manually.

             mov edi, eax
             assume edi:ptr _IRP

             mov [edi].IoStatus.Status, STATUS_NOT_SUPPORTED
             and [edi].IoStatus.Information, 0

             mov iosb.Status, STATUS_NOT_SUPPORTED
             and iosb.Information, 0

             lea eax, iosb
             mov [edi].UserIosb, eax

             assume edi:nothing 

Forming different types of IRPs may require filling in different fields. I have filled in only the essentials for us and you should not take this as a model. Details can be found in the DDK.

After filling the body of the IRP, we must form a stack block for the driver to which we are addressing the request. If we use stack block numbering as SoftIce uses, then we should fill the stack block number 5. As you remember, now the CurrentStackLocation field points to an invalid stack block. To get a pointer to the next stack block belonging to the driver to which we are addressing the request, we use the IoGetNextIrpStackLocation macro:

 IoGetNextIrpStackLocation MACRO pIrp:REQ
     mov eax, pIrp
     mov eax, (_IRP PTR [eax]).Tail.Overlay.CurrentStackLocation
     sub eax, sizeof IO_STACK_LOCATION
 ENDM

Don't be confused by the word next in the macro name. We are dealing with a stack. "Next driver" means the downstream driver, and "next stack block" means a stack block with an address sizeof (IO_STACK_LOCATION) less than the current stack block. Accordingly, "previous driver" means an upstream driver, and "previous stack block" means a stack block with an address sizeof (IO_STACK_LOCATION) larger than the current stack block. The IoGetNextIrpStackLocation macro takes the value from the CurrentStackLocation field and shrinks it by the size of the IO_STACK_LOCATION structure. Thus, we are moving towards smaller addresses towards the body of the IRP.

             IoGetNextIrpStackLocation edi
             mov ebx, eax
             assume ebx:ptr IO_STACK_LOCATION

             mov [ebx].MajorFunction, IRP_MJ_PNP
             mov [ebx].MinorFunction, IRP_MN_QUERY_PNP_DEVICE_STATE

We send a request of the IRP_MJ_PNP type, and the additional code IRP_MN_QUERY_PNP_DEVICE_STATE determines what kind of information about the PnP characteristics of the device we want to receive.

              invoke KeInitializeEvent, addr keEvent, NotificationEvent, FALSE

We initialize the "event" object. At this facility, we will wait for the completion of the IRP. The event type can also be SyncronizationEvent, since anyway, except for us, no one will wait for him. Both options can be found in the source codes of the drivers.

In just one line, we're going to send an IRP to the kbdclass driver. If we don't take special measures, we will never be able to see our IRP again. Paradoxical as it may seem at first glance, after calling IoCallDriver, you cannot access the IRP. By the end of the article, I hope it will be clear why. The only possibility to regain control of the IRP - is to establish a special procedure - the completion of the procedure (completion routine). The completion routine will be called when any driver down the stack completes the IRP by calling IoCompleteRequest. One of the tasks of the IoCompleteRequest function is precisely the task of calling all completion routines. I named our completion routine IrpComplete, and you can install it using the IoSetCompletionRoutine macro (full version in ntddk.inc):

 IoSetCompletionRoutine MACRO pIrp:REQ, Routine:REQ, CompletionContext:REQ, Success:REQ, Error:REQ, Cancel:REQ

     mov eax, pIrp

     mov eax, (_IRP PTR [eax]).Tail.Overlay.CurrentStackLocation
     sub eax, sizeof IO_STACK_LOCATION

     assume eax:ptr IO_STACK_LOCATION

     push Routine
     pop [eax].CompletionRoutine

     push CompletionContext
     pop [eax].Context

     and byte ptr [eax].Control, 0

     IF Success NE 0
         or byte ptr [eax].Control, SL_INVOKE_ON_SUCCESS
     ENDIF

     IF Error NE 0
         or byte ptr [eax].Control, SL_INVOKE_ON_ERROR
     ENDIF

     IF Cancel NE 0
         or byte ptr [eax].Control, SL_INVOKE_ON_CANCEL
     ENDIF

     assume eax:nothing

 ENDM

The first parameter is a pointer to the IRP, upon completion of which the procedure should be called, the pointer to which is passed in the second parameter. The third parameter is a pointer to any data. This pointer will be passed to the finalization procedure, and in it we will indicate the address of our "event" object, which the finalization procedure, if necessary, will have to transfer to the signal state. The last three parameters determine when the procedure will be called. We want it to be called in any case: when an IRP finishes with a success code, when an IRP finishes with an error code, when an IRP is canceled. Those. no matter how the IRP ends, we will still intercept it on the way back. Note that the IoSetCompletionRoutine macro uses the following stack block, i.e. intended for the downstream driver. Those. the address of the termination procedure and its parameter are placed not in the stack block of the driver to which it belongs, but in the stack block of the downstream driver. Why do we climb into someone else's stack block with our own termination procedure? The fact is that, firstly, we do not have our own stack block, more precisely, we do not need it. We ourselves form the IRP and know perfectly well what it contains. On the other hand, the driver lower in the stack that will complete the IRP does not need a completion routine. He completes it himself and knows perfectly well how. the driver below the stack that will complete the IRP does not need a termination routine. He completes it himself and knows perfectly well how. the driver below the stack that will complete the IRP does not need a termination routine. He completes it himself and knows perfectly well how.

And one more very important point regarding the completion procedures. In general, I / O processing from a physical device is as follows. The driver initiates an I / O operation. When the device completes the operation, it generates an interrupt, which is handled by the interrupt handling routine(Interrupt Service Routine, ISR) registered by the driver. Moreover, processing will take place in the context of the thread that was current at the time of interruption, and this is a random thread. Because ISR operates at a higher IRQL (greater than DISPATCH_LEVEL), the work of all other threads on this processor is blocked. Moreover, all interrupts with the same or lower level are blocked (masked). In order to handle possible interrupts from devices of lower priority, it is necessary to lower the IRQL as soon as possible. To do this, the ISR does only what needs to be done immediately and queues up the so- called deferred procedure call.(Deferred Procedure Call, DPC). DPC works with IRQL = DISPATCH_LEVEL. When the IRQL is lowered to DISPATCH_LEVEL, the system calls the deferred procedure and it does additional operations to complete the IRP. In the very last phase, the deferred procedure calls the IoCompleteRequest, which, as I said above, calls all completion procedures. Therefore, the termination routine can be called in the context of a random thread and at IRQL less than or equal to DISPATCH_LEVEL .

Since the completion routine can be called at a high IRQL, it is obvious that both itself and all the data to which it accesses must be in non-swapped memory. Our completion routine refers to two structures: IO_STATUS_BLOCK and KEVENT (the IRP itself does not count, since it is always allocated from non-swapped memory), which are located on the stack of the thread executing the QueryPnpDeviceState procedure. If this thread waits, then its stack can be paged out into the swap file (which, in this case, is not the system thread). To prevent the system from doing this, you must specify KernelMode in the WaitMode parameter of the wait functions. I have already spoken about this once, but, just in case, I repeat.

             IoSetCompletionRoutine edi, IrpComplete, addr keEvent, TRUE, TRUE, TRUE

             invoke IoCallDriver, esi, edi

Well then. We now have everything we need: the destination formed by the IRP and the termination routine ready to intercept it on the way back. By calling the IoCallDriver function, we send the IRP to the driver serving the device object, the pointer to which is contained in the first parameter.

The implementation of the IoCallDriver function is surprisingly simple:

 NTSTATUS
 IoCallDriver(
     IN PDEVICE_OBJECT  pDeviceObject,
     IN OUT PIRP        pIrp
     )
 {

     NTSTATUS             status
     PIO_STACK_LOCATION   pStack
     PDRIVER_OBJECT       pDriverObject

     Irp->CurrentLocation--

     if  pIrp->CurrentLocation <= 0  {

         KeBugCheckEx( NO_MORE_IRP_STACK_LOCATIONS, pIrp, ... )
     }

     pIrp->Tail.Overlay.CurrentStackLocation -= sizeof(IO_STACK_LOCATION)

     pStack = pIrp->Tail.Overlay.CurrentStackLocation

     pStack->DeviceObject = pDeviceObject

     pDriverObject = pDeviceObject->DriverObject

     status = pDriverObject->MajorFunction[pStack->MajorFunction]( pDeviceObject, pIrp )

     return status
 }

First, IoCallDriver decreases the CurrentLocation value by one, and if it suddenly becomes equal to zero or even less, then the system shows a "blue screen of death", because a zero value in the CurrentLocation field means that we have exhausted all stack blocks and if IoCallDriver goes further, it will simply "overwrite" the IRP body, which sooner or later will still lead to a crash. The value in CurrentStackLocation is then reduced by the size of the IO_STACK_LOCATION structure. Now both fields: CurrentLocation and CurrentStackLocation correspond to the stack block we filled. CurrentLocation is 5 and CurrentStackLocation is 83887108. Our IRP now looks like this:

 :irp -f 83887008
 MdlAddress *         : 00000000
 Flags                : 00000000
 AssociatedIrp        : 00000000
 &ThreadListEntry     : 83887018
 IoStatus.Status      : C00000BB
 IoStatus.Information : 00000000
 RequestorMode        : 00
 PendingReturned      : False
 StackCount           : 05
 CurrentLocation      : 05
 Cancel               : False
 CancelIrql           : 00
 ApcEnvironment       : 00
 UserIosb *           : BE0A4C78
 UserEvent *          : 00000000
 Overlay              : 00000000 00000000
 CancelRoutine *      : 00000000
 UserBuffer *         : 00000000
 Tail.Overlay
        &DeviceQueueEntry : 83887048
        Thread *          : 00000000
        AuxiliaryBuffer * : 00000000
        &ListEntry        : 83887060
        CurrentStackLoc * : 83887108
        OrigFileObject *  : 00000000
 Tail.Apc *           : 83887048
 Tail.ComplKey        : 00000000

 StackLocation 1 at 83887078:
 <заполнен нулями>

 StackLocation 2 at 8388709C:
 <заполнен нулями>

 StackLocation 3 at 838870C0:
 <заполнен нулями>

 StackLocation 4 at 838870E4:
 <заполнен нулями>

 CurrentStackLocation at 83887108:
 MajorFunction     : 1B IRP_MJ_PNP
 MinorFunction     : 14 IRP_MN_QUERY_PNP_DEVICE_STATE
 Control           : E0
 Flags             : 00
 Others            : 00000000 00000000 00000000 00000000
 DeviceObject *    : 81852AB0
 FileObject *      : 00000000
 CompletionRout *  : ED5E14C0
 Context *         : BE0A4C68

Next, IoCallDriver places a pointer to the called device object in the DeviceObject field of the current block of the stack. This pointer may be required by the completion routine. Then a pointer to the driver serving it is retrieved from the device object and one of the driver dispatch procedures is called. Because pStack-> MajorFunction contains IRP_MJ_PNP, IoCallDriver takes a pointer to a procedure from the corresponding element of the MajorFunction array and passes it the addresses of the "device" object and IRP (remember any dispatch function, of which we have written quite a few). If the driver has not entered a pointer to its procedure for processing this IRP type in the corresponding field of the MajorFunction array, then by default there is a pointer to the IopInvalidDeviceRequest system function, which simply returns STATUS_INVALID_DEVICE_REQUEST, and at this point the IRP processing will be complete without starting. If the driver has the necessary procedure, and kbdclass has a procedure for processing IRP_MJ_PNP requests, then we will get into it, and IoCallDriver will return what this procedure returns.

Now, before we dive into kbdclass, let's "step aside" a bit and imagine that the IRP we just generated is not an IRP of the IRP_MJ_PNP type, but a hypothetical IRP_MJ_UNKNOWN, and we send it to the abstract driver unknown, whose dispatch procedure looks like this:

 .data?

 g_IrpQueue          LIST_ENTRY  <>
 g_fCompleteLater    BOOL        ?

 .code

 DispatchUnknown proc uses esi pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP

 local status:NTSTATUS              

     mov esi, pIrp
     assume esi:ptr _IRP

     .if g_fCompleteLater

         IoMarkIrpPending esi

         <LockQueue>

         lea ecx, [esi].Tail.Overlay.ListEntry
         InsertTailList addr g_IrpQueue, ecx

         <UnlockQueue>

         mov status, STATUS_PENDING

     .else

         mov status, STATUS_SUCCESS

         mov [esi].IoStatus.Status, STATUS_SUCCESS
         mov [esi].IoStatus.Information, SOME_INFORMATION
    
         fastcall IofCompleteRequest, esi, IO_NO_INCREMENT

     .endif
    
     assume esi:nothing

     mov eax, status
     ret

 DispatchUnknown endp

The unknown driver either completes the IRP immediately, or enqueues it to complete later. Let's look at the first case first.

Before you add to the queue IRP, the driver should mark it as pending completion of the (pending). This can be done using the IoMarkIrpPending macro, which looks like this:

 IoMarkIrpPending MACRO pIrp:REQ
     mov eax, pIrp
     mov eax, (_IRP PTR [eax]).Tail.Overlay.CurrentStackLocation
     or (IO_STACK_LOCATION PTR [eax]).Control, SL_PENDING_RETURNED
 ENDM

Please note - the flag indicating that the IRP is awaiting completion is placed not in the body of the IRP, but in the current block of the stack. Those. each driver independently of the others can do this operation.

The driver then queues the IRP and returns a STATUS_PENDING code telling the upstream driver that the IRP has been postponed indefinitely. In our case, the upstream driver is our driver and needs the results of the IRP completion. Therefore, we will wait on the "event" object we created.

There are several mechanisms that drivers can use to enqueue IRPs, but in the end it all comes down to adding IRPs to a doubly linked list. In the simplest case, you can use the IRP.Tail.Overlay.ListEntry field. In order to guarantee themselves exclusive access to the queue, drivers use a lock. How queuing and blocking works is not important now.

After some time has passed, the driver decides to remove the IRP from the queue and terminate it.

     <LockQueue>

     RemoveHeadList addr g_IrpQueue
     sub eax, _IRP.Tail.Overlay.ListEntry
     mov esi, eax           ; esi -> _IRP

     <UnlockQueue>

     assume esi:ptr _IRP

     mov [esi].IoStatus.Status, STATUS_SUCCESS
     mov [esi].IoStatus.Information, SOME_INFORMATION

     assume esi:nothing

     fastcall IofCompleteRequest, esi, IO_NO_INCREMENT

This can happen in the context of any thread and at any time (as a result of an interruption). In this case, all that matters to us is that the driver calls the IoCompleteRequest.

 VOID
   ZeroIrpStackLocation(
     PIO_STACK_LOCATION pStack
     )
 {
     pStack->MinorFunction               = 0
     pStack->Flags                       = 0
     pStack->Control                     = 0
     pStack->Parameters.Others.Argument1 = 0
     pStack->Parameters.Others.Argument2 = 0
     pStack->Parameters.Others.Argument3 = 0
     pStack->Parameters.Others.Argument4 = 0
     pStack->FileObject                  = NULL
 }


 VOID
   IoCompleteRequest(
     IN PIRP  pIrp,
     IN CCHAR PriorityBoost
     )
 {

     NTSTATUS           status
     PIO_STACK_LOCATION pStack

     if  pIrp->CurrentLocation > pIrp->StackCount + 1  {

         KeBugCheckEx( MULTIPLE_IRP_COMPLETE_REQUESTS, ... )
     }
 
     ASSERT( pIrp->IoStatus.Status != STATUS_PENDING )

     pStack = IoGetCurrentIrpStackLocation( pIrp )

     pIrp->CurrentLocation++
     pIrp->Tail.Overlay.CurrentStackLocation += sizeof(IO_STACK_LOCATION)

     while  pIrp->CurrentLocation <= pIrp->StackCount + 1  {

         pIrp->PendingReturned = pStack->Control & SL_PENDING_RETURNED

         if  pIrp->IoStatus.Status == STATUS_SUCCESS  &&  pStack->Control & SL_INVOKE_ON_SUCCESS
                 ||
             pIrp->IoStatus.Status != STATUS_SUCCESS  &&  pStack->Control & SL_INVOKE_ON_ERROR
                 ||
             pIrp->Cancel == TRUE  &&  pStack->Control & SL_INVOKE_ON_CANCEL
         {

             ZeroIrpStackLocation( pStack )

             PDEVICE_OBJECT    pDeviceObject

             if  pIrp->CurrentLocation == pIrp->StackCount + 1  {

                 pDeviceObject = NULL

             }  else  {

                 pDeviceObject = IoGetCurrentIrpStackLocation( pIrp )->DeviceObject
             }

             status = pStack->CompletionRoutine( pDeviceObject, pIrp, pStack->Context )

             if  status == STATUS_MORE_PROCESSING_REQUIRED  {

                 return
             }

         }  else  {

             if  pIrp->PendingReturned  &&  pIrp->CurrentLocation <= pIrp->StackCount  {

                 IoMarkIrpPending( pIrp )
             }

             ZeroIrpStackLocation( pStack )
         }

         pStack += sizeof(IO_STACK_LOCATION)

         pIrp->CurrentLocation++
         pIrp->Tail.Overlay.CurrentStackLocation += sizeof(IO_STACK_LOCATION)
     }
 }

Rice. 15-6. Block diagram of the IoCompleteRequest function.

The IoCompleteRequest must iterate through all of the IRPs in the stack, in reverse order, and call all completion routines. As the IRP moves down, the CurrentLocation and CurrentStackLocation values ​​decrease with each IoCallDriver call (except when the driver passes its own stack block to the downstream driver using the IoSkipCurrentIrpStackLocation macro). The IoCompleteRequest does the opposite, starting at the current block on the stack, i.e. the one, the pointer to which is in the CurrentStackLocation field (it was this stack block that was current for the driver that called IoCompleteRequest). When the IoCompleteRequest goes up to the top, the values ​​of these two fields will be the same as they were immediately after the call to IoAllocateIrp. Those. the value in the CurrentLocation field must be one greater than StackCount, and CurrentStackLocation will point to an invalid stack block. Therefore, if CurrentLocation is greater than or equal to StackCount + 1, it means that the IRP has already been completed. And completing an IRP twice is about the same as calling ExFreePool again with the same pointer. The "Blue Screen of Death" is very useful here. That's whyyou can complete an IRP only once .

Next comes the ASSERT debug statement. The code enclosed in the ASSERT macro only gets into the checked build of the system. In a free build of the system, you can catch such a bug using Driver Verifier. I added this line on purpose, because completing an IRP with STATUS_PENDING is a very common error. The IRP can either complete or wait for completion. There is no third.

Rule:

You cannot terminate an IRP with STATUS_PENDING.

Next, IoCompleteRequest gets a pointer to the current stack block by calling the IoGetCurrentIrpStackLocation macro. What block of the stack is the current one in this case? The current block is currently owned by the unknown driver. After all, the IRP moved down only "one step". If unknown driver needed a pointer to its stack block, then by calling IoGetCurrentIrpStackLocation, it would get the same address.

Then IoCompleteRequest spins the loop, going through all the blocks of the stack participating in the IRP processing in reverse order. If the SL_PENDING_RETURNED flag is set in the stack block, then the driver that owns it called IoMarkIrpPending. If so, the IRP.PendingReturned field is set to a nonzero value. And if the SL_PENDING_RETURNED flag is not set, then the IRP.PendingReturned field is cleared. This is so that the upstream driver can see in its completion routine that the downstream driver has marked the IRP pending completion. Drivers should not access foreign stack blocks (an exception is copying / filling a stack block when passing an IRP down the stack). IoCompleteRequest even intentionally zeroes some fields of the processed stack block using ZeroIrpStackLocation (in fact, this is a macro, not a function). Therefore, SL_PENDING_RETURNED is, as it were, "shifted" into the PendingReturned field of the IRP itself. When we get to the circuit in fig. 15-7, the purpose of the PendingReturned field will become clearer.

If the upstream driver has installed a completion routine (you must remember that drivers install completion routines on the downstream driver's stack block), it is called. A pointer to the "device" object belonging to the driver that installed this procedure is passed to the finalization procedure. Since the initiator of the request (our driver, in this case) does not have its own stack block, it will receive NULL as a pointer to the "device" object.

If the termination routine needs to access the current block of the stack, it can also use the IoGetCurrentIrpStackLocation macro. What block of the stack will it get? The termination routine will receive the stack block owned by its driver. Those. in both dispatch and finalization, IoGetCurrentIrpStackLocation returns the same pointer. Can we, as IRP creators, call IoGetCurrentIrpStackLocation in our finalizer? No. More precisely, we will get a pointer, but to an invalid stack block. After all, we do not have our own stack block, tk. we don't need it.

If the completion routine returned STATUS_MORE_PROCESSING_REQUIRED, then IoCompleteRequest immediately returns control without making any unnecessary movements, since she no longer has the right to touch the IRP - perhaps the IRP no longer exists. In our case, this is exactly the case, because we call IoFreeIrp in the finalization procedure and in order to force IoCompleteRequest to immediately stop further actions upon completion of the IRP, we return STATUS_MORE_PROCESSING_REQUIRED. If the completion routine returns any other code, then the IoCompleteRequest continues. The DDK recommends returning STATUS_SUCCESS as "any other code" simply because it is 0, which results in more optimal code being generated by the compiler. In later DDKs, definitions like this can be found:

 #define STATUS_CONTINUE_COMPLETION      STATUS_SUCCESS

 typedef enum _IO_COMPLETION_ROUTINE_RESULT {
     ContinueCompletion = STATUS_CONTINUE_COMPLETION,
     StopCompletion     = STATUS_MORE_PROCESSING_REQUIRED
 } IO_COMPLETION_ROUTINE_RESULT, *PIO_COMPLETION_ROUTINE_RESULT;

The names of the constants ContinueCompletion and StopCompletion reflect the essence much better than STATUS_SUCCESS and STATUS_MORE_PROCESSING_REQUIRED. Thus, by returning StopCompletion, we are telling the IoCompleteRequest function to terminate immediately and return. If we return ContinueCompletion (more precisely, do not return StopCompletion), then the IoCompleteRequest continues the process of completing the IRP.

Why do we need to stop the IoCompleteRequest? As the creators of the IRP, we cannot allow the I / O dispatcher to complete the IRP we created. This is our job. The only way to do this is to establish a termination routine.

If the stack block being processed does not contain a pointer to a completion routine, then IoCompleteRequest checks to see if the IRP.PendingReturned field was set in the previous step. If so, and there is still a valid stack block, it sets the SL_PENDING_RETURNED flag in the previous stack block (this IoCompleteRequest block will process on the next iteration of the loop) using the IoMarkIrpPending macro.

Let's now imagine two bad scenarios:

Scenario 1 : If the driver returns STATUS_PENDING code from dispatch procedure, IoCallDriver will pass this code to us. Seeing this code, we wait indefinitely until our completion routine releases the event. After some time, the unknown driver initiates the completion of the IRP. IoCompleteRequest looks into the stack block belonging to the unknown driver, and, failing to find the SL_PENDING_RETURNED flag there, resets IRP.PendingReturned. Seeing that a completion routine (set by our driver) is set in the stack block, IoCompleteRequest calls it. Upon gaining control, our termination routine does not signal an event and releases the memory occupied by the IRP. As a result, the event will never be released and the thread waiting on it will never resume.

A variation of this scenario would be a situation where the unknown driver queues the IRP and then calls IoMarkIrpPending (meaning that the queue has already been unlocked). Then, even before it gets to the IoMarkIrpPending, the IRP can be dequeued and completed.

Scenario 2 : After receiving a code other than STATUS_PENDING from IoCallDriver, our driver considers the IRP complete and, depending on the erroneously returned code, either receives incorrect data or receives nothing. But this is not the worst thing. Worse if we move IoFreeIrp from the completion routine to the main routine after the IoCallDriver, which we have every right to do. The unknown driver does not know the implementation details of the upstream driver, and should in no way rely on it. Assuming the IRP is complete, we call IoFreeIrp. After some time, the unknown driver tries to retrieve a no longer existing IRP from the queue ...

It’s not hard to guess that there is a simple antidote for Scenario 1: regardless of the value of the PendingReturned field, always call KeSetEvent in the completion routine. It is possible, of course, but then in all cases when the IRP completes immediately, we will call KeSetEvent in vain, and it locks the thread manager database, looks for threads waiting on the event, and makes them scheduled, unlocks the thread manager database. In general, there will be some overhead costs. But that's not the point. We can rewrite our completion routine, but we cannot rewrite the I / O dispatcher code that implements its own logic. The I / O dispatcher does not set up a termination routine at all. It uses different mechanisms, but it also relies on the code returned by IoCallDriver and the value of the PendingReturned field to make decisions.

Rule:

If the driver returns the STATUS_PENDING code from the dispatch procedure, then it must call IoMarkIrpPending first. If the driver calls IoMarkIrpPending in the dispatch procedure, it must return STATUS_PENDING. Either both, or neither.

Let's go back to the keyboard stack. We have already called IoCallDriver and are now in the KeyboardPnP dispatch procedure of the kbdclass driver.

I am using the source code here from the 2003 IFS KIT. In the 2000 DDK, the KeyboardPnP function code is different: the kbdclass driver synchronizes IRP processing using the KeyboardSendIrpSynchronously function, which is almost identical to the I8xSendIrpSynchronously function of the i8042ptr driver (see below). Firstly, it will be easier for us, and secondly, it will introduce some variety.

     PIO_STACK_LOCATION    pStack
     NTSTATUS              status

     pStack = IoGetCurrentIrpStackLocation( pIrp )

     if  pStack->MinorFunction == IRP_MN_QUERY_PNP_DEVICE_STATE  {

         pIrp->IoStatus.Information |= PNP_DEVICE_NOT_DISABLEABLE

         pIrp->IoStatus.Status = STATUS_SUCCESS

         IoCopyCurrentIrpStackLocationToNext( pIrp )
         status = IoCallDriver( NextLowerDeviceObject, pIrp )
     }

     return status

The first thing kbdclass does is get a pointer to its stack block to see what they want from it.

When processing IRP_MN_QUERY_PNP_DEVICE_STATE, the driver must place a flag in the IRP.IoStatus.Information field that defines the state of the device. Moreover, since there is only one field IRP.IoStatus.Information, and there are many drivers in the stack, they all use logical operations to set or clear the necessary flags. The kbdclass driver adds the PNP_DEVICE_NOT_DISABLEABLE flag and places the success code in the IRP. Now he has to pass it on to the downstream driver. At the same time, he is not interested in the further fate of this request, and he does not establish a termination procedure. When the IRP is completed, kbdclass will never know. Despite the fact that after calling IoCallDriver, the pIrp variable will still store the number that was the pointer to the IRP, the kbdclass driver has no right to access this pointer, because, possibly,

Before calling the downstream driver, the kbdclass driver (and any other) must fill the stack block due to it (downstream driver). In this case, since kbdclass does not form a new IRP, but forwards the one given to it from above, it can simply copy its stack block to the next one (remember that this is a stack where everything is turned upside down, i.e. the next one will be the stack block located in memory below). This can be done using the IoCopyCurrentIrpStackLocationToNext macro. An optimized version can be seen in ntddk.inc, and here is a whiter version.

 IoCopyCurrentIrpStackLocationToNext MACRO pIrp:REQ

     push esi
     push edi

     mov eax, pIrp

     mov esi, (_IRP PTR [eax]).Tail.Overlay.CurrentStackLocation

     mov edx, (_IRP PTR [eax]).Tail.Overlay.CurrentStackLocation
     sub edx, sizeof IO_STACK_LOCATION
     mov edi, edx

     mov ecx, sizeof IO_STACK_LOCATION

     rep movsb

     and (IO_STACK_LOCATION PTR [edx]).Control, 0
     and (IO_STACK_LOCATION PTR [edx]).CompletionRoutine, 0
     and (IO_STACK_LOCATION PTR [edx]).Context, 0

     pop edi
     pop esi

 ENDM

As you can see, the macro copies the current stack block to the next one, but the three fields: Control, CompletionRoutine and Context are zeroed out. Why these fields are reset, we know below. Now kbdclass calls IoCallDriver, passing in its NextLowerDeviceObject variable a pointer to the device object directly below it. This pointer is obtained by kbdclass when it is connected to the stack. Because we agreed to consider the classical composition of the stack, the next object on the stack is the "device" object belonging to the i8042ptr driver, and we find ourselves in its dispatch procedure I8xPnP.

     PIO_STACK_LOCATION  pStack
     NTSTATUS            status

     pStack = IoGetCurrentIrpStackLocation( pIrp )

     if  pStack->MinorFunction == IRP_MN_QUERY_PNP_DEVICE_STATE  {

         status = I8xSendIrpSynchronously( TopOfStack, pIrp, FALSE )

         pIrp->IoStatus.Information |= PnpDeviceState
         pIrp->IoStatus.Status = status

         IoCompleteRequest( pIrp, IO_NO_INCREMENT )
     }

     return status;

i8042ptr also receives a pointer to its stack block and synchronously redirects the IRP to the next (downstream) acpi driver, the pointer to which is stored in the TopOfStack variable.

 NTSTATUS
 I8xPnPComplete (
     IN PDEVICE_OBJECT pDeviceObject,
     IN PIRP           pIrp,
     IN PKEVENT        pEvent
     )
 {

     KeSetEvent( pEvent, 0, FALSE )   // Four-F: It's not good to signal event unconditionaly.

     return STATUS_MORE_PROCESSING_REQUIRED
 }


 NTSTATUS
 I8xSendIrpSynchronously (
     IN PDEVICE_OBJECT pDeviceObject,
     IN PIRP           pIrp
     )
 {
     KEVENT   Event
     NTSTATUS status

     KeInitializeEvent( &event, SynchronizationEvent, FALSE )

     IoCopyCurrentIrpStackLocationToNext( pIrp )

     IoSetCompletionRoutine( pIrp, I8xPnPComplete, &Event, TRUE, TRUE, TRUE )

     status = IoCallDriver( pDeviceObject, pIrp )

     if  status == STATUS_PENDING  {

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

        status = pIrp->IoStatus.Status
     }

     return status
 }

I will not analyze the I8xSendIrpSynchronously and I8xPnPComplete functions, because they implement the same logic as our QueryPnpDeviceState and IrpComplete. Having dealt with the code of our driver, you can easily understand how these two functions work.

Upon returning from I8xSendIrpSynchronously, the i8042ptr driver adds its own portion of flags from the PnpDeviceState variable to the Information field and ends the IRP by calling IoCompleteRequest.

Well, and finally, the procedure for dispatching the acpi driver will look like this (in fact, everything is much more complicated):

 NTSTATUS
  SomeProc (
     IN PDEVICE_OBJECT pDeviceObject,
     IN PIRP           pIrp
     )
 {

     pIrp->IoStatus.Information |= PNP_DEVICE_NOT_DISABLEABLE

     pIrp->IoStatus.Status = STATUS_SUCCESS

     IoCompleteRequest( pIrp, IO_NO_INCREMENT )

     return STATUS_SUCCESS
 }

Now consider the case where the IRP processing will be synchronous, i.e. will take place in the context of the same thread. All drivers on the stack complete the IRP immediately, and therefore none of the dispatchers return STATUS_PENDING. We will use the scheme in Fig. 15-7. Having drawn this diagram, I was pleasantly surprised at how well some completely non-obvious things are visible on it.

Rice. 15-7. IRP processing steps.

  1. Our QueryPnpDeviceState driver creates an IRP, initializes an "event" object to wait for the IRP to complete if it is delayed, sets up an IrpComplete completion routine, and sends the IRP to the kbdclass driver.

  2. The kbdclass driver redirects the IRP to the downstream i8042prt driver without setting up a completion routine.

     :irp -f 83887008
     MdlAddress *         : 00000000
     Flags                : 00000000
     AssociatedIrp        : 00000000
     &ThreadListEntry     : 83887018
     IoStatus.Status      : C00000BB
     IoStatus.Information : 00000000
     RequestorMode        : 00
     PendingReturned      : False
     StackCount           : 05
     CurrentLocation      : 04
     Cancel               : False
     CancelIrql           : 00
     ApcEnvironment       : 00
     UserIosb *           : BE0A4C78
     UserEvent *          : 00000000
     Overlay              : 00000000 00000000
     CancelRoutine *      : 00000000
     UserBuffer *         : 00000000
     Tail.Overlay
            &DeviceQueueEntry : 83887048
            Thread *          : 00000000
            AuxiliaryBuffer * : 00000000
            &ListEntry        : 83887060
            CurrentStackLoc * : 838870E4
            OrigFileObject *  : 00000000
     Tail.Apc *           : 83887048
     Tail.ComplKey        : 00000000
    
     StackLocation 1 at 83887078:
     <заполнен нулями>
    
     StackLocation 2 at 8388709C:
     <заполнен нулями>
    
     StackLocation 3 at 838870C0:
     <заполнен нулями>
    
     CurrentStackLocation at 838870E4:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 14 IRP_MN_QUERY_PNP_DEVICE_STATE
     Control           : 00
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 81852CA0
     FileObject *      : 00000000
     CompletionRout *  : 00000000
     Context *         : 00000000
    
     StackLocation 5 at 83887108:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 14 IRP_MN_QUERY_PNP_DEVICE_STATE
     Control           : E0
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 81852AB0
     FileObject *      : 00000000
     CompletionRout *  : ED5E14C0
     Context *         : BE0A4C68
    
    
  3. The i8042prt driver initializes an "event" object on which it will wait for the IRP to complete if completion is delayed, sets up the I8xPnpComplete completion routine and passes the IRP to the downstream acpi driver.

     :irp -f 83887008
     MdlAddress *         : 00000000
     Flags                : 00000000
     AssociatedIrp        : 00000000
     &ThreadListEntry     : 83887018
     IoStatus.Status      : C00000BB
     IoStatus.Information : 00000000
     RequestorMode        : 00
     PendingReturned      : False
     StackCount           : 05
     CurrentLocation      : 03
     Cancel               : False
     CancelIrql           : 00
     ApcEnvironment       : 00
     UserIosb *           : BE0A4C78
     UserEvent *          : 00000000
     Overlay              : 00000000 00000000
     CancelRoutine *      : 00000000
     UserBuffer *         : 00000000
     Tail.Overlay
            &DeviceQueueEntry : 83887048
            Thread *          : 00000000
            AuxiliaryBuffer * : 00000000
            &ListEntry        : 83887060
            CurrentStackLoc * : 838870C0
            OrigFileObject *  : 00000000
     Tail.Apc *           : 83887048
     Tail.ComplKey        : 00000000
     
     StackLocation 1 at 83887078:
     <заполнен нулями>
    
     StackLocation 2 at 8388709C:
     <заполнен нулями>
    
     CurrentStackLocation at 838870C0:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 14 IRP_MN_QUERY_PNP_DEVICE_STATE
     Control           : E0
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 81852CA0
     FileObject *      : 00000000
     CompletionRout *  : ED09043F
     Context *         : BE0A4B64
    
     StackLocation 4 at 838870E4:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 14 IRP_MN_QUERY_PNP_DEVICE_STATE
     Control           : 00
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 81852CA0
     FileObject *      : 00000000
     CompletionRout *  : 00000000
     Context *         : 00000000
    
     StackLocation 5 at 83887108:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 14 IRP_MN_QUERY_PNP_DEVICE_STATE
     Control           : E0
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 81852AB0
     FileObject *      : 00000000
     CompletionRout *  : ED5E14C0
     Context *         : BE0A4C68
    
    
  4. The acpi driver completes the IRP (possibly having previously sent it to some other drivers) by calling IoCompleteRequest.

    The IoCompleteRequest function starts the completion of the IRP. Looks into the stack block owned by the acpi driver. Not finding the SL_PENDING_RETURNED flag there (the acpi driver did not call the IoMarkIrpPending macro), does not set the IRP.PendingReturned field. Finds a pointer to the I8xPnpComplete termination routine of the superior i8042prt driver and calls it.

     :irp -f 83887008
     MdlAddress *         : 00000000
     Flags                : 00000000
     AssociatedIrp        : 00000000
     &ThreadListEntry     : 83887018
     IoStatus.Status      : 00000000         <- STATUS_SUCCESS
     IoStatus.Information : 00000020         <- PNP_DEVICE_NOT_DISABLEABLE
     RequestorMode        : 00
     PendingReturned      : False
     StackCount           : 05
     CurrentLocation      : 04
     Cancel               : False
     CancelIrql           : 00
     ApcEnvironment       : 00
     UserIosb *           : BE0A4C78
     UserEvent *          : 00000000
     Overlay              : 00000000 00000000
     CancelRoutine *      : 00000000
     UserBuffer *         : 00000000
     Tail.Overlay
            &DeviceQueueEntry : 83887048
            Thread *          : 00000000
            AuxiliaryBuffer * : 00000000
            &ListEntry        : 83887060
            CurrentStackLoc * : 838870E4
            OrigFileObject *  : 00000000
     Tail.Apc *           : 83887048
     Tail.ComplKey        : 00000000
    
     StackLocation 1 at 83887078:
     <заполнен нулями>
    
     StackLocation 2 at 8388709C:
     <заполнен нулями>
    
     StackLocation 3 at 838870C0:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 00                  <- обнулено ZeroIrpStackLocation
     Control           : 00                  <- обнулено ZeroIrpStackLocation
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 818A64F0
     FileObject *      : 00000000
     CompletionRout *  : ED09043F
     Context *         : BE0A4B64
    
     CurrentStackLocation at 838870E4:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 14 IRP_MN_QUERY_PNP_DEVICE_STATE
     Control           : 00
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 81852CA0
     FileObject *      : 00000000
     CompletionRout *  : 00000000
     Context *         : 00000000
    
     StackLocation 5 at 83887108:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 14 IRP_MN_QUERY_PNP_DEVICE_STATE
     Control           : E0
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 81852AB0
     FileObject *      : 00000000
     CompletionRout *  : ED5E14C0
     Context *         : BE0A4C68
    
    
  5. The I8xPnpComplete termination routine signals the event in vain (the i8042prt driver does not and will not wait on this event) and returns the STATUS_MORE_PROCESSING_REQUIRED code.

    Upon seeing the STATUS_MORE_PROCESSING_REQUIRED code, the IoCompleteRequest terminates immediately and returns to the acpi driver dispatch routine.

  6. The acpi driver returns STATUS_SUCCESS and we exit the IoCallDriver function in the i8042prt driver.

    Seeing that the code returned from IoCallDriver is not STATUS_PENDING, the i8042prt driver does not wait on the event. Now the i8042prt driver has every right to access the IRP, since set up a termination routine that aborted IRP processing. Since the i8042prt driver interrupted the IRP completion by returning the STATUS_MORE_PROCESSING_REQUIRED code from its completion routine, it must resume this process. Which it does by calling IoCompleteRequest.

    Above, we found out that you cannot complete an IRP twice. Here we see the second call to IoCompleteRequest. Is there a contradiction here? No. Completing an IRP isn't just about calling IoCompleteRequest. This is a multi-step process. At each stage, it can be interrupted and resumed. Only when all these stages are passed is the IRP considered complete.

  7. The IoCompleteRequest function continues to complete the IRP from where it left off, i.e. from the current stack block, and the current one is the stack block of the i8042prt driver. The i8042prt driver stack block does not contain the SL_PENDING_RETURNED flag (the i8042prt driver did not call the IoMarkIrpPending macro either). Therefore, IRP.PendingReturned is reset to zero again. IoCompleteRequest does not find a terminator pointer in the i8042prt driver stack block and goes to the previous and last kbdclass driver stack block. kbdclass also did not use the IoMarkIrpPending macro and IRP.PendingReturned is reset to zero. In the stack block of the kbdclass driver there is a pointer to our IrpComplete completion routine, which is called.

    Recall that when passing the IRP to the downstream driver, the kbdclass driver copied its stack block to the next one using the IoCopyCurrentIrpStackLocationToNext macro. However, this macro does not copy the fields associated with the completion routine. If it did not, then the pointer to our termination routine (it is located in the stack block of the kbdclass driver) would go into the stack block of the i8042prt driver, and our termination routine would be called twice. In the old days, when there was no macro IoCopyCurrentIrpStackLocationToNext, programmers manually copied stack blocks, sometimes forgetting to zero the fields associated with the completion procedure, which led to hard-to-find bugs.

     :irp -f 83887008
     MdlAddress *         : 00000000
     Flags                : 00000000
     AssociatedIrp        : 00000000
     &ThreadListEntry     : 83887018
     IoStatus.Status      : 00000000
     IoStatus.Information : 00000020
     RequestorMode        : 00
     PendingReturned      : False
     StackCount           : 05
     CurrentLocation      : 06
     Cancel               : False
     CancelIrql           : 00
     ApcEnvironment       : 00
     UserIosb *           : BE0A4C78
     UserEvent *          : 00000000
     Overlay              : 00000000 00000000
     CancelRoutine *      : 00000000
     UserBuffer *         : 00000000
     Tail.Overlay
            &DeviceQueueEntry : 83887048
            Thread *          : 00000000
            AuxiliaryBuffer * : 00000000
            &ListEntry        : 83887060
            CurrentStackLoc * : 8388712C
            OrigFileObject *  : 00000000
     Tail.Apc *           : 83887048
     Tail.ComplKey        : 00000000
    
     StackLocation 1 at 83887078:
     <заполнен нулями>
     
     StackLocation 2 at 8388709C:
     <заполнен нулями>
    
     StackLocation 3 at 838870C0:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 00                 <- обнулено ZeroIrpStackLocation
     Control           : 00                 <- обнулено ZeroIrpStackLocation
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 818A64F0
     FileObject *      : 00000000
     CompletionRout *  : ED09043F
     Context *         : BE0A4B64
     
     StackLocation 4 at 838870E4:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 00                 <- обнулено ZeroIrpStackLocation
     Control           : 00
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 81852CA0
     FileObject *      : 00000000
     CompletionRout *  : 00000000
     Context *         : 00000000
     
     StackLocation 5 at 83887108:
     MajorFunction     : 1B IRP_MJ_PNP
     MinorFunction     : 00                 <- обнулено ZeroIrpStackLocation
     Control           : 00                 <- обнулено ZeroIrpStackLocation
     Flags             : 00
     Others            : 00000000 00000000 00000000 00000000
     DeviceObject *    : 81852AB0
     FileObject *      : 00000000
     CompletionRout *  : ED5E14C0
     Context *         : BE0A4C68
    
     CurrentStackLocation at 8388712C:
     <заполнен нулями>                      <- недействительный блок стека
    
    

    Our completion procedure is somewhat smarter. Seeing that the PendingReturned field is zero, it realizes that the downstream driver did not return STATUS_PENDING, which means that the driver dispatcher's QueryPnpDeviceState does not wait on the event. Therefore, it makes no sense to signal it. We set up a completion routine only to delete the IRP we created. We can do this right now by calling IoFreeIrp. Since the IRP is no longer there, we must stop it from completing by returning STATUS_MORE_PROCESSING_REQUIRED.

  8. Upon seeing the STATUS_MORE_PROCESSING_REQUIRED code, the IoCompleteRequest terminates immediately and returns control to the i8042prt driver dispatch procedure. Here you can see very clearly why after calling IoCompleteRequest you cannot access the IRP. After all, perhaps the IRP no longer exists, and the driver that calls IoCompleteRequest cannot find out about it. Note that the IoCompleteRequest function does not return any value.

    Rule:

    After calling the IoCompleteRequest procedure, you cannot access the IRP. The IRP may no longer exist.

  9. The dispatch procedure of the i8042prt driver returns the code returned by the IoCallDriver called by it, which, in this case, is STATUS_SUCCESS and we exit the IoCallDriver function in the kbdclass driver. Again, you can clearly see here why after calling IoCallDriver, you cannot access the IRP, unless, of course, you set up a completion routine and interrupt the completion of the IRP. After all, the IRP does not exist anymore. The kbdclass driver refused to set the completion routine, which means that after calling IoCallDriver, it completely lost control of the IRP. The kbdclass driver does not know who and when will complete the IRP, which means it cannot make any assumptions about whether the IRP still exists or not. The i8042prt driver was able to access the IRP after calling IoCallDriver only because its completion routine aborted the IRP completion process, and the kbdclass driver cannot.

    Rule:

    If you do not have a completion routine, or if your final routine returns a code other than STATUS_MORE_PROCESSING_REQUIRED, you cannot access the IRP after calling IoCallDriver. The IRP may no longer exist.

  10. The dispatch procedure of the driver kbdclass returns the code returned by the IoCallDriver called by it, which, in this case, is STATUS_SUCCESS and we exit the IoCallDriver function in our QueryPnpDeviceState driver.

    Seeing that the code returned from IoCallDriver is not STATUS_PENDING, we do not wait on the event. Although we installed a completion routine and it returned STATUS_MORE_PROCESSING_REQUIRED, we still cannot touch the IRP after returning from IoCallDriver. This is an exception to the rule, since we are the creator of the IRP. Hope it's obvious here. We ourselves removed the IRP in the termination routine and stopped its further termination.

Now let's put the unknown driver in the place of the acpi driver and imagine that it defers the completion of the IRP and returns STATUS_PENDING from its dispatch routine. Those. IRP processing will be asynchronous.

Because The unknown driver postpones the completion of the IRP, then, using the IoMarkIrpPending macro, pops the SL_PENDING_RETURNED flag into its stack block, enqueues the IRP, and returns STATUS_PENDING. We exit the IoCallDriver function in the i8042prt driver. Seeing the STATUS_PENDING code, the i8042prt driver starts waiting for the event to be released and the current thread is blocked.

After some time, as a result of an interruption or for some other reason, but in the context of some other thread, the unknown driver retrieves the IRP from the queue and ends it with a call to IoCompleteRequest. The IoCompleteRequest detects the SL_PENDING_RETURNED flag in the unknown driver's stack block and the IRP.PendingReturned field is nonzero. Having found a pointer to the I8xPnpComplete completion procedure of the superior driver i8042prt, it calls it. The I8xPnpComplete completion routine signals the event and returns STATUS_MORE_PROCESSING_REQUIRED, which causes the IoCompleteRequest to terminate and return to where it was called from.

The thread waiting on the event wakes up. Now the i8042prt driver has every right to access the IRP, since aborted the completion of the IRP by returning STATUS_MORE_PROCESSING_REQUIRED from its completion routine, and knows for sure that the IRP has not completed yet. He does this in order to find out the code with which the deferred IRP ended (see the source code of the I8xSendIrpSynchronously function). The driver extracts this code from the IRP.IoStatus.Status field and will return it from its dispatch procedure, and not the initial STATUS_PENDING. The i8042prt driver then resumes completing the IRP by calling IoCompleteRequest.

The IoCompleteRequest function continues to complete the IRP from where it left off, i.e. from the current stack block, and the current one is the stack block of the i8042prt driver. This stack block does not have the SL_PENDING_RETURNED flag ... More precisely, it should not be there, but take a look at the source code for the 2000 DDK I8xPnPComplete function. You will see lines like this there:

 w2000:

 NTSTATUS
   I8xPnPComplete (
     IN PDEVICE_OBJECT pDeviceObject,
     IN PIRP           pIrp,
     IN PKEVENT        pEvent
     )
 {

     if  pIrp->PendingReturned  {

         IoMarkIrpPending( pIrp )     // Four-F: Do not do this if you return
                                      //         STATUS_MORE_PROCESSING_REQUIRED!
     }

     KeSetEvent( pEvent, 0, FALSE )   // Four-F: It's not good to signal event unconditionaly.
     return STATUS_MORE_PROCESSING_REQUIRED
 }

In the 2003 DDK, these lines are already commented out.

 wnet:

 NTSTATUS
   I8xPnPComplete (
     IN PDEVICE_OBJECT pDeviceObject,
     IN PIRP           pIrp,
     IN PKEVENT        pEvent
     )
 {

     //
     // Since this completion routines sole purpose in life is to synchronize
     // Irp, we know that unless something else happens that the IoCallDriver
     // will unwind AFTER the we have complete this Irp.  Therefore we should
     // NOT bubble up the pending bit.
     //
     // if  pIrp->PendingReturned  {
     //     IoMarkIrpPending( pIrp )
     // }
     //

     KeSetEvent( pEvent, 0, FALSE )   // Four-F: It's not good to signal event unconditionaly.
     return STATUS_MORE_PROCESSING_REQUIRED
 }
 

The two lines highlighted in red must be in the completion routine, but only if it does not return STATUS_MORE_PROCESSING_REQUIRED. We'll see why a little later.

Let's say we are using I8xPnPComplete from 2000 DDK and the SL_PENDING_RETURNED flag is mistakenly present in the i8042prt driver stack block. Seeing this, IoCompleteRequest again puts a nonzero value in the IRP.PendingReturned field. If you analyze the further course of events, you will see that a nonzero value in the IRP.PendingReturned field will reach our completion procedure. When it sees a nonzero IRP.PendingReturned field, it decides that the downstream driver returned STATUS_PENDING and that the dispatcher, QueryPnpDeviceState, is waiting for the event to be released, although this is not the case. In this case, nothing terrible will happen. We'll just signal the event in vain and that's it. In some other case, perhaps more serious consequences are possible, tk. the driver will base its actions on incorrect assumptions.

We have already made sure several times that you should not blindly believe the DDK documentation. Now it turns out that the DDK source cannot be trusted ?! Yes, unfortunately it is. There are especially many, let's say, non-optimal solutions in the 2000 DDK sources. By the way, I advise you not to trust the text of this article :) In the end, we are all human, and people, as you know ...

Analyze the rest of the possible scenarios yourself. I just want to once again draw special attention to the IRP.PendingReturned field. In all the sources that I have seen, including the DDK, the purpose of this field is not quite correctly interpreted. Typically this field is said to tell the I / O manager or upstream driver that the downstream driver marked the IRP as pending (called IoMarkIrpPending and returned STATUS_PENDING from the dispatcher). It's right. It is also said that if a driver marked the IRP as pending completion, then a nonzero value of this field is saved at the end of the IRP to the very top. But this is no longer quite the case. The IoCompleteRequest function (and you and I will also have to take part in this a little later) really tries to save the state of this field, but only if it does not meet a termination routine. Why is this needed? In the scenario we just reviewed with the unknown driver instead of acpi, the IRP processing before it went down to the i8042prt driver was synchronous (it took place in the context of the same thread). After the unknown driver returned STATUS_PENDING from the dispatch procedure, IRP processing became asynchronous (the i8042prt driver termination procedure is called in the context of a random thread, and the i8042prt driver dispatcher procedure waits for events in the context of the original thread). After waiting for the event to be released, the i8042prt driver dispatch procedure continues processing the IRP in the context of the original thread, and the IRP processing becomes synchronous again. Here the dog is buried. All drivers above i8042prt shouldn't know at all, that the unknown driver was delaying the completion of the IRP. This is a problem with the i8042prt driver and he solved it himself. For all upstream drivers, everything was synchronous and remains. In the section between the unknown and i8042prt drivers, the IRP.PendingReturned field will contain a nonzero value, and in the section above the i8042prt driver, it will be zero, because IRP processing is synchronous again and no one is waiting for anyone. I hope I explained it clearly and was not mistaken anywhere :)

Well, okay, all the completion routines we've seen so far have returned STATUS_MORE_PROCESSING_REQUIRED. But, as we found out above, this is not the only possible return code. This termination code is returned in one of three cases:

  1. The driver-creator of the IRP wants to see its child again, in order to ... let's put it mildly, release it (for example, our driver) or reuse it;
  2. The driver wants to synchronize IRP processing (example - i8042prt driver);
  3. Because the completion routine can be called at a higher IRQL, the driver wants to do some additional processing on PASSIVE_LEVEL in its dispatch routine.

If the driver does not need such functionality, but it still needs to intercept the IRP on the way back (for example, in order to view the data read from the disk or the code of the pressed key, which we will do in the next article) and the driver can do all the processing in the terminator, even at DISPATCH_LEVEL, then the terminator does not need to interrupt the IRP completion and can return STATUS_SUCCESS or ContinueCompletion (which is the same thing).

In this case, the completion routine might look something like this:

 JustComplete proc uses esi edi ebx pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP, pContext:PVOID

     mov esi, pIrp
     assume esi:ptr _IRP

     .if [esi].IoStatus.Status == STATUS_SUCCESS
        
         mov edi, [esi].AssociatedIrp.SystemBuffer
         assume edi:ptr SOME_DATA
        
         ; Что-то делаем с данными

         assume edi:nothing
    
     .endif
    
     .if [esi].PendingReturned
         IoMarkIrpPending esi
     .endif

     assume esi:nothing

     mov eax, STATUS_SUCCESS
     ret

 JustComplete endp

The most important thing here, in the context of our conversation, is the call to the IoMarkIrpPending macro if the IRP.PendingReturned field is not zero. Above, we figured out that IoCompleteRequest sort of "shifts" the SL_PENDING_RETURNED flag from the current stack block into the PendingReturned field of the IRP itself, and vice versa, if there is no completion procedure in the stack block, and the PendingReturned field is not equal to zero, then it calls the IoMarkIrpPending macro. In short, IoCompleteRequest is trying to convey to the first termination routine it encounters, the fact that some downstream driver marked the IRP as pending completion. When IoCompleteRequest finds a completion routine, it assigns this task to it (see the source code of IoCompleteRequest, or better the block diagram).

Imagine that instead of the acpi driver, we have the unknown driver and the I8xPnPComplete completion procedure of the i8042prt driver is similar to the JustComplete procedure, i.e. does not signal the event and returns STATUS_SUCCESS code. Accordingly, the dispatch procedure of the i8042prt driver does not initialize or wait for any event, but simply returns the code returned by IoCallDriver.

The unknown driver calls the IoMarkIrpPending macro, queues the IRP, and returns STATUS_PENDING. This code goes up to our dispatch procedure and we start to wait.

Some time later, due to an interruption, or for some other reason, but in the context of some other thread, the unknown driver fetches the IRP from the queue and ends it with a call to IoCompleteRequest. The IoCompleteRequest detects the SL_PENDING_RETURNED flag in the unknown driver's stack block and the IRP.PendingReturned field is nonzero. Having found a pointer to the JustComplete completion routine of the upstream driver i8042prt, it calls it (I repeat, we have replaced the code with JustComplete). Having done its work, the JustComplete completion routine sees that the IRP.PendingReturned field is not zero and, by calling the IoMarkIrpPending macro, puts the SL_PENDING_RETURNED flag into its stack block. The IoCompleteRequest function does the same in the else branch, but since IoCompleteRequest has met a completion routine, then this task is transferred to it. Because the I8xPnpComplete completion procedure returns a code other than STATUS_MORE_PROCESSING_REQUIRED, the IoCompleteRequest function continues to climb up the stack blocks. Upon further analysis, you will see that the information that the IRP has been marked as pending completion as a nonzero value in the IRP.PendingReturned field is passed through our completion routine. Our termination routine understands that the dispatch routine, QueryPnpDeviceState, waits on the event, signals it, and everything ends well. PendingReturned arrives safely at our completion procedure. Our termination routine understands that the dispatch routine, QueryPnpDeviceState, waits on the event, signals it, and everything ends well. PendingReturned arrives safely at our completion procedure. Our termination routine understands that the dispatch routine, QueryPnpDeviceState, waits on the event, signals it, and everything ends well.

And if you also analyze what will happen if the JustComplete completion routine forgets to properly use the IoMarkIrpPending macro, you will come to another rule.

Rule:

If the completion routine returns a code other than STATUS_MORE_PROCESSING_REQUIRED, then it must use (anywhere) the IoMarkIrpPending macro in this way.

if pIrp-> PendingReturned {

IoMarkIrpPending (pIrp)
}

And the last thing. Because the termination routine has no other way of knowing what code the downstream driver is completing the IRP with other than referring to the IRP.IoStatus.Status field, we will write the last rule.

Rule:

Before calling IoCompleteRequest in the dispatch procedure, the driver must place the code with which it terminates the IRP in the IRP.IoStatus.Status field and return the same code from the dispatch procedure.

Starting to write this "endless" article, I also planned to talk about what logic the I / O dispatcher uses when processing IRPs. most often it is he who is the creator of the IRP, but I feel that my strength is leaving me. If you are interested in this question, I recommend reading the article "How Windows NT Handles I / O Completion" in the IFS KIT or "The NT Insider" ( http://www.osronline.com/ ). Unfortunately, you won't find the source code for the I / O dispatcher there, but you will get a general idea.



What you should and shouldn't do

Summarize.

Rule 1:

Before calling IoCompleteRequest in the dispatch procedure, the driver must place the code with which it terminates the IRP in the IRP.IoStatus.Status field and return the same code from the dispatch procedure.

Rule 2:

After calling the IoCompleteRequest procedure, you cannot access the IRP. The IRP may no longer exist.

Rule 3:

You cannot terminate an IRP with STATUS_PENDING.

Rule 4:

If the driver returns the STATUS_PENDING code from the dispatch procedure, then it must call IoMarkIrpPending first. If the driver calls IoMarkIrpPending in the dispatch procedure, it must return STATUS_PENDING. Either both, or neither.

Rule 5:

If you do not have a completion routine, or if your final routine returns a code other than STATUS_MORE_PROCESSING_REQUIRED, you cannot access the IRP after calling IoCallDriver. The IRP may no longer exist.

Rule 6:

If the completion routine returns a code other than STATUS_MORE_PROCESSING_REQUIRED, then it must use (anywhere) the IoMarkIrpPending macro in this way.

if pIrp-> PendingReturned {

IoMarkIrpPending (pIrp)
}

Some of these rules can probably be violated if you have a very good understanding of the details of the IRP handling mechanism. If there is no such idea, then it is better to follow them strictly.

Next time we will try to apply some of the knowledge gained today in practice.

The source code of the driver in the archive .