If you look at the various types of requests that come to a device, most of them involve reading or writing data. On occasion, however, an application needs to perform an IOCTL operation on a device. An application uses the standard Microsoft Win32 API function DeviceIoControl to perform such an operation. On the driver side, an application's call to DeviceIoControl turns into an IRP with the major function code IRP_MJ_DEVICE_CONTROL.
The user-mode DeviceIoControl API has the following prototype:
result = DeviceIoControl(Handle, Code, InputData, InputLength, OutputData, OutputLength, &Feedback, &Overlapped); |
Handle (HANDLE) is an open handle open to the device. You obtain this handle by calling CreateFile in the following manner:
Handle = CreateFile("\\\\.\\IOCTL", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, flags, NULL); if (Handle == INVALID_HANDLE_VALUE) <error> ... CloseHandle(Handle); |
The flags argument to CreateFile is either FILE_FLAG_OVERLAPPED or zero to indicate whether or not you'll be performing asynchronous operations with this file handle. While you have the handle open, you can make calls to ReadFile, WriteFile, or DeviceIoControl. When you're done accessing the device, you should explicitly close the handle by calling CloseHandle. Bear in mind, though, that the operating system automatically closes any handles that are left open when your process terminates.
The Code (DWORD) argument to DeviceIoControl is a control code that indicates what control operation you want to perform. I'll discuss how you define these codes a bit further on (in "Defining I/O Control Codes"). The InputData (PVOID) and InputLength (DWORD) arguments describe a data area that you are sending to the device driver. (That is, this data is input from the perspective of the driver.) The OutputData (PVOID) and OutputLength (DWORD) arguments describe a data area that the driver can completely or partially fill with information that it wants to send back to you. (That is, this data is output from the perspective of the driver.) The driver will update the Feedback variable (a DWORD) to indicate how many bytes of output data it gave you back. Figure 9-8 illustrates the relationship of these buffers with the application and driver. The Overlapped (OVERLAPPED) structure is used to help control an asynchronous operation, which is the subject of the next section. If you specified FILE_FLAG_OVERLAPPED in the call to CreateFile, you must specify the OVERLAPPED structure pointer. If you didn't specify FILE_FLAG_OVERLAPPED, you might as well supply NULL for this last argument because the system is going to ignore it anyway.
Figure 9-8. Input and output buffers for DeviceIoControl.
Whether a particular control operation requires an input buffer or an output buffer depends on the function being performed. For example, an IOCTL that retrieves the driver's version number would probably require an output buffer only. An IOCTL that merely notifies the driver of some fact pertaining to the application would probably require only an input buffer. You can imagine still other operations that would require either both or neither of the input and output buffers—it all depends on what the control operation does.
The return value from DeviceIoControl is a Boolean value that indicates success (if TRUE) or failure (if FALSE). In a failure situation, the application can call GetLastError to find out why the call failed.
When you make a synchronous call to DeviceIoControl, the calling thread blocks until the control operation completes. For example:
HANDLE Handle = CreateFile("\\\\.\\IOCTL", ..., 0, NULL); DWORD version, junk; if (DeviceIoControl(Handle, IOCTL_GET_VERSION_BUFFERED, NULL, 0, &version, sizeof(version), &junk, NULL)) printf("IOCTL.SYS version %d.%2d\n", HIWORD(version), LOWORD(version)); else printf("Error %d in IOCTL_GET_VERSION_BUFFERED call\n", GetLastError()); |
Here, we open the device handle without the FILE_FLAG_OVERLAPPED flag. Our subsequent call to DeviceIoControl therefore doesn't return until the driver supplies the answer we're asking for.
When you make an asynchronous call to DeviceIoControl, the calling thread does not block immediately. Instead, it continues processing until it reaches the point where it requires the result of the control operation. At that point, it calls some API that will block the thread until the driver completes the operation. For example:
HANDLE Handle = CreateFile("\\\\.\\IOCTL", ..., FILE_FLAG_OVERLAPPED, NULL); DWORD version, junk; OVERLAPPED Overlapped; Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); DWORD code; if (DeviceIoControl(Handle, ..., &Overlapped)) code = 0; else code = GetLastError(); <continue processing> if (code == ERROR_IO_PENDING) { if (GetOverlappedResult(Handle, &Overlapped, &junk, TRUE)) code = 0; else code = GetLastError(); } CloseHandle(Overlapped.hEvent); if (code != 0) <error> |
Two major differences exist between this asynchronous example and the earlier synchronous example. First, we specify the FILE_FLAG_OVERLAPPED flag in the call to CreateFile. Second, the call to DeviceIoControl specifies the address of an OVERLAPPED structure, within which we've initialized the hEvent event handle to describe a manual reset event. (For more information about events and thread synchronization in general, see Jeffrey Richter's Programming Applications for Microsoft Windows, Fourth Edition [Microsoft Press, 1999].)
The asynchronous call to DeviceIoControl will have one of three results. First, it might return TRUE, meaning that the device driver's dispatch routine was able to complete the request right away. Second, it might return FALSE, and GetLastError might retrieve the special error code ERROR_IO_PENDING. This result indicates that the driver's dispatch routine returned STATUS_PENDING and will complete the control operation later. Note that ERROR_IO_PENDING isn't really an error—it's one of the two ways in which the system indicates that everything is proceeding normally. The third possible result from the asynchronous call to DeviceIoControl is a FALSE return value coupled with a GetLastError value other than ERROR_IO_PENDING. Such a result would be a real error.
At the point at which the application needs the result of the control operation, it calls one of the Win32 synchronization primitives, such as GetOverlappedResult, WaitForSingleObject, or the like. GetOverlappedResult, the synchronization primitive I use in this example, is especially convenient because it also retrieves the bytes-transferred feedback value and sets the GetLastError result to indicate the result of the I/O operation. Although you could call WaitForSingleObject or a related API—passing the Overlapped.hEvent event handle as an argument—you wouldn't be able to learn the results of the DeviceIoControl operation; you'd just learn that the operation had finished.
The Code argument to DeviceIoControl is a 32-bit numeric constant that you define using the CTL_CODE preprocessor macro that's part of both the DDK and the Platform SDK. Figure 9-9 illustrates the way in which the operating system partitions one of these 32-bit codes into subfields.
Figure 9-9. Fields in an I/O control code.
The fields have the following interpretation:
I want to clarify one point of possible confusion. When you create your driver, you're free to design a series of IOCTL operations that applications can use in talking to your driver. Although some other driver author might craft a set of IOCTL operations that uses exactly the same numeric values for control codes, the system will never be confused by the overlap because IOCTL codes are interpreted by only the driver to which they're addressed. Mind you, if you opened a handle to a device belonging to that hypothetical other driver and then tried to send what you thought was one of your own IOCTLs to it, confusion would definitely ensue.
Mechanically, your life and the life of application programmers who need to call your driver will be easier if you place all of your IOCTL definitions in a dedicated header file. In the samples on the companion disc, the projects each have a header named IOCTLS.H that contains these definitions. For example:
#ifndef CTL_CODE #pragma message ("CTL_CODE undefined. Include winioctl.h or wdm.h") #endif #define IOCTL_GET_VERSION_BUFFERED \ CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_GET_VERSION_DIRECT \ CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_OUT_DIRECT, FILE_ANY_ACCESS) #define IOCTL_GET_VERSION_NEITHER \ CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS) |
The reason for the message #pragma, by the way, is that I'm forever forgetting to include the header file (WINIOCTL.H) that defines CTL_CODE for user-mode programs, and I also tend to forget the name. Better a message that will tell me what I'm doing wrong than a few minutes grep'ing through the include directory, I always say.
Each user-mode call to DeviceIoControl causes the I/O Manager to create an IRP with the major function code IRP_MJ_DEVICE_CONTROL and to send that IRP to the driver dispatch routine at the top of the stack for the addressed device. The top stack location contains the parameters listed in Table 9-1. Filter drivers might interpret some private codes themselves but will—if correctly coded, that is—pass all others down the stack. A dispatch function that understands how to handle the IOCTL will reside somewhere in the driver stack—most likely in the function driver, in fact.
Table 9-1. Stack location parameters for IRP_MJ_DEVICE_CONTROL.
Parameters.DeviceIoControl field | Description |
---|---|
OutputBufferLength | Length of the output buffer—sixth argument to DeviceIoControl |
InputBufferLength | Length of the input buffer—fourth argument to DeviceIoControl |
IoControlCode | Control code—second argument to DeviceIoControl |
Type3InputBuffer | User-mode virtual address of input buffer for METHOD_NEITHER |
A skeletal dispatch function for control operations looks like this:
1 2 3 4 5 |
#pragma PAGEDCODE NTSTATUS DispatchControl(PDEVICE_OBJECT fdo, PIRP Irp) { PAGED_CODE(); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp); if (!NT_SUCCESS(status)) return CompleteRequest(Irp, status, 0); ULONG info = 0; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength; ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength; ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; switch (code) { ... default: status = STATUS_INVALID_DEVICE_REQUEST; break; } IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return CompleteRequest(Irp, status, info); } |
The way you handle each IOCTL depends on two factors. The first, and most important, of these is the actual purpose of the IOCTL in your scheme of things. (Duh.) The second factor, which is critically important to the mechanics of your code, is the method you selected for buffering user-mode data.
In Chapter 7, "Reading and Writing Data," I discussed how you work with a user-mode program sending you a buffer load of data for output to your device or filling a buffer with input from your device. As I indicated there, when it comes to read and write requests, you have to make up your mind at AddDevice time whether you're going to use the so-called buffered method or direct method (or neither of them) for accessing user-mode buffers in all read and write requests. Control requests also utilize one of these addressing methods, but they work a little differently. Rather than specify a global addressing method via device-object flags, you specify the addressing method for each IOCTL by means of the two low-order bits of the function code. Consequently, you can have some IOCTLs that use the buffered method, some that use a direct method, and some that use neither method. Moreover, the methods you pick for IOCTLs don't affect in any way how you address buffers for read and write IRPs.
You choose one or the other buffering method based on several factors. Most IOCTL operations transfer much less than a page worth of data in either direction and therefore use the METHOD_BUFFERED method. Operations that will transfer more than a page of data should use one of the direct methods. The names of the direct methods seem to oppose common sense: you use METHOD_IN_DIRECT if the application is sending data to the driver and METHOD_OUT_DIRECT if it's the other way around. If you know that you'll get control in the same thread context as the application—usually true for IOCTL operations because no filter driver above you should be pending these and calling you later in an arbitrary thread context—you could use METHOD_NEITHER and decide on the fly how to access user-mode data.
With METHOD_BUFFERED, the I/O Manager creates a kernel-mode copy buffer big enough for the larger of the user-mode input and output buffers. When your dispatch routine gets control, the user-mode input data is sitting in the copy buffer. Before completing the IRP, you fill the copy buffer with the output data you want to send back to the application. When you complete the IRP, you set the IoStatus.Information field equal to the number of output bytes you put into the copy buffer. The I/O Manager then copies that many bytes of data back to user mode and sets the feedback variable equal to that same count. Figure 9-10 illustrates these copy operations.
Figure 9-10. Buffer management with METHOD_BUFFERED.
Inside the driver, you access both buffers at the same address—namely, the AssociatedIrp.SystemBuffer pointer in the IRP. Once again, this is a kernel-mode virtual address that points to a copy of the input data. It obviously behooves you to finish processing the input data before you overwrite this buffer with output data. (I hardly need to tell you—it's the kind of mistake you'll make only once.)
Here's a simple example, drawn from the IOCTL sample program, of the code-specific handling for a METHOD_BUFFERED operation:
case IOCTL_GET_VERSION_BUFFERED: { if (cbout < sizeof(ULONG)) { status = STATUS_INVALID_BUFFER_SIZE; break; } PULONG pversion = (PULONG) Irp->AssociatedIrp.SystemBuffer; *pversion = 0x0004000A; info = sizeof(ULONG); break; } |
We first verify that we've been given an output buffer at least long enough to hold the doubleword we're going to store there. Then we use the SystemBuffer pointer to address the system copy buffer, into which we store the result of this simple operation. The info local variable ends up as the IoStatus.Information field when the surrounding dispatch routine completes this IRP. The I/O Manager copies that much data from the system copy buffer back to the user-mode buffer.
Both METHOD_IN_DIRECT and METHOD_OUT_DIRECT are handled the same way in the driver. They differ only in the access rights required for the user-mode buffer. METHOD_IN_DIRECT needs read access; METHOD_OUT_DIRECT needs read and write access. With both of these methods, the I/O Manager provides a kernel-mode copy buffer (at AssociatedIrp.SystemBuffer) for the input data and an MDL for the output data buffer. Refer to Chapter 7 for all the gory details about MDLs and to Figure 9-11 for an illustration of this method of managing the buffers.
Figure 9-11. Buffer management with METHOD_XXX_DIRECT.
Here's an example of a simple handler for a METHOD_XXX_DIRECT request:
case IOCTL_GET_VERSION_DIRECT: { if (cbout < sizeof(ULONG)) { status = STATUS_INVALID_BUFFER_SIZE; break; } PULONG pversion = (PULONG) MmGetSystemAddressForMdl(Irp->MdlAddress); *pversion = 0x0004000B; info = sizeof(ULONG); break; } |
The only substantive difference between this example and the previous one is the bold line. (I also altered the reported version number so that I could easily know I was invoking the correct IOCTL from the test program.) With either DIRECT-method request, we use the MDL pointed to by the MdlAddress field of the IRP to access the user-mode output buffer. You can do direct memory access (DMA) using this address. In this example, I just called MmGetSystemAddressForMdl to get a kernel-mode alias address pointing to the physical memory described by the MDL.
With METHOD_NEITHER, the I/O Manager doesn't try to translate the user-mode virtual addresses in any way. You get (in the Type3InputBuffer parameter in the stack location) the user-mode virtual address of the input buffer, and you get (in the UserBuffer field of the IRP) the user-mode virtual address of the output buffer. Neither address is of any use unless you know you're running in the same process context as the user-mode caller. If you do know you're in the right process context, you can just directly dereference the pointers:
case IOCTL_GET_VERSION_NEITHER: { if (cbout < sizeof(ULONG)) { status = STATUS_INVALID_BUFFER_SIZE; break; } PULONG pversion = (PULONG) Irp->UserBuffer; if (Irp->RequestorMode != KernelMode) { _ _try { ProbeForWrite(pversion, sizeof(ULONG), 1); *pversion = 0x0004000A; } _ _except(EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); break; } } else *pversion = 0x0004000A; info = sizeof(ULONG); break; } |
As shown in the previous code in boldface, the only real glitch here is that you want to make sure that it's OK to write into any buffer you get from an untrusted source. Refer to Chapter 3 ("Basic Programming Techniques") if you're rusty about structured exceptions. ProbeForWrite is a standard kernel-mode service routine for testing whether a given user-mode virtual address can be written. The second argument indicates the length of the data area you want to probe, and the third argument indicates the alignment you require for the data area. In this example, we want to be sure that we can access four bytes for writing, but we're willing to tolerate single-byte alignment for the data area itself. What ProbeForWrite (and its companion function ProbeForRead) actually tests is whether the given address range has the correct alignment and occupies the user-mode portion of the address space—it doesn't actually try to write to (or read from) the memory in question.
Conventional wisdom holds that you should never access user-mode memory directly in the way I just showed you for fear that some other thread in the same process might call VirtualFree to release memory in between the time of the ProbeFor Xxx call and the time you make the access. According to this conventional wisdom, you should therefore always create an MDL and call MmGetSystemAddressForMdl to obtain a safe virtual address. In fact, however, it's perfectly safe to directly access the user-mode pointer if three things are true: First, you must be running in the process context to which the buffer belongs. Second, you must have done a ProbeForXxx. Finally, you must perform the access within a structured exception frame. If any portion of the buffer happens to belong to non-existent pages at the time of the access, the memory manager will raise an exception instead of immediately bug-checking. Your exception handler will backstop the exception and prevent the system from crashing.
The system uses IRP_MJ_DEVICE_CONTROL to implement a DeviceIoControl call from user mode. Drivers sometimes need to talk to each other too, and they use the related IRP_MJ_INTERNAL_DEVICE_CONTROL to do so. A typical code sequence is as follows:
ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); KEVENT event; KeInitializeEvent(&event, NotificationEvent, FALSE); IO_STATUS_BLOCK iostatus; PIRP Irp = IoBuildDeviceIoControlRequest(IoControlCode, DeviceObject, pInBuffer, cbInBuffer, pOutBuffer, cbOutBuffer, TRUE, &event, &iostatus); if (IoCallDriver(DeviceObject, Irp) == STATUS_PENDING) KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL); |
Being at PASSIVE_LEVEL is a requirement for calling KeInitializeEvent and IoBuildDeviceIoControlRequest as well as for blocking on the event object as shown here.
The IoControlCode argument to IoBuildDeviceIoControlRequest is a control code expressing the operation you want the target device driver to perform. This code is the same kind of code as you use with regular control operations. DeviceObject is a pointer to the DEVICE_OBJECT whose driver will perform the indicated operation. The input and output buffer parameters serve the same purpose as their counterparts in a user-mode DeviceIoControl call. The seventh argument, which I specified as TRUE in this fragment, indicates that you're building an internal control operation. (You could say FALSE here to create an IRP_MJ_DEVICE_CONTROL instead.) I'll describe the purpose of the event and iostatus arguments in a bit.
IoBuildDeviceIoControlRequest builds an IRP and initializes the first stack location to describe the operation code and buffers you specify. It returns the IRP pointer to you so that you can do any additional initialization that might be required. In Chapter 11, for example, I'll show you how to use an internal control request to submit a URB to the USB bus driver. Part of that process involves setting a stack parameter field to point to the URB. You then call IoCallDriver to send the IRP to the target device. Whatever the return value, you wait on the event object you specified as the eighth argument to IoBuildDeviceIoControlRequest. The I/O Manager will set the event when the IRP finishes, and it will also fill in your iostatus structure with the ending status and information values. Finally, it will call IoFreeIrp to release the IRP. Consequently, you don't want to access the IRP pointer at all after you call IoCallDriver.
Since internal control operations require cooperation between two drivers, fewer rules about sending them exist than you'd guess from what I've just described. You don't have to use IoBuildDeviceIoControlRequest to create one of them, for example: you could just call IoAllocateIrp and perform your own initialization. Provided that the target driver isn't expecting to handle internal control operations solely at PASSIVE_LEVEL, you could also send one of these IRPs at DISPATCH_LEVEL, say from inside an I/O completion or deferred procedure call (DPC) routine. (Of course, you couldn't use IoBuildDeviceIoControlRequest in such a case, and you couldn't wait for the IRP to finish. But you could send it because IoAllocateIrp and IoCallDriver can run at DISPATCH_LEVEL or below.) You don't even have to use the I/O stack parameter fields exactly like you would for a regular IOCTL. In fact, calls to the USB bus driver use the field that would ordinarily be the output buffer length to hold the URB pointer. So, if you're designing an internal control protocol for two of your own drivers, just think of IRP_MJ_INTERNAL_DEVICE_CONTROL as being an envelope for whatever kind of message you want to send.
It's not a good idea to use the same dispatch routine for internal and external control operations, by the way, at least not without checking the major function code of the IRP. Here's an example of why not. Suppose that your driver has an external control interface that allows an application to query the version number of your driver and an internal control interface that allows a trusted kernel-mode caller to determine some vital secret that you don't want to share with user-mode programs. Then suppose that you use one routine to handle both interfaces, as in this example:
NTSTATUS DriverEntry(...) { DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchControl; DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] = DispatchControl; ... } NTSTATUS DispatchControl(...) { ... switch (code) { case IOCTL_GET_VERSION: ... case IOCTL_INTERNAL_GET_SECRET: ... // exposed for user-mode calls } } |
If an application is able to somehow determine the numeric value of IOCTL_INTERNAL_GET_SECRET, it can issue a regular DeviceIoControl call and bypass the intended security on that function.
One extremely important use of IOCTL operations is to give a WDM driver a way to notify an application that an interesting event has occurred. To motivate this discussion, suppose you had an application that needed to work closely with your driver in such a way that whenever a certain kind of hardware event occurred your driver would alert the application so that it could take some sort of user-visible action. For example, a button press on a medical instrument might trigger an application to begin collecting and displaying data. Whereas Windows provides a couple of ways for a driver to signal an application in this kind of situation—namely, asynchronous procedure calls or posted window messages—those methods don't work in Windows 2000 because the operating system lacks the necessary infrastructure to make them work. A method that does work, though, is having the application issue an IOCTL operation that the driver completes when the interesting event, whatever it might be, occurs. Implementing this scheme requires excruciating care on the driver side, so I'll explain the mechanics in detail.
The central idea in this section is that when the application wants to receive event notifications from the driver, it calls DeviceIoControl:
HANDLE hDevice = CreateFile("\\\\.\\<driver-name>", ...); BOOL okay = DeviceIoControl(hDevice, IOCTL_WAIT_NOTIFY, ...); |
(IOCTL_WAIT_NOTIFY, by the way, is the control code I used in the NOTIFY sample on the companion disc.)
The driver will pend this IOCTL and complete it later. If other considerations didn't intrude, the code in the driver might be as simple as this:
NTSTATUS DispatchControl(...) { ... switch (code) { case IOCTL_WAIT_NOTIFY: pdx->NotifyIrp = Irp; IoMarkIrpPending(Irp); return STATUS_PENDING; ... } } VOID OnInterestingEvent(...) { ... CompleteRequest(pdx->NotifyIrp, STATUS_SUCCESS, 0); } |
The "other considerations" I just so conveniently tucked under the rug are, of course, all important in crafting a working driver. The originator of the IRP might decide to cancel it. The application might call CancelIo, or termination of the application thread might cause a kernel-mode component to call IoCancelIrp. In either case, we must provide a cancel routine so that the IRP gets completed. If power is removed from our device, or if our device is suddenly removed from the computer, we need to abort any outstanding IOCTL requests. In general, any number of IOCTLs might need to be aborted. Consequently, we'll need a linked list of them. Since multiple threads might be trying to access this linked list, we'll also need a spin lock so that we can access the list safely.
To simplify my own life, I wrote a set of helper routines for managing asynchronous IOCTLs. The two most important of these routines are named CacheControlRequest and UncacheControlRequest. They assume that you're willing to accept only one asynchronous IOCTL having a particular control code per device object and that you can, therefore, reserve a pointer cell in the device extension to point to the IRP that's currently outstanding. In NOTIFY, I call this pointer cell NotifyIrp. You accept the asynchronous IRP this way:
IoAcquireRemoveLock(...); switch (code) { case IOCTL_WAIT_NOTIFY: if (<parameters invalid in some way>) status = STATUS_INVALID_PARAMETER; else status = CacheControlRequest(pdx, Irp, &pdx->NotifyIrp); break; } IoReleaseRemoveLock(...); return status == STATUS_PENDING ? status : CompleteRequest(Irp, status, info); |
The important statement here is the call to CacheControlRequest, which registers this IRP in such a way that we'll be able to cancel it later, if necessary. It also records the address of this IRP in the NotifyIrp member of our device extension. We expect it to return STATUS_PENDING, in which case we avoid completing the IRP and simply return STATUS_PENDING to our caller.
NOTE
You could easily generalize the scheme I'm describing to permit an application to have an IRP of each type outstanding for each open handle. Instead of putting the current IRP pointers in your device extension, put them instead into a structure that you associate with the FILE_OBJECT that corresponds to the handle. You'll get a pointer to this FILE_OBJECT in the I/O stack location for IRP_MJ_CREATE, IRP_MJ_CLOSE, and, in fact, all other IRPs generated for the file handle. You can use either the FsContext or FsContext2 field of the file object for any purpose you choose.
Later, when whatever event the application is waiting for occurs, we execute code like this:
PIRP nfyirp = UncacheControlRequest(pdx, &pdx->NotifyIrp); if (nfyirp) { <do something> CompleteRequest(nfyirp, STATUS_SUCCESS, <info value>); } |
This logic retrieves the address of the pending IOCTL_WAIT_NOTIFY request, does something to provide data back to the application, and then completes the pending I/O request packet.
I hid a wealth of complications inside the CacheControlRequest and UncacheControlRequest functions. These two functions provide a thread-safe and multiprocessor-safe mechanism for keeping track of asynchronous IOCTL requests. They use a variation on the techniques we've discussed elsewhere in the book for safely queuing and dequeuing IRPs at times when someone else might be flitting about trying to cancel the IRP. There's a little bit of extra code to show you, though (refer to CONTROL.CPP in the NOTIFY sample on the companion disc):
1 2 3 4 5 6 7 8 9 10 |
typedef struct _DEVICE_EXTENSION { KSPIN_LOCK IoctlListLock; LIST_ENTRY PendingIoctlList; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; NTSTATUS CacheControlRequest(PDEVICE_EXTENSION pdx, PIRP Irp, PIRP* pIrp) { KIRQL oldirql; KeAcquireSpinLock(&pdx->IoctlListLock, &oldirql); NTSTATUS status; if (*pIrp) status = STATUS_UNSUCCESSFUL; else if (pdx->IoctlAbortStatus) status = pdx->IoctlAbortStatus; else { IoSetCancelRoutine(Irp, OnCancelPendingIoctl); if (Irp->Cancel && IoSetCancelRoutine(Irp, NULL)) status = STATUS_CANCELLED; else { IoMarkIrpPending(Irp); status = STATUS_PENDING; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); stack->Parameters.Others.Argument1 = (PVOID) *pIrp; IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE) OnCompletePendingIoctl, (PVOID) pdx, TRUE, TRUE, TRUE); PFILE_OBJECT fop = stack->FileObject; IoSetNextIrpStackLocation(Irp); stack = IoGetCurrentIrpStackLocation(Irp); stack->DeviceObject = pdx->DeviceObject; stack->FileObject = fop; *pIrp = Irp; InsertTailList(&pdx->PendingIoctlList, &Irp->Tail.Overlay.ListEntry); } } KeReleaseSpinLock(&pdx->IoctlListLock, oldirql); return status; } VOID OnCancelPendingIoctl(PDEVICE_OBJECT fdo, PIRP Irp) { KIRQL oldirql = Irp->CancelIrql; IoReleaseCancelSpinLock(DISPATCH_LEVEL); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; KeAcquireSpinLockAtDpcLevel(&pdx->IoctlListLock); RemoveEntryList(&Irp->Tail.Overlay.ListEntry); KeReleaseSpinLock(&pdx->IoctlListLock, oldirql); Irp->IoStatus.Status = STATUS_CANCELLED; IoCompleteRequest(Irp, IO_NO_INCREMENT); } NTSTATUS OnCompletePendingIoctl(PDEVICE_OBJECT junk, PIRP Irp, PDEVICE_EXTENSION pdx) { KIRQL oldirql; KeAcquireSpinLock(&pdx->IoctlListLock, &oldirql); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); PIRP* pIrp = (PIRP*) stack->Parameters.Others.Argument1; if (*pIrp == Irp) *pIrp = NULL; KeReleaseSpinLock(&pdx->IoctlListLock, oldirql); return STATUS_SUCCESS; } PIRP UncacheControlRequest(PDEVICE_EXTENSION pdx, PIRP* pIrp) { KIRQL oldirql; KeAcquireSpinLock(&pdx->IoctlListLock, &oldirql); PIRP Irp = (PIRP) InterlockedExchangePointer(pIrp, NULL); if (Irp) { if (IoSetCancelRoutine(Irp, NULL)) { RemoveEntryList(&Irp->Tail.Overlay.ListEntry); } else Irp = NULL; } KeReleaseSpinLock(&pdx->IoctlListLock, oldirql); return Irp; } |
NOTIFY also has an IRP_MJ_CLEANUP handler for pending IOCTLs that looks just about the same as the cleanup handlers I've discussed for read and write operations. Finally, it includes an AbortPendingIoctls helper function for use at power-down or surprise removal time, as follows:
VOID AbortPendingIoctls(PDEVICE_EXTENSION pdx, NTSTATUS status) { InterlockedExchange(&pdx->IoctlAbortStatus, status); CleanupControlRequests(pdx, status, NULL); } |
CleanupControlRequests is the handler for IRP_MJ_CLEANUP. I wrote it in such a way that it cancels all outstanding IRPs if the third argument—normally a file object pointer—is NULL.
NOTIFY is a bit too simple to serve as a complete model for a real-world driver. Here are some additional considerations for you to mull over in your own design process: