Programming the Microsoft Windows Driver Model
Updates & Errata
Revised January 9, 2001

General Note [10/06/00]:

There are a lot of corrections on this page. I thought you’d rather get the straight scoop instead of apologies, excuses, or (worse yet) a cover-up. Also, starting in October 2000, I began adding date stamps (mm/dd/yy) to the entries. Sorry I didn’t think of the time stamps sooner…

p. 5—Incorrect entry in figure

The box labeled “Virtual Device Drivers” in Figure 1-4 should use the abbreviation VDD instead of VxD. VxD is the term one uses to describe Windows 3.x and Windows 9x kernel-mode drivers. Thanks to Alberto Orioli.

p. 7—Meaning of DDK

The acronym DDK now means the Driver Development Kit.

p. 8—WBEM renamed

References to Web Based Enterprise Management and WBEM should be to Windows Management Instrumentation and WMI instead. Refer to the sidebar on p. 452 for an explanation.

pp. 12-13—Error in sample setup

The sample program setup program incorrectly sets the WDMBOOK environment variable to "x:\samples\samples", where "x" is the drive letter of your CD-ROM drive, if you select the Compact option or the Custom option without sample code. Edit your AUTOEXEC.BAT or environment settings to remove the extra "samples" in this path.

p. 12—Additional note on sample programs

Most of the sample programs on the disc rely on GENERIC.SYS to handle some of the extreme complexities of Plug and Play and power management. When you get to chapters 6 and 8, it will probably not be obvious how GENERIC and any given sample dovetail. It happens this way:

  1. The AddDevice function in most samples calls InitializeGenericExtension with a structure that contains pointers to routines for handling the device specifics of Plug and Play and power management (see the table below).
  2. The dispatch routines for IRP_MJ_PNP and IRP_MJ_POWER delegate these IRPs to GENERIC.
  3. GENERIC calls back into the driver at appropriate points.
  4. Refer to GENERIC.RTF (in the GENERIC subdirectory) for details of the various APIs and callbacks used with GENERIC. There should be a shortcut to this file in the start menu.

Driver callback routines used by GENERIC

IRP Type

Callback Function

Purpose

IRP_MJ_PNP

StartDevice

Start the device (map memory registers, connect interrupt, etc.)

 

StopDevice

Halt device and release I/O resources (unmap memory registers, disconnect interrupt, etc.)

 

RemoveDevice

Undo steps performed in AddDevice (disconnect from lower device object, delete device object, etc.)

 

OkayToStop

(Optional) Is it okay to stop this device now (used while processing IRP_MN_QUERY_STOP_DEVICE)?

 

OkayToRemove

(Optional) Is it okay to remove this device now (used while processing IRP_MN_QUERY_REMOVE_DEVICE)?

IRP_MJ_POWER

QueryPower

(Optional) Is a proposed change in device power okay (used while processing IRP_MN_QUERY_POWER)?

 

SaveDeviceContext

(Optional) Save any device context that will be lost during a period of low power.

 

RestoreDeviceContext

(Optional) Restore device context after a period of low power.

 

Thanks to Bill Proctor for pointing out how confusing this was.

p. 13—Windows 2000 environment settings

Windows 2000 no longer uses the AUTOEXEC.BAT file on the boot disk to initialize environment variables. The sample disc setup program therefore updates the Windows 2000 registry instead of AUTOEXEC.BAT. If you're installing the samples onto a computer that runs both Windows 2000 and Windows 98, you should therefore allow setup to update both systems' environment settings.

p. 24—Access rights to Enum key [10/28/00]:

An administrator can now view, but not change, the entries in the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Enum key in the Windows 2000 registry. Thanks to Nuno Romao.

p. 26—CFGMGR32 deprecated

The sidebar "Accessing Device Keys from User Mode" refers to APIs exported by CFGMGR32.DLL as a way to get information about devices. Microsoft considers these APIs obsolete and would prefer people use SetupDiXxx functions instead. Chapter 12 illustrates a few of these functions.

Suppose you had a symbolic link name for a device interface that your driver registered by means of IoRegisterDeviceInterface. (You could get this name by enumerating all instances of the interface GUID or from the parameters to a WM_DEVICECHANGE message, for example.) To obtain the Manufacturer name from the hardware key, you could use this sequence instead of what's shown in the sidebar:

#include <setupapi.h>
...
LPTSTR lpszDeviceName;
HDEVINFO info = SetupDiCreateDeviceInfoList(NULL, NULL);
SP_DEVICE_INTERFACE_DATA ifdata = {sizeof(SP_DEVICE_INTERFACE_DATA)};
SetupDiOpenDeviceInterface(info, lpszDeviceName, 0, &ifdata);
SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)};
SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL,
  0, NULL, &did);
TCHAR buffer[256];
SetupDiGetDeviceRegistryProperty(info, &did, SPDRP_MFG,
  NULL, (PBYTE) mfgname, sizeof(mfgname), NULL);
SetupDiDestroyDeviceInfoList(info);

p. 28 ff.—Mistakes in references to USB42 sample

Several of the illustrations about registry keys relate to an earlier version of the USB42 sample than accompanies the book, and they include mistakes. The reference to the "USB class" immediately after Fig. 2-5 (p. 28) should be to the "Sample class" instead. The service key I was attempting to illustrate in Fig. 2-7 (p. 29) is really named USB42. The FDO service name in Fig. 2-11 (p. 34) is USB42, and the Device Class is “WDM Book Samples”. The Driver name in Fig. 2-12 (p. 34) is USB42, and the Characteristics flags should include FILE_DEVICE_SECURE_OPEN. Thanks to Will Barker.

p. 40—DO_POWER_NOOP no longer defined

The DO_POWER_NOOP flag mentioned inTable 2-2 is no longer defined in the DDK.

p. 47—When DriverUnload is called [10/28/00]:

If the DriverEntry routine creates one or more device objects and then calls IoDeleteDevice in preparation for returning a failure code, the DriverUnload routine will be called in Windows 98. Thanks to Matt Arnold.

In fact, Win98 calls DriverUnload anytime you call IoDeleteDevice to delete the last device object, especially including your RemoveDevice function. It would be therefore be important that the code path leading back out of your driver after your last call to IoDeleteDevice not depend on anything that DriverUnload will undo.

p. 50—Incomplete description of Characteristics argument

