IO Control Operations

[Previous] [Next]

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 DeviceIoControl API

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.

Synchronous and Asynchronous Calls to DeviceIoControl

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.

Defining I/O Control Codes

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.

click to view at full size.

Figure 9-9. Fields in an I/O control code.

The fields have the following interpretation:

  • The device type (16 bits, first argument to CTL_CODE) is supposed to indicate what type of device implements this control operation. I'm unaware of any "IOCTL police" inside either Microsoft Windows 98 or Microsoft Windows 2000, however, and I believe that the content of the field is actually pretty arbitrary. It is customary, though, to use the same value (for example, FILE_DEVICE_UNKNOWN) that you use in the driver when you call IoCreateDevice.
  • The access code (2 bits, fourth argument to CTL_CODE) indicates the access rights an application needs to its device handle to issue this control operation.
  • The function code (12 bits, second argument to CTL_CODE) indicates precisely which control operation this code describes. Microsoft reserves the first half of the range of this field—that is, values 0 through 2047. You and I therefore assign values in the range 2048 through 4095. I'm pretty sure I'll never feel cramped by being able to define only 2048 IOCTLs for one of my devices.
  • The buffering method (2 bits, third argument to CTL_CODE) indicates how the I/O Manager is to handle the input and output buffers supplied by the application. I'll have a great deal to say about this field in the next section when I describe how to implement IRP_MJ_DEVICE_CONTROL in a driver.

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.

Handling IRP_MJ_DEVICE_CONTROL

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); }

  1. You can be sure of being called at PASSIVE_LEVEL, so there's no particular reason for a simple dispatch function to be anywhere but paged memory.
  2. Like other dispatch functions, this one needs to claim the remove lock while it does its work. That prevents the device object from disappearing out from underneath us because of a PnP event.
  3. The next few statements extract the function code and buffer sizes from the parameters union in the I/O stack. You often need these values no matter which specific IOCTL you're processing, so I find it easier to always include these statements in the function.
  4. This is where you get to exercise your own creativity by inserting case labels for the various IOCTL operations you support.
  5. It's a good idea to return a meaningful status code if you're given an IOCTL operation you don't understand.

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.

METHOD_BUFFERED

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.

A Security Hole?

I always get a slight nervous feeling when I think about the importance of the buffering and access-control flags in an IOCTL function code. Suppose some malicious application were to submit an IOCTL that used flag values other than the ones I intended. Would that cause a driver to crash or do something else it shouldn't? Well, usually not.

Most of the time, you code the dispatch function for IOCTL requests with a switch statement. The case labels reference numeric constants that must match exactly with all 32 bits of whatever code the application supplies. So, if an application were to change any of the bits in an IOCTL code, none of the case labels in the driver would match and some (presumably benign) default action would occur.

The DIRECT Buffering Methods

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.

METHOD_NEITHER

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.

Internal I/O Control Operations

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.

Notifying Applications of Interesting Events

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); }

Application Notification by Using Events

Sometimes all you need to do in a driver is notify an application that an event has occurred, without passing any explanatory data to the application. A standard technique for doing so involves an ordinary Win32 event that the driver signals. To use this method, the application first calls CreateEvent or OpenEvent to open a handle to an event object, which it then passes to the driver via DeviceIoControl. The driver can convert the user-mode handle to an object pointer by making this call:

PKEVENT pEvent; status = ObReferenceObjectByHandle(hEvent, EVENT_MODIFY_STATE, *ExEventObjectType, Irp->RequestorMode, (PVOID*) &pEvent, NULL);

Note that the IOCTL must be handled at PASSIVE_LEVEL and in the context of the process that owns the hEvent handle.

At this point, the driver has a pointer to a KEVENT object, which it can use as an argument to KeSetEvent at an auspicious moment. The driver also owns a reference to the event object, and it must call ObDereferenceObject at some point. The right time to dereference the object depends on the exact way the application and the driver fit together. A good guideline might be to dereference the event as part of handling the IRP_MJ_CLOSE for the handle used in the IRP_MJ_DEVICE_CONTROL that supplied the event handle in the first place. The EVWAIT driver sample on the companion disc illustrates this particular method.

The kernel service routines IoCreateNotificationEvent and IoCreateSynchronizationEvent create event objects that can also be shared by user-mode programs. They are unavailable in Windows 98 and, therefore, unavailable to true WDM drivers.

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.

