IRPs (I/O request packets) are the probably one of most important concepts to understand since they form the foundation of I/O. Please note that the I/O Manager and IRPs are a very lengthy topic, there are many nuances to learn and most of the I/O Manager’s functionality is undocumented. In this article, we’ll be covering the general structure of IRPs and how they are related to the device stack. This will help you understand the purpose of I/O stack locations and how they relate to IRP completion. With this knowledge, you’ll be able to understand and solve a number of more difficult bugchecks, especially crashes which involve a Stop 0x9F, Stop 0x133 or Stop 0x44.
With almost every I/O operation, wherever it be a disk read or a network request, an IRP is usually involved at some point during the lifetime of the operation. IRPs are created during the initiation of a I/O operation and disposed of once the I/O operation has been completed. They are used to enable drivers to communicate with each other during I/O, and the IRP is merely an abstraction of a I/O operation being performed. They are represented by a structure named _IRP. Here the word “opaque” refers to the fact that the structure is partially undocumented.
Typically, an IRP is intialised by the I/O Manager or one of its children: PnP Manager and the Power Manager. Please note that I said “typically” because drivers can create their own IRPs, and as you can imagine, the number of issues which can come with it. In fact, the I/O Manager is simply a set of APIs, there is no process or service which runs in the background.
IRPs are always allocated using non-paged pool. When an IRP is initialised it will specify a number of I/O stack locations. Since most IRPs typically use a common number of stack locations, lookaside lists are used for efficiency. The small lookaside list is used for IRPs which have one stack location; the medium lookaside list is used for requests which have between 2 and 4 stack locations and the large lookaside list is used for requests which have more than 4 stack locations. A non-paged lookaside list is typically represented by an opaque structure called NPAGED_LOOKASIDE_LIST.
The I/O Manager will initialise the first I/O stack location, however, it is the responsibility of all the subsequent drivers to initialise the other required I/O stack locations. A driver will only initialise the next I/O stack location directly below it and not the others. If not enough I/O stack locations are created with the IRP, then eventually the system will crash with a Stop 0x35.
I/O Stack Locations:
Let’s examine the general structure of an IRP and see where I/O stack locations come into play.
8: kd> dt _IRP nt!_IRP +0x000 Type : Int2B +0x002 Size : Uint2B +0x004 AllocationProcessorNumber : Uint2B +0x006 Reserved : Uint2B +0x008 MdlAddress : Ptr64 _MDL +0x010 Flags : Uint4B +0x018 AssociatedIrp : <anonymous-tag> +0x020 ThreadListEntry : _LIST_ENTRY +0x030 IoStatus : _IO_STATUS_BLOCK +0x040 RequestorMode : Char +0x041 PendingReturned : UChar +0x042 StackCount : Char +0x043 CurrentLocation : Char +0x044 Cancel : UChar +0x045 CancelIrql : UChar +0x046 ApcEnvironment : Char +0x047 AllocationFlags : UChar +0x048 UserIosb : Ptr64 _IO_STATUS_BLOCK +0x050 UserEvent : Ptr64 _KEVENT +0x058 Overlay : <anonymous-tag> +0x068 CancelRoutine : Ptr64 void +0x070 UserBuffer : Ptr64 Void +0x078 Tail : <anonymous-tag>
The StackCount member indicates the number of stack locations which have been allocated to the IRP. The CurrentLocation member indicates which I/O stack location is currently active. As you can see, both members are char types and thus only take up 1 byte.
In fact, the I/O stack locations are connected to the end of the IRP as an array of I/O stack location structures. You can find the first I/O stack location by using the following technique. Firstly, dump the size of the IRP structure using the following expression.
0: kd> ?? sizeof(_IRP) unsigned int64 0xd0
Next, find the size of the I/O stack location structure.
0: kd> ?? sizeof(_IO_STACK_LOCATION) unsigned int64 0x48
Now, we have two offsets to work with and we’ll be using these with the address of a known IRP. I’m going to dump all the stack locations using the !irp command to demonstrate where we are in the stack.
0: kd> !irp ffff870aba01f8a0 Irp is active with 6 stacks 4 is current (= 0xffff870aba01fa48) No Mdl: No System Buffer: Thread 00000000: Irp stack trace. cmd flg cl Device File Completion-Context [N/A(0), N/A(0)] 0 0 00000000 00000000 00000000-00000000 Args: 00000000 00000000 00000000 00000000 [N/A(0), N/A(0)] 0 0 00000000 00000000 00000000-00000000 Args: 00000000 00000000 00000000 00000000 [IRP_MJ_POWER(16), IRP_MN_WAIT_WAKE(0)] 0 0 ffff870ab6e72050 00000000 fffff80486559190-ffff870aba05a5c0 \Driver\storahci CLASSPNP!ClasspPowerUpCompletion Args: 00000000 00000000 00000000 00000000 >[IRP_MJ_POWER(16), IRP_MN_SET_POWER(2)] 0 e1 ffff870aba05a080 00000000 fffff804855f4730-00000000 Success Error Cancel pending \Driver\disk partmgr!PmPowerCompletion Args: 00000000 00000001 00000001 00000000 [IRP_MJ_POWER(16), IRP_MN_SET_POWER(2)] 0 e1 ffff870ab6fa58d0 00000000 fffff80482177aa0-ffff870aba0721e8 Success Error Cancel pending \Driver\partmgr nt!PopRequestCompletion Args: 00000000 00000001 00000001 00000000 [N/A(0), N/A(0)] 0 0 00000000 00000000 00000000-ffff870aba0721e8 Args: 00000000 00000000 00000000 00000000
We’re got the base address of the structure and we know the size, so let’s apply that together to find the first I/O stack location. Please note that it will directly follow the stack shown above.
0: kd> ? ffff870aba01f8a0+d0 Evaluate expression: -132994836596368 = ffff870a`ba01f970
Let’s dump it like so.
0: kd> dt _IO_STACK_LOCATION ffff870a`ba01f970 win32k!_IO_STACK_LOCATION +0x000 MajorFunction : 0 '' +0x001 MinorFunction : 0 '' +0x002 Flags : 0 '' +0x003 Control : 0 '' +0x008 Parameters : _IO_STACK_LOCATION::<unnamed-type-Parameters> +0x028 DeviceObject : (null) +0x030 FileObject : (null) +0x038 CompletionRoutine : (null) +0x040 Context : (null)
It’s empty as shown in the stack produced by the !irp command. This is to be expected. Let’s jump to the third frame since we know that has data.
0: kd> ? ffff870a`ba01f970+(48*2) Evaluate expression: -132994836596224 = ffff870a`ba01fa00
Let’s break down this expression quickly. I’ve used the address of the first I/O stack location and then added an offset equivalent to the size of two I/O stack locations since we’re jumping to the third one. Remember the size of a single I/O stack location is 0x48 bytes.
0: kd> dt _IO_STACK_LOCATION ffff870a`ba01fa00 win32k!_IO_STACK_LOCATION +0x000 MajorFunction : 0x16 '' +0x001 MinorFunction : 0 '' +0x002 Flags : 0 '' +0x003 Control : 0 '' +0x008 Parameters : _IO_STACK_LOCATION::<unnamed-type-Parameters> +0x028 DeviceObject : 0xffff870a`b6e72050 _DEVICE_OBJECT +0x030 FileObject : (null) +0x038 CompletionRoutine : 0xfffff804`86559190 long CLASSPNP!ClasspPowerUpCompletion+0 +0x040 Context : 0xffff870a`ba05a5c0 Void
As we can see, it’s the same as the third I/O stack location. For completeness, let’s dump the next I/O stack location.
0: kd> ? ffff870a`ba01fa00+48 Evaluate expression: -132994836596152 = ffff870a`ba01fa48
0: kd> dt _IO_STACK_LOCATION ffff870aba01fa48 win32k!_IO_STACK_LOCATION +0x000 MajorFunction : 0x16 '' +0x001 MinorFunction : 0x2 '' +0x002 Flags : 0 '' +0x003 Control : 0xe1 '' +0x008 Parameters : _IO_STACK_LOCATION::<unnamed-type-Parameters> +0x028 DeviceObject : 0xffff870a`ba05a080 _DEVICE_OBJECT +0x030 FileObject : (null) +0x038 CompletionRoutine : 0xfffff804`855f4730 long partmgr!PmPowerCompletion+0 +0x040 Context : (null)
Since we had known the address of the active I/O stack location, we could have also simply used +- 0x48 with the known address to see the next and previous I/O stack location.
The number of I/O stack locations should match the number of devices in the device stack. Please note that an IRP may pass through several different device stacks, so as a simplification, when I refer to the device stack, I’ll be talking about all the different device stacks merged together. In our example above, there would have been three different device stacks or three different device nodes. A single device stack represents a device node. Bear in mind, when I mention the word “device” I’m referring to the device node.
Device nodes are located within the device tree and represent an entire device. The device could be a SSD for example. Before we delve into device stacks, it should be noted that there are three different types of device object in the WDM (Windows Driver Model): physical device objects, functional device objects and filter device objects.
Physical device objects aren’t strictly “physical” and merely represent the fact that a device exists within the bus slot. For example, a graphics card driver would generate a form of physical device object. Physical device objects are created by bus drivers such as pci.sys. Upon boot, the PnP Manager will sent out an IRP with the Minor Code (we’ll discuss these later) QUERY_DEVICE_RELATIONS, this is what will be used to determine if a device exists on that particular bus, and if so, a physical device object will generated. You’ll actually end up with a Stop 0xCA crash if this isn’t handled properly.
Functional device objects are the objects which are the actually ‘heavy-duty’ devices which complete the work for the device. These are created by the functional device driver (nvlddmkm.sys) and provide the actual functionality.
Filter device objects are optional and a device may have several filter objects associated to it. These are created by device filter drivers.
Now that we’ve discussed device objects and I/O stack locations, let’s see how they relate to each other. To begin, let’s dump the device stack shown in the IRP above. We can do this with a known device object and the !devstack command.
Notice how the device objects correspond to the same device objects shown in the IRP? The first device object to be created at the bottom and the last device object to be created is shown at the top. Each device stack will have one physical device object which is always created first, followed by one functional device object and then a set of optional filter drivers. From the example below, we can see that storahci.sys is the bus driver; disk.sys is the functional device driver and partmgr.sys is the upper filter driver. Any filter drivers which are created before the functional device object are known as lower filter drivers.
Each device object is “attached” to the device object below it through the use of a pointer. This pointer is stored in the the AttachedDevice member of the device object structure shown earlier. The member shows the device object which was attaches above it. If no device object exists above itself, then the member will be null.
0: kd> dt _DEVICE_OBJECT -y AttachedDevice ffff870ab6e72050 win32k!_DEVICE_OBJECT +0x018 AttachedDevice : 0xffff870a`ba05a080 _DEVICE_OBJECT
Each device object attaches itself to the device object below it by calling IoAttachDeviceToDeviceStack API function. Alternatively, in some cases, the IoAttachDevice API function may be used instead.
0: kd> !devstack ffff870ab6e72050 !DevObj !DrvObj !DevExt ObjectName ffff870ab6fa58d0 \Driver\partmgr ffff870ab6fa5a20 InfoMask field not found for _OBJECT_HEADER at ffff870ab6fa58a0 ffff870aba05a080 \Driver\disk ffff870aba05a1d0 InfoMask field not found for _OBJECT_HEADER at ffff870aba05a050 > ffff870ab6e72050 \Driver\storahci ffff870ab6e721a0 Cannot read info offset from nt!ObpInfoMaskToOffset !DevNode ffff870ab6e9f420 : DeviceInst is "SCSI\Disk&Ven_WDC&Prod_WD2500BEVT-22ZCT\5&399a7c98&0&040000" ServiceName is "disk"
The command also provides us with the corresponding device node. However, as we can already see from the instance path, it is likely we are dealing with a Western Digital hard-disk. Nevertheless, let’s dump the device node and see what we can find.
0: kd> !devnode ffff870ab6e9f420 DevNode 0xffff870ab6e9f420 for PDO 0xffff870ab6e72050 Parent 0xffff870ab6dae9a0 Sibling 0xffff870ab6e9f750 Child 0000000000 InstancePath is "SCSI\Disk&Ven_WDC&Prod_WD2500BEVT-22ZCT\5&399a7c98&0&040000" ServiceName is "disk" State = DeviceNodeStarted (0x308) Previous State = DeviceNodeEnumerateCompletion (0x30d) StateHistory = DeviceNodeEnumerateCompletion (0x30d) StateHistory = DeviceNodeEnumeratePending (0x30c) StateHistory = DeviceNodeStarted (0x308) StateHistory = DeviceNodeStartPostWork (0x307) StateHistory = DeviceNodeStartCompletion (0x306) StateHistory = DeviceNodeStartPending (0x305) StateHistory = DeviceNodeResourcesAssigned (0x304) StateHistory = DeviceNodeDriversAdded (0x303) StateHistory = DeviceNodeInitialized (0x302) StateHistory = DeviceNodeUninitialized (0x301) StateHistory = Unknown State (0x0) StateHistory = Unknown State (0x0) StateHistory = Unknown State (0x0) StateHistory = Unknown State (0x0) StateHistory = Unknown State (0x0) StateHistory = Unknown State (0x0) StateHistory = Unknown State (0x0) StateHistory = Unknown State (0x0) StateHistory = Unknown State (0x0) StateHistory = Unknown State (0x0) Flags (0x24000130) DNF_ENUMERATED, DNF_IDS_QUERIED, DNF_NO_RESOURCE_REQUIRED, DNF_NO_LOWER_DEVICE_FILTERS, DNF_NO_UPPER_DEVICE_FILTERS CapabilityFlags (0x00002180) SilentInstall, RawDeviceOK, WakeFromD3
As mentioned earlier, device nodes are part of a larger structure known as a device tree. From the output shown above, we can see that the device node has one sibling node and one parent node. We can use these addresses to transverse through the device tree if we so wish.
IRP Major Codes & Minor Codes:
Now that we have a general idea of how I/O stack locations of an IRP and device stacks are related to each other. Let’s discuss what happens when a device driver receives an IRP. An IRP is always sent to the uppermost driver of a device stack, or put another way, the first I/O stack location associated to the IRP. The device driver receives the IRP via it’s dispatch routine. To illustrate this, I’ll dump the dispatch table for the storahci.sys driver.
0: kd> !drvobj \Driver\storahci 7 Driver object (ffffe3845e4ec410) is for: \Driver\storahci Driver Extension List: (id , addr) (fffff8003ad03b60 ffffe3845d6f5ba0) Device Object list: ffffe3845e576050 DriverEntry: fffff8003acbe010 storahci!GsDriverEntry DriverStartIo: 00000000 DriverUnload: fffff8003ad02e30 storport!RaDriverUnload AddDevice: fffff8003ad02b60 storport!RaDriverAddDevice Dispatch routines:  IRP_MJ_CREATE fffff8003ad41780 storport!RaDriverCreateIrp  IRP_MJ_CREATE_NAMED_PIPE fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_CLOSE fffff8003ad41830 storport!RaDriverCloseIrp  IRP_MJ_READ fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_WRITE fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_QUERY_INFORMATION fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_SET_INFORMATION fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_QUERY_EA fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_SET_EA fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_FLUSH_BUFFERS fffff80038706e00 nt!IopInvalidDeviceRequest [0a] IRP_MJ_QUERY_VOLUME_INFORMATION fffff80038706e00 nt!IopInvalidDeviceRequest [0b] IRP_MJ_SET_VOLUME_INFORMATION fffff80038706e00 nt!IopInvalidDeviceRequest [0c] IRP_MJ_DIRECTORY_CONTROL fffff80038706e00 nt!IopInvalidDeviceRequest [0d] IRP_MJ_FILE_SYSTEM_CONTROL fffff80038706e00 nt!IopInvalidDeviceRequest [0e] IRP_MJ_DEVICE_CONTROL fffff8003ad27230 storport!SrbShimHookDeviceControl [0f] IRP_MJ_INTERNAL_DEVICE_CONTROL fffff8003acda230 storport!RaDriverScsiIrp  IRP_MJ_SHUTDOWN fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_LOCK_CONTROL fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_CLEANUP fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_CREATE_MAILSLOT fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_QUERY_SECURITY fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_SET_SECURITY fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_POWER fffff8003acdee00 storport!RaDriverPowerIrp  IRP_MJ_SYSTEM_CONTROL fffff8003ad43dd0 storport!RaDriverSystemControlIrp  IRP_MJ_DEVICE_CHANGE fffff80038706e00 nt!IopInvalidDeviceRequest  IRP_MJ_QUERY_QUOTA fffff80038706e00 nt!IopInvalidDeviceRequest [1a] IRP_MJ_SET_QUOTA fffff80038706e00 nt!IopInvalidDeviceRequest [1b] IRP_MJ_PNP fffff8003acd7d30 storport!RaDriverPnpIrp
If a driver supports a particular IRP then it will have associated dispatch routine in its dispatch table. In this case, when the driver receives an IRP with a major code IRP_MJ_POWER, the storport!RaDriverPowerIrp function will be called. Each major code will then have a set of minor codes associated with it. When the driver receives the IRP via its dispatch routine, it will check what the minor code of the request is and then respond accordingly. In our case, we can see that the device from resuming from a sleep state.
The dispatch table is simply an array of function pointers which point to a dispatch routine. On x64 systems, each entry in the table is 8 bytes. For those interested, we can dump the dispatch table without the assistance of !drvobj using the following method.
0: kd> dt _DRIVER_OBJECT ffffe3845e4ec410 nt!_DRIVER_OBJECT +0x000 Type : 0n4 +0x002 Size : 0n336 +0x008 DeviceObject : 0xffffe384`5e576050 _DEVICE_OBJECT +0x010 Flags : 0x412 +0x018 DriverStart : 0xfffff800`3ac90000 Void +0x020 DriverSize : 0x32000 +0x028 DriverSection : 0xffffe384`5c2806d0 Void +0x030 DriverExtension : 0xffffe384`5e4ec560 _DRIVER_EXTENSION +0x038 DriverName : _UNICODE_STRING "\Driver\storahci" +0x048 HardwareDatabase : 0xfffff800`3912d990 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM" +0x050 FastIoDispatch : (null) +0x058 DriverInit : 0xfffff800`3acbe010 long storahci!GsDriverEntry+0 +0x060 DriverStartIo : (null) +0x068 DriverUnload : 0xfffff800`3ad02e30 void storport!RaDriverUnload+0 +0x070 MajorFunction :  0xfffff800`3ad41780 long storport!RaDriverCreateIrp+0
0: kd> ? ffffe3845e4ec410+70 Evaluate expression: -31317319302016 = ffffe384`5e4ec480
This is the base address of the array, from here, we can use the dps command and dump all the function pointers.
0: kd> dps ffffe384`5e4ec480 L20 ffffe384`5e4ec480 fffff800`3ad41780 storport!RaDriverCreateIrp << Each offset is 8 bytes; second address is the dispatch routine address ffffe384`5e4ec488 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec490 fffff800`3ad41830 storport!RaDriverCloseIrp ffffe384`5e4ec498 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4a0 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4a8 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4b0 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4b8 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4c0 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4c8 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4d0 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4d8 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4e0 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4e8 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec4f0 fffff800`3ad27230 storport!SrbShimHookDeviceControl ffffe384`5e4ec4f8 fffff800`3acda230 storport!RaDriverScsiIrp ffffe384`5e4ec500 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec508 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec510 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec518 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec520 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec528 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec530 fffff800`3acdee00 storport!RaDriverPowerIrp ffffe384`5e4ec538 fffff800`3ad43dd0 storport!RaDriverSystemControlIrp ffffe384`5e4ec540 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec548 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec550 fffff800`38706e00 nt!IopInvalidDeviceRequest ffffe384`5e4ec558 fffff800`3acd7d30 storport!RaDriverPnpIrp [...]
0: kd> ? ffffe384`5e4ec480+0n8 Evaluate expression: -31317319302008 = ffffe384`5e4ec488
This produces the following:
0: kd> dps ffffe384`5e4ec488 L1 ffffe384`5e4ec488 fffff800`38706e00 nt!IopInvalidDeviceRequest
0: kd> ? ffffe384`5e4ec480+0n16 Evaluate expression: -31317319302000 = ffffe384`5e4ec490
0: kd> dps ffffe384`5e4ec490 L1 ffffe384`5e4ec490 fffff800`3ad41830 storport!RaDriverCloseIrp
Before we move onto describing how an IRP is completed, I should mention the difference between a Major Code (MJ) and Minor Code (MN) for an IRP is. An MJ provides a category for the type of I/O operation, there is a MJ for every I/O stack location for the associated IRP, and it will be the exact same for each location. A device driver must provide an appropriate dispatch routine for every MJ it wishes to support. For example, if a device driver wishes to support read requests, then it must provide a routine for the IRP_MJ_READ major code.
In some cases, a MJ might be accompanied by a MN, this is used to describe a particular kind of I/O request for the corresponding MJ. For instance, let’s take a look at the IRP_MJ_POWER IRP. It has four different minor codes associated with it, each of which represents a slightly different kind of power request. It wouldn’t be wise to have one general dispatch routine for handling a device powering up and a device powering down. Instead, a handler method is provided as the dispatch routine, and smaller functions are assigned for each individual minor code. One of the MNs you’ll frequently see for a IRP_MJ_POWER request is the IRP_MN_SET_POWER minor code; this is used to set the power state of a particular device.
Let’s take a brief look at the completion of an IRP and how it transverses through the device stack. Please note that we’ll assume that the IRP will pass through the device stack with no issues and completes successfully for the sake of simplicity. This is usually not the case unfortunately, hence why you’ll see many Stop 0x9Fs and Stop 0x133s.
As noted earlier, the IRP will always be sent to the top of the device stack, from which the driver will examine through its dispatch routine. When the driver successfully receives the IRP, it has a few different decisions it can make. Please note that the scenarios described are general cases, please bear in mind, there are particular nauances which must be followed for Power and PnP IRPs.
If the IRP is uninteresting to the driver and it has no further input, it can simply pass the IRP onto the next device in the stack by calling IoSkipCurrentIrpLocation and then subsequently calling IoCallDriver. Remember it is the responsibility of the driver to properly initialise the next I/O stack location before passing the IRP on. Additionally, it can add a completion routine to the stack location by calling IoSetCompletionRoutine and then pass the request to the next driver in the stack.
Alternatively, it can complete the IRP immediately and the lower-level drivers will never see the IRP. This is because once an IRP has been completed, it transverses up the device stack, with each stack location being examined to see if it has a registered completion routine, and if so, this is called as well. This process continues until the request reaches the original caller or the I/O Manager.
Lastly, it could do what we all love, and mark the IRP as pending by calling IoMarkIrpPending and returning a status code of STATUS_PENDING. This successfully pends the IRP with the idea that it will be completed at a later time.
I hope that this article helps to clarify how I/O stack locations and device stacks are related, along with their role in IRP completion. If you have any questions, then please let me know.
Windows Internals 7th Edition Part 1
Programming the Windows Kernel
Programming the Windows Driver Model 2nd Edition