The description of the Characteristics argument to IoCreateDevice at the top of the page is incorrect in that the argument I used in the example is FILE_DEVICE_SECURE_OPEN instead of zero. As explained later in the chapter, this value causes the Object Manager to perform security checks on device opens even if they supply additional name components past the name of the device object.

p. 50—Routing of IRP_MJ_CREATE

When I said that the PDO would be the target of an open operation, here's what I really meant. The object manager uses the name someone supplied in a call to CreateFile (or ZwCreateFile, or whatever other function they called) to locate a DEVICE_OBJECT. The security settings and the setting of the DO_EXCLUSIVE flag on that object are the things that control whether the open will be allowed to proceed. The ensuing IRP_MJ_CREATE is still sent to the topmost DEVICE_OBJECT in the stack, however.

p. 57—Using a \?? device name

In the sidebar "Notes on Device Naming", I suggest the temporary expedient of giving your device object a name in the \?? (a/k/a \DosDevices) directory of the kernel name space in order to facilitate testing. This will probably not work in the Terminal Server version of Windows 2000, inasmuch as the device objects won't be replicated outside the console session. Symbolic link objects are replicated, however. Thus, naming your device in the \Device directory and creating a symbolic link in \DosDevices will work just fine.

p. 64—Device Extension fields

The device extension structure declaration shouldn't have the powerstate member. None of the disc samples still does, in fact.

p. 67—Miscellaneous Objects

There's no such thing as a controller object in WDM.

p. 67—DEVICE_OBJECT flags

DO_POWER_PAGABLE means you'll get power IRPs at PASSIVE_LEVEL, not DISPATCH_LEVEL. DO_POWER_NOOP no longer exists, so you don't want to try setting it. Thanks to Doug Kehn.

p. 68—Calling IoAttachDeviceToDeviceStack

The text doesn't mention that IoAttachDeviceToDeviceStack may fail by returning a NULL pointer. The correct thing to do in that case is to fail AddDevice with STATUS_DEVICE_REMOVED. All the sample drivers on the disc do this, in fact.

p. 69—Windows 98 is different than...

One of the references to Windows 98 in the first paragraph on p. 69 should be to Windows 2000. Take your pick—either choice will make sense. Thanks to Scott Taggart.

p. 73—The Special Sauce layer

Look carefully at Fig. 3-1. I could lie and say we left this in to gauge from the e-mail response how many readers actually looked at the figure. The truth is, it slipped by from an early draft. (The drawing looks to me like a hamburger sitting on a pedestal, if you're curious.)

p. 75—Customer flag in status codes

If you complete an IRP with a status code that has the Customer flag set, it will be passed unchanged to the invoking application via GetLastError. (Usually, status codes are translated into Win32 error codes as described in Knowledge Base article Q113996.) Thanks to Udo Eberhardt.

p. 75—NT_SUCCESS is not a status code

The reference to NT_SUCCESS in the 3rd line from the bottom of page 75 should be to STATUS_SUCCESS instead. Thanks to Heidi DeGarmo for the correction (and to Scott Taggert, who corrected my initial mistranscription of the correction).

p. 77—Kernel mode exceptions you can trap

The text indicates, rather imperfectly I must say, that a structured exception handler can trap access violations involving invalid user-mode addresses. A number of other exceptions can also be trapped. Gary Nebbett investigated this in detail and reported his results in the comp.os.ms-windows.programmer.nt.kernel-mode forum on Sept. 3, 1999. See http://www.deja.com/getdoc.xp?AN=520633839.

p. 89—The GSOD

The background color in the e-book version of this figure is wrong, which you probably already knew if you've ever seen the BSOD. If the figure in the printed book appears to be colored, please call Microsoft Press to claim your prize for getting the one copy of the book that was printed in color.

p. 114—Wrong argument for RtlAnsiStringToUnicodeString

The point being made in the section entitled Allocating and Releasing String Buffers is quite correct, but the code sample should read as follows:

UNICODE_STRING foo;
if (bArriving)
  RtlInitUnicodeString(&foo, L"Hello, world!");
else
  {
  ANSI_STRING bar;
  RtlInitAnsiString(&bar, "Goodbye, cruel world!");
  RtlAnsiStringToUnicodeString(&foo, &bar, TRUE);
  }
...
RtlFreeUnicodeString(&foo); //
ç don't do this!

p. 119—Status code misspelled

The correct spelling of the status code used in the code sample is STATUS_OBJECT_NAME_NOT_FOUND. This code is correctly spelled in the text describing the sample.

p. 120—Don’t call RtlQueryRegistryValues [01/09/01]

The RtlQueryRegistryValues function is part of the Win2K kernel’s initialization section. It’s therefore not available by the time a typical WDM driver would like to call it.

p. 122—How to call ZwQueryKey

The first full code sample on p. 122 has two calls to ZwQueryKey. The second call has too many arguments. In addition, it has an incorrect argument. The call in question should should read:

ZwQueryKey(hkey, KeyFullInformation, fip, size, &size);

Thanks to Bill McKenzie.

p. 128—Definition of ASSERT

The definition of the ASSERT macro is incorrect. The correct definition reads as follows:

#define ASSERT(e) if (!(e)){DbgPrint("Assertion failure in "\
  
__FILE__ ", line %d: " #e "\n", __LINE__);\
  _asm int 1;\
  }

(The mistake is the way __FILE__ is spelled in the text.)

p. 129—Feedback from Win98 file I/O functions

The FILEIO sample includes functions for reading and writing disk files via IFSMgr_Ring0_FileIO. These functions can return the wrong number of bytes read or written. The cause of this problem lies in wrapper functions named R0_ReadFile and R0_WriteFile (located in ifsmgr.h in the FILEIO\SYS directory). These wrapper functions both contain the following incorrect statement:

mov [ebx], ecx ; ç wrong!

which should be replaced with the following correct statement:

mov [ebx], eax ; ç right!

Note that the Windows 95 documentation for this VxD service call, which states that the count by bytes read or written is returned in ECX, is incorrect. SP-3 contains this correction. Thanks to Monte Creasor.

p. 129—Win98SE and file I/O functions

Reader Thomas Koeller reports that Win98SE fixes the validity checking problem described for ZwReadFile, etc. Consequently, these functions should work correctly from a WDM driver once the system is past the point where IFSMgr processes DEVICE_INIT.

