We’ve all* defined our own IOCTL codes in the past. They’re a primary way to enable user-mode -> driver and driver -> driver communication. Although they were probably originally envsioned in the context of storage-related drivers, Microsoft supports the use of an IOCTL dispatch in most kinds of drivers. NDIS, for example, supports NdisMRegisterDevice for the explicit purpose of providing access to the IOCTL framework in network drivers.
IOCTL codes are defined using the CTL_CODE() macro, which is part of ntddk.h in kernel-mode and winioctl.h in user-mode. The first parameter takes a Device Type, which can either be one of the Microsoft-defined device types (see the DDK headers for a list) or a custom device type code.
There are a couple of things to keep in mind here. The first is that the Device Type parameter must match the device type that is passed into IoCreateDevice(). Also, If you define a custom device type, it should be above 0x8000. The bottom 15 bits are what actually represent the device type, and the 16th bit is known as the Common bit. The DDK requires that the Common bit be set on all custom Device Types. Another way of saying this is by requiring all custom codes to be between 0x8000 and 0xFFFF. Similarly, the function code is required to be between 0x800 and 0xFFF, because the top bit (“Custom” in this case) is required to be set for non-Microsoft-defined function codes. Playing by the rules will make your driver as compatible as possible with all releases of the OS, present and future.
Method is one of METHOD_BUFFERED, METHOD_IN_DIRECT, METHOD_OUT_DIRECT, or METHOD_NEITHER. METHOD_BUFFERED is the most common transfer method, and is generally the safest and easiest to use. This method double-buffers your data by copying it from the supplied user-mode buffer into a newly-created kernel-mode buffer, and then passing that new buffer to your driver instead of the original one. If you’re transferring less than one page of data (4K on x86), and especially if you’r doing it infrequently, this is the way to go. If you’re tranferring larger amounts of data, one of the DIRECT methods may make sense. This is particularly true if you’re going to wind up DMAing your data to or from a device, but it is also true if you just want to avoid the double-buffer in general. I’m not going to discuss METHOD_NEITHER at the moment, other than to say that you shouldn’t use it. I’ll get in to more detail about why another day.
The final knob to turn is the RequiredAccess parameter. I admit that I really didn’t understand what this parameter was for until quite a while after I wrote my first driver. It turns out that it is a method for enforcing some small but nontrivial amount of access control on who can call your IOCTL. This specifies the kind of access the user must have to the device, as specified in the CreateFile() call, in order for the IO manager to let the IRP through. FILE_ANY_ACCESS means that they can send the IRP with virtually any access at all, as long as they have an open file handle to the driver. FILE_READ_ACCESS and FILE_WRITE_ACCESS loosely correlate to the ability to read and write data to and from the device.
Most driver writers just set this to FILE_ANY_ACCESS and forget about it. This is, of course, exactly the wrong thing to do. A much better strategy is to specify the most restrictive access possible (FILE_READ_ACCESS|FILE_WRITE_ACCESS — yes, you can OR them together) whenever possible, and only remove bits when necessary (“necessary” depends on the kind of driver you’re writing). This parameter is particularly important in IOCTLs where you’re actually reading and writing data — why would you allow a user to read data from an IOCTL if you wouldn’t allow the same user to read data using ReadFile()? — but it should probably be applied carefully to all IOCTLs.
Finally, it might be obvious, but try to name your IOCTL codes something obvious. My office has a standard that goes IOCTL__. In other words, you might have IOCTL_POSVPN_SET_INFO to configure our VPN driver. This goes with the standard rants about variable naming, and is generally an important thing if you want someone else to be able to work on your code.
OK, I expect everyone to run out and tighten up their use of CTL_CODE(). When you’re done with that, go listen to Fred Jones, Part 2, by Ben Folds. It’ll make you a Better Person.
* OK, “All” might be a bit of an exaggeration. 🙂