Working with an Asynchronous IOCTL

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.

How the Helper Routines Work

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; }

  1. We use a spin lock to guard the list of pending IOCTLs and also to guard all of the pointer cells that are reserved to point to the current instance of each different type of asynchronous IOCTL request.
  2. This is where we enforce the rule—it's more of a design decision, really—that only one IRP of each type can be outstanding at one time.
  3. This if statement accommodates the fact that we may need to start failing incoming IRPs at some point because of PnP or power events.
  4. Since we'll pend this IRP for what might be a long time, we need to have a cancel routine for it. I've discussed cancel logic so many times in this book that I feel sure you'd rather not read about it once more.
  5. Here, we've decided to go ahead and cache this IRP so that we can complete it later. Since we're going to end up returning STATUS_PENDING from our DispatchControl function, we need to call IoMarkIrpPending.
  6. We need to have a way to NULL out the cache pointer cell when we cancel the IRP. It's very difficult to get context parameters into a cancel routine, so I decided to set up an I/O completion routine instead. I use the Parameters.Others.Argument1 slot in the stack to record the cache pointer address.
  7. In order for the completion routine we've just installed to get called, we must advance the I/O stack pointer by calling IoSetNextIrpStackLocation. In this particular driver, we know there must be at least one more stack location for us to use because our AddDevice function would have failed if there hadn't been a driver object underneath ours. The device and file object pointers that later routines need come from the then-current stack location, so we must initialize them as well.
  8. This statement is the point of installing a completion routine. If the IRP gets cancelled, we'll eventually gain control to nullify the cache pointer.
  9. In the normal course of events, this statement uncaches an IRP.
  10. Now that we've uncached our IRP, we don't want it to be cancelled any more. If IoSetCancelRoutine returns NULL, however, we know that this IRP is currently in the process of being cancelled. We return a NULL IRP pointer in that case.

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:

  • A driver might have several types of events that trigger notifications. You could decide to deal with these by using a single IOCTL code, in which case you'd indicate the type of event by some sort of output data, or by using multiple IOCTL codes.
  • You might want to allow multiple threads to register for events. If that's the case, you certainly can't have a single IRP pointer in the device extension—you need a way of keeping track of all the IRPs that relate to a particular type of event. If you use only a single type of IOCTL for all notifications, one way to keep track is to rely on the PendingIoctlList I've already discussed. Then, when an event occurs, you execute a loop in which you call ExInterlockedRemoveHeadList and IoCompleteRequest to empty the pending list. (I avoided this complexity in NOTIFY by fiat—I decided I'd run only one instance of the test program at a time.)
  • Your IOCTL dispatch routine might be in a race with the activity that generates events. For example, in the USBINT sample I'll discuss in Chapter 11, we have a potential race between the IOCTL dispatch routine and the pseudointerrupt routine that services an interrupt endpoint on a USB device. To avoid losing events or taking inconsistent actions, you need a spin lock. Refer to the USBINT sample on the companion disc for an illustration of how to use the spin lock appropriately. (Synchronization wasn't an issue in NOTIFY because by the time a human being is able to perform the keystroke that unleashes the event signal, the notification request is almost certainly pending. If not, the signal request gets an error.)

More About the NOTIFY Sample

NOTIFY consists of a WDM device driver (in the SYS subdirectory) and a Win32 console-mode test program (in the TEST subdirectory). You can install the driver via the Add New Hardware wizard or the FASTINST utility. Then you can launch the test program. It will spawn a separate thread to issue the IOCTL_WAIT_NOTIFICATION I/O control request. Then it prompts you to execute a keystroke or to press Ctrl+Break to end the test. If you type a key, the test program performs an IOCTL_GENERATE_EVENT, passing the scan code of your keystroke as input data. The driver then completes the pending notification IRP after storing this scan code as output data. Alternatively, if you hit Ctrl+Break at the point at which TEST is prompting you for a keystroke, this will eventually cause the I/O Manager to cancel the outstanding notification IRP.



Programming the Microsoft Windows Driver Model
Programming the Microsoft Windows Driver Model
ISBN: 0735618038
EAN: 2147483647
Year: 1999
Pages: 93
Authors: Walter Oney

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net