p. 139—KeRaiseIrqlToDpcLevel not WDM

WDM drivers should not use KeRaiseIrqlToDpcLevel because it's not declared in WDM.H.

p. 150—Windows 98 compatibility note

The Windows 98 versions of KeWaitForSingleObject and KeWaitForMultipleObjects will return the bogus value 0xFFFFFFFF if the blocked thread terminates during the wait, even if the call specifies FALSE for the alertable parameter. This return value is documented for the user-mode functions WaitForSingleObject and WaitForMultipleObjects as WAIT_FAILED and is supposed to denote a failure whose cause could be determined by calling GetLastError. It has no meaning in kernel mode, however.

p. 153—IRQL restriction for KeReleaseMutex

According to the DDK documentation, KeReleaseMutex must be called at IRQL PASSIVE_LEVEL. Noted online by Philip Boucherat.

pp. 154 ff.—Timeout argument to KeSetTimerXxx

The timeout argument to KeSetTimer and KeSetTimerEx is a LARGE_INTEGER, not a pointer to a LARGE_INTEGER. The examples of calls to these functions should therefore use duetime as the argument instead of &duetime. Thanks to A. Orioli.

p. 156—Cancelling a periodic timer

Be sure to call KeCancelTimer to cancel any periodic timer you create before the timer object passes out of scope. If you use a DPC with a periodic timer, call KeRemoveQueueDpc after cancelling the timer, too. Even if you do both of these things, there's a possible problem for which there is currently no bulletproof solution. If you cancel the timer in your DriverUnload routine, it's barely possible for your driver to be unloaded out from under an instance of the DPC routine running on another CPU. This problem may be addressed in a future release of the operating system. You can minimize its impact by cancelling the timer earlier, for example in a handler for IRP_MN_REMOVE_DEVICE.

p. 163—Incorrect table entry

In the fourth row of Table 4-6, the right-hand column should read "Ditto unless you use the XxxUnsafe function and execute at PASSIVE_LEVEL".

p. 164-67—Noninterlocked access to shared data

If you want to fetch the value of an aligned data value that you also manipulate using one of the InterlockedXxx routines, go right ahead. Of necessity, the CPUs that support NT guarantee that you'll get a self-consistent value even if an interlocked operation starts just before or just after the fetch. If a data value is misaligned, however, the interlocked access will lock out other interlocked accesses but may permit a simultaneous fetch to get inconsistent data. Imagine an integer than spans a cache boundary in physical memory, that CPU A wants to fetch the integer, and that CPU B manages to execute an interlocked increment at about the same time. The sequence of events could be: (a) CPU A fetches the cache line containing the high-order fragment of the value, (b) CPU B executes an interlocked increment that causes a carry into the high-order fragment, (c) CPU A fetches the cache line containing the low-order fragment. You avoid this problem by making sure that the value doesn't span a cache boundary, and the easiest way to do that is make sure the value is aligned on the natural boundary for its data type—e.g., 4-byte alignment for a ULONG. Thanks to Rich Testardi.

p. 166—Incorrect function argument

The InsertElement function near the bottom of the page receives an anchor argument that should be typed as a PSOMESTRUCTURE*. That is, the argument is a pointer to the variable that contains the address of the first structure on the list. Thanks to Steve Huggins.

p. 177—Note should refer to StackSize

The note at the bottom of the page should refer to the StackSize field of the DEVICE_OBJECT; there’s nothing called StackCount. Thanks to Steve Huggins.

p. 185—Arguments for DpcForIsr

All of the DpcForIsr routines in my sample drivers now have a context argument of "PDEVICE_EXTENSION pdx".

p. 186—Typographical error

In the next-to-last paragraph, the reference to a KDEVICE_OBJECT should instead be to a KDEVICE_QUEUE. Thanks to Brent Rector.

p. 194—Confusing labels in figure

Figure 5-7 would be more clear if (a) the box now labelled "First stack location" read "Current stack location", and (b) the box labelled "Next stack location" read "Previous stack location" instead. The idea is that IoCompleteRequest works backwards through the stack starting at whatever IoGetCurrentIrpStackLocation would return.

p. 194—When to call IoMarkIrpPending

The rule stated in the text that "any completion routine that doesn't return STATUS_MORE_PROCESSING_REQUIRED" should call IoMarkIrpPending if Irp->PendingReturned is TRUE is almost categorically true, but just almost. If your driver allocated the IRP, installed a completion routine, and then called IoCallDriver without changing the stack pointer, your completion routine should not include this boilerplate code because there's no stack location associated with your driver. (In this situation, it will also be the case that the device object argument to your completion routine will be NULL. What drivers often do is allocate the IRP with an extra stack location, set the DeviceObject pointer in the first location, and use IoSetNextIrpStackLocation to skip over the extra location before calling IoSetCompletionRoutine and IoCallDriver. If you do this, there's no problem calling IoMarkIrpPending in the completion routine, and the completion routine also gets a valid device object.) Thanks to Fred Hewitt.

p. 196—Sense of PendingReturned flag confused [11/15/00]

In the first full paragraph on p. 196, the second sentence should read, “If it’s clear, . . .” Thanks to Eric Vandewater.

p. 199—Caution about IoSkipCurrentIrpStackLocation

When you use IoSkipCurrentIrpStackLocation, be careful: it’s defined as a macro that contains two statement and no surrounding braces. That is, a construction like this:

if (<expression>)
  IoSkipCurrentIrpStackLocation(Irp);  //
ç don’t do this!

will produce unexpected results (i.e., a crash) because the second of the two statements will always be executed. Noted online by Gary Nebbett.

p. 203—IoStartNextPacket must be called at DISPATCH_LEVEL

The Standard Model cancel routine must arrange to call IoStartNextPacket at DISPATCH_LEVEL, as follows:

VOID OnCancel(PDEVICE_OBJECT fdo, PIRP Irp)
  {
  if (fdo->CurrentIrp == Irp)
    {
    KIRQL oldirql = Irp->CancelIrql;
    IoReleaseCancelSpinLock(DISPATCH_LEVEL);
    IoStartNextPacket(fdo, TRUE);
    KeLowerIrql(oldirql);
    }
  else
    {
    KeRemoveEntryDeviceQueue(&fdo->DeviceQueue,
      &Irp->Tail.Overlay.DeviceQueueEntry);
    IoReleaseCancelSpinLock(Irp->CancelIrql);
    }
  CompleteRequest(Irp, STATUS_CANCELLED, 0);
  }

