Cached at:
10/06/26, 07:27 PM
# Inside the Windows I/O Manager : Deep dive into how read I/O requests are initialized.
Source: [https://winware31.blogspot.com/2026/10/inside-windows-io-manager-deep-dive.html](https://winware31.blogspot.com/2026/10/inside-windows-io-manager-deep-dive.html)
## **Introduction:**
If you are someone who periodically debug and reverse many different Windows programs, you may have noticed a common point shared among them despite they aren't related in any sense\. This point is that the vast majority of them use at some stage a famous well documented***Win32***API named
***ReadFile\( \)***exported by the module***KERNELBASE\.dll***\. For someone who doesn't do Windows native programming, he may think that***ReadFile\( \)***is solely used to read data from a file as its name suggests \(a file in its traditional meaning is a filesystem file\)\. Despite this is not totally incorrect, it's not the full image\. Before continuing, let me clarify what is the full definition of a file in the world of Windows\. So, a file simply is an I/O endpoint to which end users can send input and from which they can receive output\. Filesystem files are just one case, for example file system directories are also considered files since they are valid I/O endpoints; named pipes and mailslots are also files; even open instances of device objects are considered files \(device objects and device stacks are beyond the scope of this article, if you want to understand these concepts checkout the official[MSDN](https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/device-nodes-and-device-stacks)documentation\)\.
Retrieving data from the majority of I/O endpoints is achieved through a documented API provided by the system which is as you might be guessing the same***kernelbase\!ReadFile\( \)***we mentioned before; there is also an async version named***ReadFileEx\( \)***that relies on an***APC***\(async procedure call\) to notify the caller that the read operation has terminated\. Each mentioned I/O endpoint is presented to user mode callers as file objects represented by the kernel by a predefined well documented data structure named**nt\!\_FILE\_OBJECT**\. So, it doesn't matter whether it's a device object, a named pipe or a mailslot; for user mode callers, it's a file object\.
Handling I/O requests is a task performed by the kernel executive layer which requires many steps and sub\-tasks to be completed\. Since the executive layer is divided into many managers; handling I/O operations is delegated to a separate manager to deal with it which is called the Input/Output Manager or for short the I/O manager\. This manager knows exactly how to differentiate between sync/async operations, the target to which the request should be sent, what drivers are involved, whether it is possible to service request following the Fast I/O path and many other aspects that help it completes the request properly\. So, as you see handling I/O operation is not a direct simple task as you might be guessing but it's the main task that any operating system should well implement to function properly\.
In this article\. I am going to explain the different steps the I/O manager follows when initializing an I/O operation starting from determining the right target, choosing the right path and finally sending the request to be processed by the right drivers\. To make it easier I choose to focus on read requests only since the major steps are common by all types of requests\.
## **Determining the request Target:**
***nt\!NtReadFile\( \)***is the kernel counterpart of***ntdll\!NtReadFile\( \)***that is called internally by***kernelbase\!ReaFile\( \)***which is just a wrapper that internally forwards the rest of the work to the native undocumented layer\. This routine takes the target of the read request as a***HANDLE***since its forbidden for user mode callers to access objects directly through their pointers\. The first thing to do is getting the corresponding object to which the caller provided***hFile***parameter refers to by passing it to the object manager's defined routine***nt\!ObReferenceObjectByHandleWithTag\( \)***that performs the lookup operation in the current process handle table and get the corresponding***nt\!\_HANDLE\_TABLE\_ENTRY***that holds the starting memory address of the object main header***nt\!\_OBJECT\_HEADER***which is sufficient to get the target object pointer which is of type***nt\!\_FILE\_OBJECT***since***nt\!NtReadFile\( \)***operates on I/O endpoints\. Only***FILE\_READ\_ACCESS***is requested in this step since the caller wants to read data from the provided target\. For more information about how the handle table lookup is done checkout my previous article[here](https://winware31.blogspot.com/2026/05/deep-dive-into-object-creation-flow-in_01568126117.html)\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjarGu9pqnp6OGHOTzmnJ286wOcoj6f1pi7Wg7dJNiYUgDcKkpYMJ1kce172Glb8tWmgPcLALFHhKQECHmIgNHDBldplBi_R6v-3lLvCX-EAwbpe42gvoGB7MC5vFGZG_PVUHIlMSuc50AxlGuQ0G-ZCAqa9t1aRjEOlZVWkl3Gfj81M50A3DJKIXCEdRMJ/s1150/target_0.jpg)
Getting the***nt\!\_FILE\_OBJECT***pointer that corresponds to the***hFile***parameter provided by the caller is not enough to properly handle the read request\. File objects aren't the real targets of I/O operations; they are just open instances to these real targets that are called device objects and represented by the kernel using the well documented data structure***nt\!\_DEVICE\_OBJECT***\. a single file object can represent an open instance of its related device object itself or to some sub item in its private namespace; however, any request sent to either the device object itself or to any of its sub items share the same target from the kernel perspective which is the device object\. To get the device object to which the target file object retrieved in the previous is related the I/O Manager provides***nt\!IoGetRelatedDeviceObject\( \)***\. Determining the right device object to use is not straightforward as you might think because there is not only device object associated with a file object\. As its declaration says, the***nt\!\_FILE\_OBJECT***structure has a member named***DeviceObject***which is a direct pointer to a***nt\!\_DEVICE\_OBJECT***structure; it has also a member named***Vpb***\(Volume parameter block\) which is pointer to a kernel data structure named
***nt\!\_VPB***which is used only if the target file object is located in a mounted volume \(it's filesystem file or directory\)\. In such cases, the***DeviceObject***member of the file object points to the storage stack; to find the real target of its I/O requests which is the volume device object, you must get it from the***DeviceObject***member of the associated
***Vpb***\. The direct***DeviceObject***pointer found in the target file object corresponding***nt\!\_FILE\_OBJECT***is used as the I/O target only if the following two conditions are true\. First, the***Vpb***member or its
***DeviceObject***member is***NULL***\. Second, if the target file object corresponding***nt\!\_FILE\_OBJECT::Flags***member has the flag***FO\_DIRECT\_DEVICE\_OPEN***set but either the
***Vpb***member or its***DeviceObject***member is***NULL***\(in this case I am talking about the members of the***DeviceObject***member of the target file object corresponding***nt\!\_FILE\_OBJECT***not the members of the file object itself\)\. One last point to remember is that The***Vpb***associated with the file object has a priority over the one associated with the***DeviceObject***of the file object\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgXinCxKmZGIAc12MmDicr1UfHs3-ErElTrIMhZfqUKZNHAjVL-D5A5a6F36llyl82nLQ62gSYpgfhgPeFF8ffTG6ywPyQSOpXLR5I0Uc6tZiYA7-49KHAAuFDjnsR4ybWBMYJ51H4MrkyJFd3w250TjN_9did2c3oprsqGpJphvN7Ig39o0_ZnMUuvEZ8O/s1170/target_1.jpg)
After determining which device object to use as the final target of the read request, we must walk its stack to get the top device object in order not to break the I/O Manager rule that says that I/O requests must be sent to the top of device stacks not to the middle of it\. Each device object corresponding***nt\!\_DEVICE\_OBJECT***structure has a member named***AttachedDevice***that represents the device object immediately above the current one in the stack\. Using this member, we can easily get the top device object in the stack involved in handling I/O requests for the target file object\.
## **Validating User\-Mode Parameters:**
As any other system service routine \(kernel counterparts of***syscall***stubs implemented in***NTDLL\.dll***\),***nt\!NtReadFile\( \)***must validate the parameters the caller has provided if the request was issued by a user mode thread\. To determine whether the request source is a user thread or not,***nt\!KeGetCurrentThread\( \)***is called to retrieve the***nt\!\_ETHREAD***that corresponds to the current thread \(the caller that issued the current read request\) then its***PreviousMode***member is checked; the value***0x1***is reserved for user mode\. The***IoStatusBlock***parameter is the first one to validate; this parameter represents the output buffer where the final status of the read request is going to be written so this is why***nt\!NtReadFile\( \)***must ensure that both its two members Status and Information must be writeable\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWILY2rFUajcrAtE3hMVQFTJowyfhZpzoG-5-HcdW3ru0PJxxNXzWo5E635VRVNa7i5VhCAlJXfQUz9SDwVfw_NU7tfPu2T6MbSRc9cF5esfznw0178csAmfYZQg3BldKA80c7_p4FyRFvGANra9LJqZq8FqcznrQyvRNiaKCxOzzDIGNr0tKYpRu_uPnx/s1209/validate_user_parameters_0.jpg)
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjROj43dtWkZ5M-kX84zbBz7pTaHeiNmE1pUJJn7aqmUjD1JDGHddSFs_yxd1n8CNdZr_-hf97_DvHxzTszJL1t-p_tAWxkEEfGaNpTUVgpbp4CUG7GqTGaQtDtuUUArOieroyqXNKbRoS35laRSpYPHSD6a_ckoDDRnb0RZ6JgJFJHFbreoH2_Iweljne1/s1111/validate_user_parameters_3.jpg)
The next step is validating the memory range provided by the caller as the location where the read data must be written at the end\. This range is delimited by two parameters provided by the caller too which are named***Buffer***and***Length***respectively\. The entire must reside in the user address space and must be writable\. To ensure writability, the system uses a smart trick; since page protection flags are set in a page\-by\-page basis and it's impossible to see a single page with more than one protection attribute; the system just checks whether the first byte of each page covered by the range is writable or not\. One last point to explain is that the***Length***parameter cannot exceed certain limit in order not to cause an overflow in the result of the sum between it and the***Buffer***parameter to determine the last address in the range\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVViZqQNtnj-2UzZvgtWbjl911Anwty7h1LwmtTIw99EH95GR4JbSRjUZNbRKiq_NYSxzrVE-R8zvEkk1x_JWuN5YL8KgRMU9GydoGI5bioPpWKLXz4PdD5FYNHsLKRZkW05jTyTnADBYhMqrcRidsD0w54GjQGsPfT9LWnHG6YknPphrWU1IbQQYMbPAc/s1209/validate_user_parameters_1.jpg)
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiqa54LTh9s2nVBNJ7CQVWHhvTuX5PU42XS53paVbWnUS_dUfVUsDfiGyQD5NhHcH3F-LDDrdzQhK3ardk9q7mDaC9cG9OPSm2tOiv5U98-KdQuqadzGkcgcWleOLafMCCpthTze_20hafFhFV-NY249WflHSMnWIpj8dCKh_7oxAN5yduXeJE1DFvDMyGe/s1254/validate_user_parameters_2.jpg)
After verifying that output parameters are valid user mode sub ranges and ensuring that all of them are writable, the next parameter that needs some special care is***ApcRoutine***that represents a user***APC***that is used as the completion mechanism when the read is to be done asynchronously\. If the calling thread \(the one that has issued the read request\) is currently running in the context of a 32bit process \(a***WOW64***process\) the***ApcRoutine***is encoded by setting its first bit to 1\. Next,***nt\!NtReadFile\( \)***ensures that the target file object is not associated with an I/O completion port before moving forward; depending on its behavior when the caller passes a non\-NULL***ApcRoutine***parameter and the target file object is associated with an I/O completion port, I can say that file I/O endpoints cannot rely on two asynchronous completion mechanisms at the same time\. For more information about the internals of WOW64 checkout my previous article[here](https://winware31.blogspot.com/2026/06/wow64-implementation-details.html)\.[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-hpIb7XtXNksJlEAVODIcmet2tREsUIPMS-Cc96QJ14-aXF6ZhqcJ17rClm26RO41e6Zv4H6HwvQCiU-DoAx6H9wbE8DkRWcRz9mDm_2R5KC1Y1Oy3olWj2K6xiAO7yFoe6b-9r2XgN9rDsirPcXUbBisopabScEO0tHHGFTdQdZXLoqr6zOX4WtArL5z/s1101/validate_user_parameters_4.jpg)
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiHUMCV-1yfiR1FJuPtWjG_SyPMbjVIMCYVkPDE1yZsuhWFxu2FpgYJ4vjK-YIIa8jC9kUj7K9AsURfOJnqZnx9Z32zgefAG44o78qSUw91LLAd5L9Y6nOqrMyt2ABLdeH6YkwX1yPAvfG273sn6tGaIGr8Og_DTNt0eL-ckT8dHP-SAxVBg950DrboA2Ga/s1150/validate_user_parameters_5.jpg)
***NOTE:***the***ApcRoutine***parameter is only encoded if the target file object was opened for asynchronous I/O operations \(the
***FO\_SYNCHRONOUS\_IO***flag is not set in its corresponding***nt\!\_FILE\_OBJECT::Flags***member\)\. Otherwise, the***ApcRoutine***is not used by the I/O manager since the current thread is going to be put in a wait state until the read ends\.
Now, it's time to validate the***alignment***for various parameters\. The first thing to ensure is that***ByteOffset***which is a pointer to a***LARGE\_INTEGER***must be***4***bytes aligned; otherwise, a data misalignment exception is raised\. Some device objects especially the ones related to the storage stack \(disks and volumes\) require some special alignment for the buffer used as the location where the read data should be written at the end, and also a defined I/O transfer ratio for callers to respect\. These limitations are only necessary when the target file object doesn't support intermediate buffering \(the***FO\_NO\_INTERMEDIATE\_BUFFERING***flag in set in its corresponding***nt\!\_FILE\_OBJECT::Flags***member\) so the request cannot be satisfied from the cache, and it will reach the storage stack\. There are three parameters that are affected by those limits; Buffer, Length and***ByteOffset***\.***Length***and***ByteOffset***must respect the***SectorSize***member of the target device object determined previously so the caller can only read full sectors and cannot start the read at the middle of some sector\. The Buffer member must be properly aligned as the***Alignment***member of the target device object says\. Before moving to the next point, I need to clarify something related to the alignment of the**ByteOffset**parameter; as I said, this parameter is a pointer the***4***byte alignment mentioned before concerns the pointer itself not the offset value while the second***SectorSize***alignment concerns the offset value not the pointer itself\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhjvIrwwCm5li0vHV5wmZXFGR2BAjAp9nsBTmdnF42CVVIf5IRKUxT7eDsP8DWRQYqWbxO6bpuvWFLPKjZ1xlPo7jGX-wa9HxmFdkv3G-n7ZfZT4jH2K11GGKQAkyQHdTGYTOUXZS4EH0MUgJTBj8o8H7W9TAhYAh4d6vYY8yvusKE5IV-MoqBcyZPueIG2/s1164/validate_user_parameters_6.jpg)
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgztyC4ahHHgYWuKb6XU76VGnxUA_kAbDpuWfKPVibiDWFROt8rsr65X3DR7WkUD_wIh6gPLJiEA21KTUM2QUPLq3UT43A21dEY9m7Hh4r5KGQ0FEP1rEUbAZQdNyENmcBhxvWhJVyx63BC3ngF7AYF_NbH5jfR3vZZch3hddQaoqPYX2MnyvuFVUnQRpIs/s979/validate_user_parameters_7.jpg)
Finally, at the end of this phase the***Key***parameter is checked to ensure that it's readable \(this parameter is a pointer too\)\. This parameter is not that important for handling read requests as it's not considered a main piece of data to complete this I/O operation\.
## **Where should the read start ?:**
Before sending the request to the driver layer to be processed, it's necessary to determine the byte where the system should start reading from the target file object\. This offset ranges from 0 \(the first byte in the target file object\) to the size of the target file object \(the word size here can mean different things depending on the exact type of the file object\)\. As I said in the previous section,
***nt\!NtReadFile\( \)***takes a pointer to a***LARGE\_INTEGER***named***ByteOffset***the caller can use it to force the I/O Manager to start the read at a specific hardcoded offset\. Unfortunately, the caller provided starting offset parameter is not directly used as it is; the I/O Manager performs additional checks beyond the ones described in the previous section to determine whether to continue or fail the request at this stage\. If the read request is meant to be processed asynchronously because the target file object was opened for asynchronous I/O \(the***FO\_SYNCHRONOUS\_IO***is not set in its corresponding***nt\!\_FILE\_OBJECT::Flags***member\), the provided starting offset is allowed to be
***NULL***only if the target is a named pipe or a mailslot; otherwise, the I/O Manager fails the request immediately returning
STATUS\_INVALID\_PARAMETER\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-yaOs0ftKW5Dy1UFFNXvpGdQWgHkve7pFBg0bAoop1bhZYFs4FA3OgGIuBsiq1dBdYZ9BPyryQ3FANXeDvBHG0txosvwcSd0j3aUzpMQMZ-VWoW90WUjX7H_63q33yOegyqo7hT7eyFg3dzu_jhPiwlx1fj46j0Q7pdvftbxNUqBaHVcO8a-bql8-w43H/s1167/start_position_0.jpg)
For synchronous reads \(the target file object has the***FO\_SYNCHRONOUS\_IO***flag set\), the system will use either the current position preserved by the I/O Manager per file object \(the***nt\!\_FILE\_OBJECT::CurrentByteOffset***member\) if and only if the caller provided***ByteOffset***parameter is***NULL***or its***LowPart***is set to the predefined value***FILE\_USE\_FILE\_POINTER\_POSITION***while its***HighPart***is set to
***0xFFFFFFFF***; or the***ByteOffset***as it is otherwise\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKRL6OxowkZfPKdcB-fTDfwrQScqgz6DpE8tB72o4CKAExNDEpLtFmX-Prl-38eIxqQu_KyUlTm81j1tFYBZOmkrFBDHp93FaFbTCT6w1MHl9LTvjOG25lacD5B4V0ewEVKStZqwniFEH3Rns76lziMfcEziH4nKEEZxPYt-sBRzy2siGMizuCeLl3-J5x/s1275/start_position_1.jpg)
***NOTE:***the last mentioned cases regarding***ByteOffset***works the same way when the target file object was opened for asynchronous I/O and it's either a named pipe or a mailslot \(Named pipes and mailslots are beyond the scope of this article\)\.
## **Synchronizing requests \(Sync I/O handling\):**
As I said in the previous sections, the target file object may have been opened for either synchronous or asynchronous I/O depending on whether the***FO\_SYNCHRONOUS\_IO***flag is set in its corresponding***nt\!\_FILE\_OBJECT::Flags***structure\. For file objects opened for synchronous I/O, the system implements some sort of mutual exclusion between threads trying to perform an operation on the file object if it happened that two or more try it at the same exact time\. To ensure that synchronization between requests, the I/O Manager has reserved***3***among the data members of the***nt\!\_FILE\_OBJECT***structure\. The first one is a***ULONG***named Busy; as its name suggests, it determines whether there is an I/O operation that was requested before and it still being processed\. The I/O Manager reserves 2 distinct values for this member; the first one is***0x0***that indicates that the target file object is not currently busy because of an incomplete synchronous I/O request; the second one is obviously***0x1***that indicates the opposite of the previous one\. As a consequence of this design pattern, the I/O Manager must check the value of the Busy member object before moving on because the file object might be busy at that moment\. To prevent a race condition that might appear between the time of the check and the time of setting the Busy member to***0x1***, the I/O Manager relies on***nt\!\_InterlockedExchange\( \)***that atomically gets the current value of the Busy member and then sets it to***0x1***to defer other requests processing until the current one gets completed\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_2Pu1cErW7bD40zQQ3U-Ci_TPyyPlvKhNiEMoFSVPxbw3EzglegEIuYQekgIwFZpoBkM80Azx4sxNN5GaMyh0Em8K_j-p9wKxiAXR20M9dHDMJjHzaI55KZ_XD74Fij8pTag5tgYAd6sWLfFjc-iEZY_iTt_RPkU3SjaMd2NzxhFxYwvfGP8JCzlgcvw0/s1225/sync_0.png)
The next member that is also involved in synchronizing I/O requests for file objects opened for synchronous I/O only is a***ULONG***too named***Waiters***\. This member is used to track the real\-time number of pending I/O requests that are temporarily blocked because the target file object is currently busy \(the***Busy***member is set to***0x1***\)\. Whenever the I/O Manager decides to defer an I/O request targeted at a busy file object it increments the***Waiters***member by one for the new pended request\. Again, a race condition might happen if two or more requests arrived at the same time which may lead to the Waiters member being incremented by one not by two as it should be\. To prevent it, the I/O Manager relies on***nt\!\_InterlockedAdd\( \)***that adds one to the Waiters member in an atomic safe manner\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEimPuvEte6eM4Zzxfq8V2O03lB_ivBQdQh-9Bk90BaRjVL5w-13PEq_cKiS-sNF8x2OhQzCeBsCzGorDS30G8kxJ1lZTR-L79cGlRwYGEi-nCTh7azd3s8NwW_tdE2YcxdDLdElBybHAguhYYiX2YbjmlEeuh8vejwXrUfmfoEqiforZJ_R7bNjvrIQV19Z/s1117/sync_1.png)
The last and the most important member involved in this task is an event object named***Lock***\. It is the dispatcher object that is used to put threads that initiate synchronous I/O while the target file object is busy in a wait state\. The wait is performed multi times until the***Busy***member becomes***0x0***indicating that the file object is not busy anymore; so, the wait is not considered satisfied just because the***Lock***event object becomes signaled; the reason for that is because there is a possibility that some other thread may get access to the file object because the Busy member was***0x0***before the current one gets a chance to set the file object to the busy state\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQ-HL0PJJg54GYPCSjDxl78d39dlZvTt0EL8mo3REk1q_6ZAbdxPH5QDG0pXlbMsREX6YDXNXcDIk2bNmgGubQftBU2avdDg6BDnnTMGHS_RUrBnlLz9nF_aC0Q1eQ1u0coyGLORRKyUBPB8v9ZxA_uofKcWnVgZvUj_akNJ7krq6ughNj7SaJFFKKOMYD/s1167/sync_5.png)
If the target file object was opened for alertable I/O \(the***FILE\_SYNCHRONOUS\_IO\_ALERT***flag was set when it's opened using***nt\!NtCreateFile\( \)***\), the wait may terminate early due to an alert or to service a user APC \(***nt\!KeWaitForSingleObject\( \)***returned either***STATUS\_ALERT***or***STATUS\_USER\_APC***\) without truly giving ownership of the file object to the current thread which was waiting\. Depending on my reversing, the next step if the wait was interrupted is checking whether the file object becomes free \(its Busy member is switched to***0x0***\); if yes, the I/O Manager just sets the Lock event object to allow other waiters to proceed if there are any \(the***Waiters***member of the target file object is greater than***0x0***\)\. Skipping this final step may lead to a deadlock leaving all the other threads waiting for the file object waiting forever\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJ5NxQW0yPhW_nT_0AF6vIYugsHRB8jokqtXa6Se1JYqrQ2HE8AnWybFKR-C8OPfK3Bg81MDvbMZ_fjZgX5AIfxwEAo5cgiMEUwOqFkMPnCgaMZAixEPQVbGRm2o5lSrF6droWwPaK1NFiomK9_6cCwOihKh3M6MxkV3WYD4nXhGXMbO7T_Y4iFvRvJqTp/s1165/sync_3.png)
If otherwise, the wait was satisfied and the***Lock***event object becomes signaled and at the same time no other thread gets access before the current one, the ownership of the file object is given to it, the Waiters member is decremented by one indicating that one thread is removed from the wait state and gets exclusive use of the file object\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsWXjiKmv2Ag94fDUYYH8TT0CkbDgvROrLXXb6mNCtDqy4s4208fEkPbNheCgje6JDirsuUfeuS1lPpf1_T6vk4_OG9zhC3VGWn2Nyc5I3-PRlExH8c8KWIHxeCLMVz0A4q2ydwd6fuZYlibzBVw_DNdHVg9-nYEL6sjoYQfd2QwORa_X-yyJe4VQ0THpw/s1200/sync_2.png)
***NOTE:***This whole synchronization mechanism is totally implemented in one I/O Manager internal routine named***nt\!***
***IopAcquireFileObjectLock\( \)***that has the following signature \(the output parameter named a4 is a pointer to a***BOOLEAN***that is set to***TRUE***only if the wait was interrupted by an alert or to deliver a user***APC***\)\. Otherwise, it remains
***FALSE***\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhjKHSd-gjTIKxEXq4Enkh6kjasnBM-CjQp17GBNDCY12i_qHU3DMW91nA7Ys2lMrFdzpUBhNbgGcSVQLBeuOIXS9wOxqk1bFWqdjI5ffMkZk93i-59wkyX-6R3k4jbKvg_xBm50OyNgacMuMKmqBJ2ZU5VDujUkWbaNGmOtBYZQylWR7NsvQ12lQUTghAi/s478/sync_4.png)
## **The Fast I/O path:**
For synchronous read requests only \(the target file object was opened for synchronous I/O and the***FO\_SYNCHRONOUS\_IO***flag is set in its corresponding***nt\!\_FILE\_OBJECT::Flags***\), the I/O Manager has two options to handle the request\. Each device object holds a pointer to the driver object that is responsible for handling all requests targeted to it in its corresponding***nt\!\_DEVICE\_OBJECT::DriverObject***member\. Some driver objects support a concept named Fast I/O which is a method defined by the I/O Manager used mainly by file system drivers \(it may be used by other types of drivers\) to either read or a write data to some target file while avoiding the overhead of sending the request down to the storage stack\. Fast I/O is implemented by the kernel as a function table data structure named***nt\!\_FAST\_IO\_DISPATCH***; this structure holds pointers to various driver defined routines each one having its own purpose and use\. To stay focusing on read requests only, I am not going to mention all the existing routines\. For read requests, the I/O Manager reserves a Fast I/O routine from the target driver's Fast I/O dispatch table named***FastIoRead\( \)***\. The implementation of the routine is left to the driver developer, though file system drivers rely on another executive manager named the Cache Manager for Fast I/O \(the cache manager is beyond the scope of this article\)\. One little point to mention about the Fast I/O path is that the I/O Manager determines whether to follow it or not by checking the driver associated with the target device object which is the highest one in the involved stack; if that one's associated driver doesn't support Fast I/O at all \(it's corresponding***nt\!\_DRIVER\_OBJECT::******FastIoDispatch***member is***NULL***\), the Fast I/O path is totally disabled for the entire stack\. So, be careful when deciding to attach a filter driver at the top of an existing stack, you must ensure whether your new driver should support Fast I/O or not\.
Even if the driver object responsible for handling the current read request for the target file object provided by the caller supports Fast I/O, the system doesn't immediately delegate the handling of the request to this driver's***FastIoRead\( \)***routine\. Despite this article is not meant to be a detailed explanation of the internals of the Cache Manager, I must mention some points related to it in this section since Fast I/O was added as a meant to speed up file system drivers work and these drivers rely on the Cache Manager for this path\. Each file object corresponding***nt\!\_FILE\_OBJECT***has two special members reserved for the Cache Manager; the first one is named***SharedCacheMap***and it's used by the cached manager to track the current memory locations where the file represented by the file object structure is currently cached \(this is the simplest way to explain the role of this member\); the second member is named***PrivateCacheMap***and it's used by the Cache Manager as helper for read ahead and write behind operations \(these two are beyond the scope of this article\)\. This***PrivateCacheMap***member has a second usefulness as it is checked by the I/O Manager to determine whether it is currently possible to follow the Fast I/O path or it should move directly to the traditional***IRP***path \(explained in the next section\)\. Having a***NULL******PrivateCacheMap***member directs the I/O Manager to the slow IRP path directly\. Otherwise, the corresponding driver's***FastIoRead\( \)***is invoked immediately to try performing the read itself\. If theFastIoRead\( \)returned FALSE \(it failed without even starting the read\) or the read request was terminated but with an error status code different than***STATUS\_END\_OF\_FILE***and***STATUS\_BUFFER\_OVERFLOW***, the I/O Manager doesn't return to the caller directly, it moves to the second option which is trying the read following the long way \(building anIRP, filling it in, then send it to the target device stack for processing\)\. Otherwise, the request is considered erroneous since those two status codes indicate that there is a problem regarding some parameters, either the Buffer provided by the caller was too small or the request number of bytes is larger than the target current size; in both cases, it is a huge waste of processor cycles and memory space to retry the read the other way without first adjusting the parameters\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLU6UlEG_MaoECFnT9Ve7HYMzy0DIWDSAPgsEsB4u3B7kViLppOdBlAjZ6qvuu9usPrNG6zrRZAR3AIK1mq0ovs_oOOfDbPUvQHMUgfELfKqug5dB7roWb0ZxG-TbvrWTcUuL8nkb8avn9ImuHfJyE03i9yc9oLzBVpPHcteGcOmUZ1WgqGGIchTuOdDUJ/s1125/fast_io_0.jpg)
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEibmBXY5glVvElNi8r3UWDBfwimJvqen7kI_aXD5w_dOUmNOAq5Y-8NN1DD4b3DfUkcEVOvrbp9sGrM-qJLKcsK45iZbBGj_K6B9yxzkb8sopBGFx3XF2wGhHIaNPXOtx3DPkyoW0YckZo-QP_r6dSdnmkgUTZ_YxYcG2mwCQfPyvoBfoW8vYc7udqZASZV/s1123/fast_io_1.jpg)
***NOTE:***the Fast I/O is totally bypassed if the target file object was opened for asynchronous I/O \(the***FO\_SYNCHRONOUS***is not set in its corresponding
***nt\!\_FILE\_OBJECT::Flags***member\) no matter whether its***PrivateCacheMap***member is***NULL***or not\.
## **Building the I/O request Packet:**
Before diving into how the I/O Manager allocates and fills in the request packet for the current read request, let me briefly describe what I/O request packets \(IRPs for short\) are and how they are filled in and used\. an I/O request packet is data structure holding all the necessary pieces of information that let the I/O Manager and the underlying drivers identify and process the request correctly; it is represented by the kernel by a predefined well documented data structure named***nt\!\_IRP***\. Each IRP is divided into two parts, the fixed header part which is the***nt\!\_IRP***structure followed by one or I/O stack locations\. I/O stack locations are also kernel data structures defined as***nt\!\_IO\_STACK\_LOCATION***, they hold the request parameters except the output buffer \(if it's an output request\) which is preserved in the***IRP***header itself\. The I/O Manager sets two strict rules regarding I/O stack locations to ensure the proper processing of I/O requests; the first one is that each driver in the target stack
should have an associated I/O stack location so it can access the request parameters; the second rule directs each driver in the stack except the last one to fill in the stack location of next driver below it the stack\. below is the declaration of the***nt\!\_IRP***structure, the most important members are highlighted\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgReWcC54zdXIFPvYbVpHzrF1t14uv10mq5IuFXiWXstlQFlcnfu1CGdt-tWMsJRIP6zUZ04CGFjxBInQ6OIS5Qxa3iT084u1Qkd1Bv4sFE3LWEIPW9VdagzKGSbHVWvI3GWE9Ml_Ssdr_Sqz2FiogGL3c-asa8M7CK1n_1EP8H1eYAmb_aaZbiCgC9kWeJ/s1479/irp_0.png)
Now, let's dive into how exactly the IRP is filled in\. The first step is obviously allocating a kernel non\- paged pool memory buffer large enough to hold the new IRP's fixed header part and all of its stack locations\. To know the exact number of stack locations the I/O Manager should associate with new IRP, the target device object \(the top in the stack\) corresponding***nt\!\_DEVICE\_OBJECT::StackSize***member is consulted\. This member holds the number of device objects attached to the target stack\. Since the header part has a fixed size, the only information needed before allocating the IRP is the number of stack locations which is then passed to***nt\!IoAllocateIrp\( \)***that allocates a new uninitialized IRP \(both the header and the stack locations left empty\)\. If the allocation was successful, the next step is filling in the new IRP's members starting by its***nt\!\_IRP::Tail::Overlay***union member that contains two major pieces of information; the first one is named***OriginlaFileObject***and it's set to the target file object corresponding***nt\!\_FILE\_OBJECT***pointer; while the second preserves the initiating thread corresponding***nt\!\_ETHREAD***pointer \(retrieved using***nt\!KeGetCurrentThread\( \)***\)\. The request origin that indicates whether it was initiated by a user thread or a kernel one is saved in the***RequestorMode***member of the***nt\!\_IRP***structure\. the next member to fill in named***UserEvent***and it's set to the caller provided***hEvent***parameter corresponding***nt\!\_KEVENT***structure that is retrieved using***nt\!ObReferenceObjectByHandleWithTag\( \)***\.The***IoStatusBlock***output parameter is used as the***UserIoSb***member of the new IRP\. The***ApcRoutine***and***ApcContext***parameters are used to fill the***Overlay::AsynchronousParameters***member\. All of Those members are set the same way no matter what category the intended request fell into\. The next step the I/O Manager jumps to is filling the stack location reserved for the target device object \(the top one in the target stack\); Since it's named a device stack, the system traverses it backward starting from the last added entry to the oldest one\. Because of that, the stack location that corresponds to the first device in the stack is the last one \(in terms of order relative to the start address of the allocated IRP structure\); each IRP has a member named***CurrentStackLocation***that is used to track the start memory address of the stack location reserved for the current device object owning the IRP\. This member is first set to the end of the allocated IRP buffer \(the address after the end of the last stack location\) and it gets decremented so it moves from location to another while IRP is moving to lower levels\. After explaining those points about the***CurrentStackLocation***member and how it is used and what initial value it gets, it becomes obvious where this***\-1***comes from in the decompiled code\. Initially only two members ofthe current stack location are filled in are***FileObject***which is the same as the IRP's***OriginalFileObject***member as both are set to the target file object corresponding***nt\!\_FILE\_OBEJCT***pointer; the second member if the***MajorFunction***that identifies the request type and it's set to the predefined macro***IRP\_MJ\_READ***reserved for read requests\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5ExToyiKoqFfIxOZW0KiJupHf2nk2YJwofdofLhutVcpXGilUj79TyeSbTBj7UhwJJkfbnAHJ-9Q-LXAqNYGvRq7KjIIBD3UX9UtNMxQyswreomRgbxT91ZSPi0vynEl67b4YsmuKpKUILeh99xl_8O1GaD-AvIBUoaRKjWpo37cmcJEUv36VwS0n6BeV/s1087/irp_params_0.jpg)
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg66YzoZXLBL1NagYE4mDgtcs4DKF3jtHKNxn4LrfHVphYx8MiUUuocVphp5D5pllkbSVZOmdUIXQWr1pn78ijLAXUE-nYG4TRYv7YSzR89LjAKxTHfvI529wO8sHWAJ-lIBL6Lr7B5qr74f0AFcpShBqs6NjGzBIU_ThA4XaBW-iIMffxUoIKyu1nTyl3U/s1084/irp_params_1.jpg)
The next step is setting up the output buffer for the read request which depends on what transfer method the target device object supports\. To determine which method to use, the I/O Manager checks the Flags member of the target device object corresponding*nt\!\_DEVICE\_OBJECT::Flags*member; the first possible method is the buffered one in which the system allocates a non\-paged system buffer equal in size to the caller provided output buffer \(its size is in the***Length***parameter\), then it saves this buffer in the new IRP's corresponding***nt\!\_IRP::AssociatedIrp::SystemBuffer***member which is used by the underlying drivers as the output buffer into which they should write data\. The Buffer parameter provided by the caller is not ignored, the I/O Manager saves it in the IRP's corresponding***UserBuffer***member \(when the IRP gets completed, the I/O Manager will copy the data from the system buffer to the caller provided one in the context of the initiating thread\)\. The last step is setting some flags in the IRP's***Flags***member; the first one is***IRP\_INPUT\_OPERATION***which is self\-explanatory,***IRP\_DEALLOCATE\_BUFFER***is also set to instruct the I/O Manager to deallocate the system buffer that it previously allocated after copying the data from it to the caller provided buffer \(letting this buffer will cause a non\-paged memory leak that may degrade system performance\), and finally***IRP\_BUFFERED\_IO***is set which is also self\-explanatory \(the target to which this IRP is going to be sent expects to find the output buffer at the corresponding***nt\!\_IRP::AssociatedIrp::SystemBuffer***\)\.One final point to mention is that the previous steps \(allocating a system buffer, saving it in the IRP and so on\) are only required if the caller provided Length parameter is greater than 0; otherwise, there is no need to do any of them and the system only sets the***IRP\_INPUT\_OPERATION***and***IRP\_BUFFERED\_IO***flags in the IRP\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQgSHu4g3QlHioXpyqTDRGLdpoOijhFC32irl7TzfH_Rci8-72qcjxDp66JBaoDATfvKKQ9QePOu_fFxYVnTRSqVqx5xBWgz_Pvcunp2ErohqyYq49bQw3_OqjvT6438lloOj50as2upwdc1zEVreYqyU963Jqa3zhen4ZrLRGk_7PS-ZW3Z2J_ikz53L7/s1087/buffered_io_0.png)
The next possible transfer method is the direct one in which the caller provided output range which is delimited by the Buffer and the Length parameters is locked in memory \(it can no longer be paged out until unlocked\) and the I/O Manager builds a memory descriptor list \(***nt\!\_MDL***\) that will represent the range physical memory layout \(MDLs are beyond the scope of this article, for more information checkout the official[MSDN](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-mdls)documentation\)\. To allocate a new MDL and initialize it***nt\!NtReadFile\( \)***calls an I/O Manager defined and well documented routine named***nt\!IoAllocateMdl\( \)***; this routine takes the caller provided Buffer parameter \(the range start address\), the caller provided Length parameter \(the range size in bytes\) and an optional***IRP***parameter to associate the new***MDL***with \(each IRP has a member named***MdlChain***that is used by the system to chain multi MDLs in a linked list and this is how the new one gets attached to the current IRP, it can either be inserted at the tail or the head of this list\)\.***nt\!IoAllocateMdl\( \)***only initializes the members of the***nt\!\_MDL***structure, but it doesn't lock the provided range itself\. To lock the range and construct the***PFN***\(Page frame number\) array that immediately follows the***nt\!\_MDL***structure in memory and that stores the physical page numbers that correspond to each virtual page included in the range that represents the output buffer, the system calls***nt\!MmProbeAndLockPages\( \)***passing the***MDL***itself and***IoWriteAccess***as the desired access modifier \(***IoWriteAccess***is used because the locked range needs to be writable so the underlying drivers can write output data to it\)\. One point to mention is that as the previous transfer method, if the Length parameter is 0 there is no need to allocate an***MDL***and lock it\.[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJUdaEsc7wXKBV9CGOep-pVNWIpxkjtTSc3xNHY8zYk-CByJ6u692lcrMXQCl4eVZCHcNG5f7ue924OYsCu7kNrodP5AyFVaLPNYBbl4yDnvnqEXbnnPcpCexrzhDvpf2p2Xx-GKaJ6pEIsNhunA18HYuq7-WYevqApv75FJ61IaoNmSY0zV_uobtpu0Oj/s1171/direct_io_0.jpg)
If the target device object doesn't support none of the previous two methods, the caller provided Buffer parameter is used as it is; the system sets it as the new IRP's corresponding***nt\!\_IRP::UserBuffer***member\. This method is not safe until the read request is guaranteed to be processed synchronously and no driver in the target stack pends it; otherwise, accessing the caller provided output buffer in the context of an arbitrary thread may lead to serious memory errors\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXv-EpuphlPYZbHrIsG8QNoMKEHSt2vzaoBYJA-0zCsf59E592JbibV9K7Xbr5ets8sNtTfmRvqWqf9zPfjfC8Il1Hvyp7tCLnaBSL5sdmTfF40bqFAO8o2gIogdcnBpSUQkzt84GgnaYCneL1KIIIA93cUGSv98L0Yqpg588Lcuo_LM14ekxsRnlbqqJX/s1092/neither_io_0.png)
One final step before moving to filling\-in the rest parameters in the first stack location, the system sets the***IRP\_NOCACHE***flag in the IRP's Flags member if the target file object doesn't support intermediate buffering as indicated by its corresponding Flags member \(the***FO\_NO\_INTERMEDIATE\_BUFFERING***flags is set\)\.***IRP\_INPUT\_OPERATION***is set also for the last two described transfer methods and depending on the pseudo code another flag I***RP\_DEFER\_IO\_COMPLETION***is also set for all three methods\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiI6TKjEh6szXKQ2ni5dOfVAYP5KgXKz7P62ob7diN6oKjaUOSOBi0LN3y9eVvWoJOCTaw-lj0jNq9daWFAsGdyL5vw5iXpGAKmkhcSiQmPofjXmqqKQQb8emoVpWwdotOoqeoTTFcCqyWZRIXbYW61GK69a1cGeYOA6pUnmWiNKAm7GKYho3Kp3QV9Rtzl/s1081/irp_flags_0.png)
At the end, the Length, key and starting offset parameters are also saved in the stack location reserved for the first device object in the target stack, then the IRP is passed with other required parameters such as the target device object and file object to an internal routine named***nt\!IopSynchronousServiceTail\( \)***that eventually calls the documented***nt\!IoCallDriver\( \)***that passes the IRP to the***DispatchRead\( \)***associated with the first driver in the stack\.
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjr-Zi5tArzU4jVD3fZSRcF_x6CvCri7nXAXJb1IjcMrk4EzJy80LxL9SfdIkvQ-xPqcHl7sxa9hA0CXp6hHxP-tno_7OeIYnt5NcfRZxs3KmCpipnn2zqecJUIa4-CkQOSuBD-oUZnWVaNiXg9-UQvx4CM4FERxUVpK7oOqYUi58v3pT5IUjHMzLk0KWS5/s1149/stack_loc_0.png)
## **Conclusion:**
That is all for now, I hope you have learned something\. Despite being semi\-documented, there are no example pseudo codes demonstrating how the initialization is done internally; I hope reading this article gave you an idea of how the I/O Manager treats I/O requests\.
Stay tuned, other posts about various topics will be published from time to time\.
If you find my posts interesting, don't forget to follow me on[LinkedIn](https://www.linkedin.com/in/omar-mahmoud-b6887a24b)and[GitHub](https://github.com/WinWare19)\.