The revised code in boldface is needed to make sure that your StartIo routine is called at DISPATCH_LEVEL, which it probably assumes. If you're using a DEVQUEUE, or some other custom IRP queuing method that doesn't use the global cancel spin lock, your cancel routine probably doesn't include a call to IoStartNextPacket, and you don't need to worry about this correction. Thanks to Jeff Pages.

p. 209—Wrong queuing field in code sample

The code sample for DispatchCleanup uses the wrong IRP queuing field. When you use what I called the “Standard Model” for queuing, IRPs are queued using the field named Tail.Overlay.DeviceQueueEntry.DeviceListEntry. Thanks to Lawrence Lo.

p. 217—How to cancel an asynchronous IRP

In order to safely cancel an IRP that you’ve created with IoAllocateIrp or IoBuildAsynchronousFsdRequest, you can follow this general plan. First define a couple of extra fields in your device extension structure:

typedef struct _DEVICE_EXTENSION {
  PIRP TheIrp;
  ULONG CancelFlag;
  } DEVICE_EXTENSION, *PDEVICE_EXTENSION;

Initialize these fields just before you call IoCallDriver to launch the IRP:

pdx->TheIrp = IRP;
pdx->CancelFlag = 0;
IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE) CompletionRoutine,
  (PVOID) pdx, TRUE, TRUE, TRUE);
IoCallDriver(. . ., Irp);

If you decide later on that you want to cancel this IRP, do something like the following:

VOID CancelTheIrp(PDEVICE_EXENSION pdx)
  {
  PIRP Irp = (PIRP) InterlockedExchangePointer(&pdx->TheIrp, NULL);
  if (Irp)
    {
    IoCancelIrp(Irp);
    if (InterlockedExchange(&pdx->CancelFlag, 1)
      IoFreeIrp(Irp);
    }
  }

This dovetails with the completion routine you install for the IRP:

NTSTATUS CompletionRoutine(PDEVICE_OBJECT junk, PIRP Irp, PDEVICE_EXTENSION pdx)
  {
  if (InterlockedExchangePointer(&pdx->TheIrp)
    || InterlockedExchange(&pdx->CancelFlag, 1))
    IoFreeIrp(Irp);
  return STATUS_MORE_PROCESSING_REQUIRED;
  }

If you have additional cleanup to do on the IRP (cleaning up an MDL, dealing with a system buffer for a DO_BUFFERED_IO situation, etc.), do that in both places in addition to calling IoFreeIrp.

If you want an explanation of how this works and why I think it’s bulletproof, come to one of my WDM programming seminars. J

p. 218—Cleaning up an asynchronous IRP

When you create an MDL for an asynchronous IRP, you may need to perform an additional cleanup step. If you use MmProbeAndLockPages to lock the pages described by the MDL, add the following line to the code sample at the bottom of page 218:

NTSTATUS CompletionRoutine(. . .)
  {
  PMDL mdl;
  while ((mdl = Irp->MdlAddress))
    {
    Irp->MdlAddress = mdl->Next;
    
MmUnlockPages(mdl);           // ç missing code
    IoFreeMdl(mdl);
    }
  . . .
  IoFreeIrp(Irp);
  }

That is, you need to unlock the pages in the MDL before freeing it.

Do not call MmUnlockPages unless you earlier called MmProbeAndLockPages—a bug check will ensue. Thus, if you use MmBuildMdlForNonPagedPool to initialize an MDL to describe an area of kernel non-paged memory, calling MmUnlockPages is incorrect.

pp. 219-20—Don't use IoAttachDevice

The text doesn't discuss a function named IoAttachDevice that was used in previous releases of NT to locate a device object given its name. This function has a horrible bug in that it generates an IRP_MJ_CLOSE request that gets sent to the attaching driver before the function returns. An attaching driver coded without knowledge of the internal implementation of IoAttachDevice will be unable to pass this request down to the target driver, leading to an orphaned handle. Moreover, if the target device happens to have the DO_EXCLUSIVE attribute (which serial port drivers now do in Windows 2000, for example), no-one will thereafter be able to open a handle to the target device. Thanks to Avi Schmidman.

p. 220—Wrong stack location

The code fragment that illustrates setting the file object pointer should use IoGetNextIrpStackLocation instead of IoGetCurrentIrpStackLocation. Thanks to Will Barker.

p. 231—Missing ampersands

The code that develops the raw and translated pointers is missing some ampersands, as follows:

raw = &stack->Parameters.StartDevice.etc;
translated =
&stack->Parameters.StartDevice.etc;

Thanks to Tim Bergel.

p. 246—How to call IoInitializeRemoveLock

The example should read:

IoInitializeRemoveLock(&pdx->RemoveLock, 0, 0, 0);

Using zero for the 3d and 4th arguments means you don't want the corresponding error checking (lifetime and nesting level, respectively) performed, even in the checked build of the OS.

p. 249—Remove lock not needed for IRP_MJ_CREATE

It's not necessary to acquire the remove lock in the IRP_MJ_CREATE dispatch routine. The compatibility note at the end of Chapter 6 (see p. 287) explains that you should not acquire the lock when handling IRP_MJ_CREATE in Windows 98 because of a potential deadlock later on. You need not acquire the lock in Windows 2000—although it can't hurt to do so—because the PnP Manager will not send you an IRP_MN_REMOVE_DEVICE while a handle is open. SP-2 contains revised sample drivers and a revised driver wizard that reflect this insight. Thanks to Mark Russinovich for making me think about this.

p. 253—StartPacket missing a right brace

A right brace is missing from the StartPacket example, and the indenting shown in the e-book is a bit off as well. The example should read as follows (including the brace-comments that are in the disc versions of this routine):

VOID NTAPI StartPacket(PDEVQUEUE pdq, PDEVICE_OBJECT fdo,
  PIRP Irp, PDRIVER_CANCEL cancel)
  {                  // StartPacket
1 KIRQL oldirql;
  KeAcquireSpinLock(&pdq->lock, &oldirql);
2 NTSTATUS abortstatus = pdq->abortstatus;
  if (abortstatus)
    {                // aborting all requests now
    KeReleaseSpinLock(&pdq->lock, oldirql);
    Irp->IoStatus.Status = abortstatus;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    }                // aborting all requests now
3 else if (pdq->CurrentIrp || pdq->stallcount)
    {                // queue this irp
4   IoSetCancelRoutine(Irp, cancel);
    if (Irp->Cancel && IoSetCancelRoutine(Irp, NULL))
      {              // IRP has already been cancelled
      KeReleaseSpinLock(&pdq->lock, oldirql);
      Irp->IoStatus.Status = STATUS_CANCELLED;
      IoCompleteRequest(Irp, IO_NO_INCREMENT);
      }              // IRP has already been cancelled
    else
      {              // queue IRP
5     InsertTailList(&pdq->head, &Irp->Tail.Overlay.ListEntry);
      KeReleaseSpinLock(&pdq->lock, oldirql);
      }              // queue IRP
    }                // queue this irp
6 else
    {                // start this irp
    pdq->CurrentIrp = Irp;
    KeReleaseSpinLock(&pdq->lock, DISPATCH_LEVEL);
    (*pdq->StartIo)(fdo, Irp);
    KeLowerIrql(oldirql);
    }                // start this irp
  }                  // StartPacket

p. 256—Race involving RestartRequests

The original version of RestartRequests has a horrible bug that can lead to multiple IRPs being sent to the StartIo routine. If someone (either in a preempting thread or on another CPU) were to call StartPacket before RestartRequest’s call to StartNextPacket manages to acquire the queue spin lock, StartNextPacket will end up thinking it’s been called because the newly started IRP has finished. The fix for this problem is to copy some of the code from StartNextPacket into RestartRequests, as follows:

VOID NTAPI RestartRequests(PDEVQUEUE pdq, PDEVICE_OBJECT fdo)
  {
  KIRQL oldirql;
  KeAcquireSpinLock(&pdq->lock, &oldirql);

  if (InterlockedDecrement(&pdq->stallcount) > 0)
    {
    KeReleaseSpinLock(&pdq->lock, oldirql);
    return;
    }

  while (!pdq->stallcount && !pdq->abortstatus && !IsListEmpty(&pdq->head))
    {
    PLIST_ENTRY next = RemoveHeadList(&pdq->head);
    PIRP Irp = CONTAINING_RECORD(next, IRP, Tail.Overlay.ListEntry);

    if (!IoSetCancelRoutine(Irp, NULL))
      {
      InitializeListHead(&Irp->Tail.Overlay.ListEntry);
      continue;
      }

    pdq->CurrentIrp = Irp;
    KeReleaseSpinLockFromDpcLevel(&pdq->lock);
    (*pdq->StartIo)(fdo, Irp);
    KeLowerIrql(oldirql);
    return;
    }

  KeReleaseSpinLock(&pdq->lock, oldirql);
  }

SP-6 contains this fix in both GENERIC.SYS and WDMWIZ.AWX. Thanks to Sink Ho.

p. 257—Missing temporary variable

The code sample for IRP cleanup is missing an all-important temporary variable (cf. CleanupRequests in GENERIC.SYS). The loop over IRPs should read as follows:

for (next = first->Flink; next != first; )
  {
  PIRP Irp = CONTAINING_RECORD(next, IRP, Tail.Overlay.ListEntry);
  PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
  
PLIST_ENTRY current = next;
  next = next->Flink;
  if (fop && stack->FileObject != fop)
    continue;
  if (!IoSetCancelRoutine(Irp, NULL))
    continue;
  RemoveEntryList(
current);
  InsertTailList(&cancellist, next);
  }

Thanks to Jicun Zhong.

p. 262—When to free a filtered resource list

Reader Tim Bergel suggests modifying the code which decides to release the previous list of filtered resources (just after note 7 in the code sample) to read as follows:

if (filtered && filtered != original)
  ExFreePool(filtered);

This is because the IoStatus.Information field of the IRP comes pre-initialized to be the same pointer as the original list of resources.

p. 277—Suballocating resources for a non-standard multifunction device

A reader has already tried out my suggestion about creating a little filter driver and exporting a direct-call interface to allow a multifunction parent driver to suballocate resources to the child functions, and so have I. The idea works just fine. Download SP-1 for my samples to get an updated MULFUNC sample that incorporates this idea.

p. 279—Typo in code example

I think the ç in the comment is supposed to be ç instead.

p. 287—Win98 Remove Locking

The compatibility note about IoAcquireRemoveLock and IRP_MJ_CREATE is not worded very artfully. What I mean to say is that, in Windows 98, you should not acquire the remove lock when you process IRP_MJ_CREATE in the expectation that you'll release the lock when you process the matching IRP_MJ_CLOSE.

p. 294—Typo in code example

There should (obviously) not be a semicolon between "sva" and the "=".

p. 296—Wrong table entry

MmGetPhysicalAddress doesn’t belong in Table 7-2 because it has nothing to do with MDLs. MmGetMdlPfnArray, on the other hand, does belong in the table. Thanks to Doug Allen.

p. 301—Undefined variables in Port Resources example

The references to mappedport and nports variables should be to the like-named fields in the device extension (e.g., pdx->mappedport).

p. 306—Meaning of affinity mask

An interrupt gets connected to those CPUs that are (a) specified in the affinity mask, and (b) actively in use. In other words, the decision about which CPUs can accept an interrupt is made during IoConnectInterrupt. (The text implies that the decision is somehow made at interrupt time by the HAL routing the interrupt to a CPU.)

p. 309—Missing argument in call

The code fragment just before Fig. 7-5 showing how to call IoRequestDpc should read as follows:

IoRequestDpc(pdx->DeviceObject, NULL, (PVOID) pdx);

pp. 313-19—Variance between text and samples

The text versions of the PCI42 DpcForisr, TransferFirst and OnInterrupt routines differ in detail from the code sample on disc:

  1. The actual sample uses a variable named busy instead of activerequest. It has the same meaning, however.
  2. The actual sample's DpcForIsr routine simply completes the IRP with whatever IoStatus values are already in the IRP. TransferFirst and OnInterrupt fill in these values before requesting the DPC. The purpose of this arrangement is to make it easier for the interrupt routine to terminate the IRP early due to an error, a PnP event, or whatever.
  3. The actual sample's OnInterrupt routine has more robust tests for exceptional conditions. The code encompassed by notes 2 and 3 actually reads as follows:

PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite);
NTSTATUS status;
if (pdx->busy)
  {
  if (Irp->Cancel)
    status = STATUS_CANCELLED;
  else
    status = AreRequestsBeingAborted(&pdx->dqReadWrite);
  if (!NT_SUCCESS(status))
    dpc = TRUE, pdx->nbytes = 0;
  }

This logic allows the interrupt routine to halt the IRP if it's been cancelled or if a PnP/Power event has occurred that requires all IRPs to be aborted.

  1. The DpcForIsr routine in the actual sample reads as follows [12/04/00]:

VOID DpcForIsr(PKDPC Dpc, PDEVICE_OBJECT fdo, PIRP junk, PDEVICE_EXTENSION pdx)
  {
  PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite);
  StartNextPacket(&pdx->dqReadWrite, fdo);
  IoCompleteRequest(Irp, IO_NO_INCREMENT);
  IoReleaseRemoveLock(&pdx->RemoveLock, Irp);
  }

p. 319—S5933DK1 now works properly

The freeze-during-upgrade referred to in the section "Testing PCI42" no longer occurs.

p. 325—DmaXxBitAddresses flags

You should always set the Dma32BitAddresses and Dma64BitAddresses flags appropriately for the characteristics of your device. Otherwise, your driver won't work correctly on Intel machines with Physical Memory Extensions.

p. 339—Slave DMA driver still needs nMapRegistersAllocated

The text suggests you don't need an nMapRegistersAllocated variable for a slave-DMA driver. You do, though, because you need to use it in the DPC routine when setting up the next stage of your transfer. If you generate a slave-DMA driver using WDMWIZ, you'll get this variable and the code that uses it.

p. 344—Missing flag bit

The code at note 2 should read:

intcsr &= ~(INTCSR_WTCI_ENABLE | INTCSR_RTCI_ENABLE);

The same correction should be applied to the OnInterrupt routine in the PKTDMA sample. Thanks to an anonymous reader.

p. 387—Power queries and USB

The Windows 98 USB hub driver has a bug that causes it to fail any IRP_MN_QUERY_POWER that specifies a device power state. To prevent my power management code from perpetuating the bug (and thereby preventing your computer from going into standby), you can change the DevQueryDownComplete handler to read as follows:

case DevQueryDownComplete:
  {
  if (ctx->status == STATUS_INVALID_PARAMETER)
    ctx->status = STATUS_SUCCESS;
  if (NT_SUCCESS(ctx->status))
    ctx->UnstallQueue = FALSE;
  action = CompleteMainIrp;
  continue;
  }

This correction appears in SP-2 for the book samples.

p. 387—DO_POWER_NOOP flag gone

The DO_POWER_NOOP flag is no longer defined in the DDK. That's just as well, because you really weren't supposed to set it anyway.

p. 393—Problem with POWCPL.DLL

The copy of POWCPL.DLL distributed on the companion disc doesn't work properly on Windows 2000, RC-2. You can rebuild the sample and copy the resulting POWCPL.DLL (from the wdmidle\sys\objchk\i386 subdirectory) to the system32 directory by hand. A revised binary file is in SP-3.

p. 403—Setting power flags in a FiDO

The code which copies flag settings from the lower device object to an upper FiDO should read as follows:

fido->Flags |= fdo->Flags & (DO_DIRECT_IO | DO_BUFFERED_IO);
fido->Flags |= DO_POWER_PAGABLE;

In other words, an upper filter should copy DO_DIRECT_IO and DO_BUFFERED_IO and should always set DO_POWER_PAGABLE. Even if the next driver down has a non-paged power dispatch routine, the power manager can deal with the FiDO having a paged handler. If the FiDO were ever to have a non-paged handler, however, a bug check might ensue if any lower driver had a paged handler.

If you're writing a lower filter driver, it probably doesn't matter what you do with the DO_DIRECT_IO and DO_BUFFERED_IO flags because the function driver above you will probably be setting these the way it wants to. You must not set DO_POWER_PAGABLE if a driver above you has a non-paged power dispatch routine, and you must not clear it if a driver below you has a paged power dispatch routine. The only way to follow this rule is to know how the drivers around you set the flag (a) during initialization, and (b) when they receive an IRP_MN_DEVICE_USAGE_NOTIFICATION. Except in the case of a lower filter for a read/write disk device, you can probably assume that the function driver will have a pagable power dispatch routine, in which case it will be correct for you to set DO_POWER_PAGABLE. But you must communicate with the author of that driver or inspect the source code to find out for sure.

You need not set the DO_POWER_INRUSH flag in a filter because only one of the device objects in the stack needs to set this flag.

SP-3 contains a revised version of the FILTER sample and the WDMWIZ wizard that embodies these suggestions.

p. 404—Note on Filter Drivers

Should read, "you can't change your mind...."

p. 424—METHOD_OUT_DIRECT

You need write access to the output buffer with this buffering method. In current implementations of the operating system, you will also have read access, but that's not guaranteed to always be true. Noted online by Jamie Hanrahan.

p. 427—When to wait for IRP_MJ_INTERNAL_DEVICE_CONTROL

The text says to call KeWaitForSingleObject to await an internal control operation no matter what status code IoCallDriver returns, whereas the code sample on p. 426 plainly only does so if the status code is STATUS_PENDING. Strictly speaking, you should only wait in the STATUS_PENDING case because IoCompleteRequest is free to set the event (which is currently done inside an expensive APC routine) only if the next driver down calls IoMarkIrpPending. In the current releases of Windows 2000 and Windows 98, IoCompleteRequest always queues an APC for this IRP, so that the event always gets set. Consequently, you could follow the advice in the text and still end up with a working driver. For future compatibility, however, it would be smarter to copy the code sample and wait only if you get back STATUS_PENDING.

p. 432—Typo in code sample [10/28/00]:

The statement after note 6 in the code sample on page 432 should read:

stack->Parameters.Others.Argument1 = (PVOID) pIrp;

That is, there should be no “*” in front of pIrp. Thanks to Will Dean.

p. 440—Missing right brace

A right-brace in the middle of the page was printed as a left brace:

if (!NT_SUCCESS(status))
  {
  kill = TRUE;
  break;
  
}

Thanks to Mark Phillips.

p. 445—When to use IoXxxWorkItem

You should always use the IoXxxWorkItem routines to queue work items because an IO_REMOVE_LOCK doesn't afford sufficient protection in the following case. Suppose a device removal is pending (inside IoReleaseRemoveLockAndWait) at the time a work item callback occurs. As soon as the callback routine calls IoReleaseRemoveLock, it's possible for the driver to be unloaded while instructions in the callback remain to be executed. The new routines prevent this occurrence. (Note also that IoFreeWorkItem is misspelled in the text.)

SP-2 for the book samples includes a revised WORKITEM sample that illustrates how to safely use a work item. In summary, you would first allocate a context structure and a separate IO_WORKITEM:

typedef struct _RANDOM_JUNK {
  PIO_WORKITEM item;
  <other stuff>
  } RANDOM_JUNK, *PRANDOM_JUNK;

...

PRANDOM_JUNK junk = (PRANDOM_JUNK) ExAllocatePool(NonPagedPool,
  sizeof(RANDOM_JUNK));
PIO_WORKITEM item = IoAllocateWorkItem(fdo);
junk->item = item;

After initializing the context structure (namely, junk), you queue the work item:

IoQueueWorkItem(item, (PIO_WORKITEM_ROUTINE) WorkItemCallback,
  DelayedWorkQueue, junk);

The callback routine cleans up by releasing the two pieces of pool memory:

VOID WorkItemCallback(PDEVICE_OBJECT fdo, PRANDOM_JUNK junk)
  {
  ...
  IoFreeWorkItem(junk->item);
  ExFreePool(junk);
  }

You should also follow the directions in the original text about acquiring and releasing the remove lock before queuing the work item and when the work item callback is about to return.

IoQueueWorkItem closes the window of vulnerability that would otherwise exist at the very end of the callback routine by taking out an extra reference to your device object. The I/O Manager releases this reference after the callback returns. Your driver cannot, of course, be unloaded while this reference exists.

The IoXxxWorkItem routines are declared in WDM.H and defined in NTOSKRNL.LIB (but not WDM.LIB). They aren't implemented in Windows 98, so you must either (a) ship WDMSTUB.VXD or something like it that implements these routines for Windows 98, or (b) ship two binaries—one for Windows 2000 that uses IoXxxWorkItem and another for Windows 98 that uses ExXxxWorkItem.

p. 446—Typo in code example

The call to InterlockedIncrement in the last example on the page should read (missing parenthesis):

if (InterlockedIncrement(&pdx->handles) == 1)

Thanks to Neal Galbo.

pp. 446-49—Variance between PIOFAKE text and sample code

PIOFAKE now uses a busy flag in the device extension to avoid requesting multiple DPCs for the same IRP.

p. 455—Namespaces for MOF schema

As of RC-2 (build 2128) of Windows 2000, it is no longer necessary to populate the WMI repository by hand. Furthermore, you should specifically not insert any namespace #pragma into the MOF file. (The DRIVER.MOF file in the WMIEXTRA sample incorrectly contains such a #pragma. To test that sample, you will need to remove the #pragma and rebuild the sample. SP-3 contains a revised copy of this file.)

p. 464—Error in SetDataBlock example

The code at note 1 should read (missing parenthesis):

if (bufsize == sizeof(ULONG))

p. 465—SetDataItem typo

Sentence beginning "To change the one field" should read, "To change the value of one field...."

p. 475—WMICORE.MOF renamed

WMICORE.MOF is now named WMICORE.MOFF in the DDK.

p. 488-89—Using the EZ-USB development board in Windows 2000

If you are unable to install the EZ-USB development board in Windows 2000, the trouble may be the INF file you're using. I modified the one distributed by Cypress to include the service declarations that are needed for Windows 2000. You can download the modified ezusb.inf from this site.

Note also that the ezusb.sys driver distributed with your development board may be causing a bug check during power-down operations because it doesn't set DO_POWER_PAGABLE in its device objects. If you've left the Anchor default device in place on the board, just be sure to unplug the device before putting the computer through any sort of power cycle.

p. 490-91—Power management with EZ-USB

Although the EZ-USB chipset from Anchor Chips (now Cypress Semiconductor) makes it easy to download new firmware from a driver, you won't want to use this feature in a production device. You need to develop a "loader" driver that downloads firmware and a function driver that manages the device after the download. Unfortunately, if you put the computer into standby, the operating system may depower the USB bus, whereupon the device loses its downloaded firmware. When power is restored, the system will unload the function driver and reload the "loader" driver (which will then download the firmware again and cause the function driver to get reloaded). The function driver will lose any state information it may have been trying to preserve across the power cycle, and any application handles that may have been open will be orphaned. You should therefore plan on putting firmware into EEPROM when you use this chipset.

p. 517—Indices for configuration descriptors

The descriptor index you should use when reading a configuration descriptor depends on how your device firmware responds to the GET_DESCRIPTOR control request and doesn't appear to be prescribed by the USB specification. Even though configurations are numbered beginning with one, the descriptors of configurations might be numbered beginning with zero. All of my sample drivers read configuration descriptor number zero because that's what the Anchor Chips EZ-USB chip expects. You might need to experiment with the (initial) value of the iconfig variable used in the example code to see what works with your device. If you get it wrong, the device will simply not respond and your driver will appear to hang in the first call to SendAwaitUrb. Unplugging the device will cause the URB to fail and allow your driver to unload.

p. 525—IRP Queuing needed for LOOPBACK [10/28/00]

The LOOPBACK sample needs to queue incoming read and write IRPs to avoid the mixup that would occur by presenting successive segments of different IRPs to the bus driver out of sequence. SP-7 includes a revised LOOPBACK sample that addresses this problem.

p. 527-28—Intermediate short transfers not allowed

According the USB specification (section 5.8.3), all transactions except the last in a transfer to a bulk endpoint must be for the full maximum size of the endpoint. In the LOOPBACK sample, I attempted to “optimize” the staging of transfers by sending just enough data in the first stage to get to a page boundary for the second and subsequent stages. Unless the user-mode buffer is fortuitously aligned on a 64-byte boundary, this strategy results in a forbidden short transfer. Thus, the code that determines the size of each stage should read as follows:

NTSTATUS ReadWrite(. . .)
  {
  . . .
  ULONG seglen = length;    //
ç code at note 6 on p. 527
  if (seglen > MAXTRANSFER)
    
seglen = MAXTRANSFER;
  . . .
  }

NTSTATUS OnReadWriteComplete(. . .)
  {
  . . .
  if (NT_SUCCESS(status) && ctx->length)
    {
    ULONG seglen = ctx->length;  //
ç code at note 10 on p. 528
    if (seglen > MAXTRANSFER)
      
seglen = MAXTRANSFER;
    . . .
    }
  . . .
  }

SP-5 for the book samples incorporates these changes. Thanks to John Hyde.

p. 527-28—Additional preparation of MDL needed

Reader Peter Diaconescu encountered a bug check under heavy load conditions in a driver patterned after the LOOPBACK sample. As best I can diagnose the problem, the USB bus driver contains a call to MmGetSystemAddressForMdl which was returning NULL. (This isn’t supposed to be possible—the function is supposed to bug-check if it can’t allocate page table entries.) A preemptive call to MmGetSystemAddressForMdlSafe would head off the problem, but the function on which that macro relies is not exported in Windows 98. An alternative, portable, way to prevent the problem from occurring is for the ReadWrite subroutine (p. 527) to obtain a kernel-mode virtual mapping of the first stage buffer in the following way:

IoBuildPartialMdl(. . .);         // ç code at note 7 on p. 527
CSHORT oldfail = mdl->MdlFlags & MDL_MAPPING_CAN_FAIL;
mdl->MdlFlags |= MDL_MAPPING_CAN_FAIL;
if (!MmMapLockedPages(mdl, KernelMode))
  {
  ExFreePool(ctx);
  IoReleaseRemoveLock(&pdx->RemoveLock, Irp);
  return CompleteRequest(Irp, STATUS_INSUFFICIENT_RESOURCES, 0);
  }
if (!oldfail)
  mdl->MdlFlags &= ~MDL_MAPPING_CAN_FAIL;

The MDL_MAPPING_CAN_FAIL flag tells the Windows 2000 version of MmMapLockedPages to return a NULL pointer instead of bug checking if no page table entries are available to accomplish the mapping. The flag has no effect in Windows 98, wherein MmMapLockedPages has always behaved this way.

The I/O completion routine for read and write IRPs (p. 528) also needs to be modified in a corresponding way and to include a call to MmPrepareMdlForReuse:

MmPrepareMdlForReuse(ctx->mdl);
IoBuildPartialMdl(. . .);  //
ç code after note 10 on p. 528
CSHORT oldfail = . . .;    // ç etc. – similar to above

SP-5 for the book samples incorporates these changes.

p. 529—How to release an MDL

The line of code at the top of p. 529 should read:

IoFreeMdl(ctx->mdl);

Thanks to Peter Diaconescu.

p. 533—USBINT Sample

The correct way to reinitialize the interrupt polling IRP would be to call IoReuseIrp instead of just clearing the Cancel flag. Unfortunately, IoReuseIrp isn't implemented in Win98. I would suggest calling IoReuseIrp anyway and shipping WDMSTUB.SYS (which contains an implementation of this function) for use on Win98 systems.

p. 533—Who calls StartInterruptUrb?

The initial call to StartInterruptUrb (which launches the first interrupt polling URB) occurs in a fairly convoluted way in the USBINT sample. StartDevice initially puts the device in a low power state. When someone opens the first handle, DispatchCreate restores power by calling into GENERIC.SYS, which eventually calls USBINT's RestoreContext routine. RestoreContext in turn calls StartInterruptUrb.

Thereafter, the completion routine for the interrupt URB, OnInterrupt, reissues the URB. If power ever goes away, SaveContext cancels the interrupt URB in a way that I believe is multiprocessor safe.

If you didn't want the behavior of powering down while idle, you could program this aspect of the driver more simply by just having StartDevice begin polling. You still have to stop and start the polling URB during power events, so you need the code that's in this driver's SaveContext and RestoreContext functions.

Thanks to Bill Proctor for pointing out how confusing this was.

p. 544—IRP Queuing needed for USBISO [10/28/00]

The USBISO sample needs to queue incoming read and write IRPs in order to avoid a reported bug in the way the USB bus driver handles multiple IRPs in a multiprocessor system. SP-7 incorporates a revised version of the sample that uses a DEVQUEUE for this purpose. Unfortunately, the same bug would also seem to make it impossible to send all the subsidiary IRPs at once. That is, as pointed out on p. 544, you miss frames if you don’t send the ISO reads or writes back to back, but the bug means you can’t do that. I don’t have a solution for this problem.

p. 554—What happens to associated IRPs [10/06/00]:

The 4th line of the sidebar on associated IRPs should say that “the I/O Manager automatically completes the master IRP” when all the associated IRPs finish. Thanks to Hrishikesh Vidwans.

p. 563—Temporary names in Win98 CopyFiles statements

If you use a temporary filename (the third argument in a statement within a CopyFiles section), Win98 always installs the file under the temporary name, even if the file isn't currently in use. You must restart in order to get the temporary file renamed.

p. 575—Error in Fig. 12-4

Figure 12-4 is out of date. The disc copy of PKTDMA's INF file has SourceDisksFiles and SourceDisksNames sections, so the errors related to those sections no longer appear when you run CHKINF.

pp. 589 ff.—Avoiding a reboot when using WDMSTUB.VXD

Appendix A describes how to create a static VxD to provide stubs for missing kernel-mode support functions. Since the VxD has to be loaded before you can install your driver, however, a reboot will normally be required as part of the installation process. WHQL requirements (not to mention good taste) preclude requiring a reboot, though.

To avoid the reboot, I recommend using a lower filter driver instead of a static VxD to supply the missing support functions. SP-4 contains a new version of WDMSTUB that embodies this idea. As described in the NEWSTUB\WDMSTUB.HTM file that accompanies the sample, you can simply install WDMSTUB.SYS as a lower filter driver on Windows 98 systems in order to satisfy otherwise unresolved symbols in your function driver.

p. 593—Obsolete power functions

PoRegisterDeviceNotify and PoCancelDeviceNotify are no longer defined in the DDK. Accordingly, the disc copy of WDMSTUB doesn't stub them either.

p. 594—WDM_MINORVERSION

The version numbering for IoIsWdmVersionAvailable uses a packed-decimal representation, so WDM_MINORVERSION is 0x10 in Windows 2000.