Why is my completion routine's DeviceObject NULL?

At first glance, this is a "what were they thinking when they designed this?" type of issue, but after a little inspection it becomes a little more clear. So, when would the PDEVICE_OBJECT DeviceObject parameter to your completion routine ever be NULL? Well, in the case where you allocate the PIRP yourself, it can be. On the flip side, if you received the PIRP in your DispatchRoutine, then the PDEVICE_OBJECT will be a valid pointer value. This leads to 2 questions:

  1. How can I get my PDEVICE_OBJECT passed to a completion routine for a PIRP that I allocated?
  2. Why is the DeviceObject NULL in this case to begin with?

To have a valid PDEVICE_OBJECT passed to your completion routine for a PIRP that you allocate, you must allocate and initialize an extra stack location in the PIRP with your DeviceObject. After you initialize the extra stack, you initialize the stack location for the device (TargetDeviceObject in this case) that you will be sending the I/O to. The following is one sample implementation:

     PIO_STACK_LOCATION pNext;
    PIRP pIrp;
    NTSTATUS status;

    //
    // Allocate an additional stack location so that we can use it on our own
    //
    pIrp = IoAllocateIrp(TargetDeviceObject->StackSize+1, FALSE);

    if (pIrp == NULL) {
        status = STATUS_INSUFFICIENT_RESOURCES;
        return status; // or do appropriate cleanup here
    }

    //
    // Currently the irp's stack location is invalid, this will set it to a
    // valid stack location
    //
    IoSetNextIrpStackLocation(pIrp);

    //
    // DeviceObject is our own DeviceObject, not the device we are sending I/O to
    //
    IoGetCurrentIrpStackLocation(pIrp)->DeviceObject = DeviceObject;

    IoSetCompletionRoutine(pIrp, CompletionRoutine, Context, TRUE, TRUE, TRUE);

    //
    // Get the next stack location, this is what the TargetDeviceObject will see
    pNext = IoGetNextIrpStackLocation(pIrp);

    // .. format pNext's MajorFunction and Parameters ...

    status = IoCallDriver(TargetDeviceObject, pIrp);

One additional note: Since you cannot control the StackSize used to allocate the PIRP when calling IoBuildXxx(), you cannot use this trick for PIRPs allocated by these functions.

So, why do you need to go through this mess just to get a valid DeviceObject? Furthermore, why does it work for PIRPs that have been sent to your device, but not for PIRPs you allocate yourself? It has to do with how the I/O manager gets the DeviceObject to pass into your completion routine. First, when you set a completion routine, it is set in the next stack location. This means that you can safely set a completion routine on a PIRP that does not have a valid current stack location (like a PIRP that you just allocated). Second, the DeviceObject comes for the current stack location when the completion routine is invoked. This means that for a PIRP which you allocated (assuming you didn't use the trick above), the current stack location for your PIRP is invalid when the completion routine is invoked, thus there is no DeviceObject to pass to it.

When a PIRP is presented to your driver's dispatch routine, it has a current stack location. By having a valid current stack location, a completion routine on the PIRP will have a valid DeviceObject pointer. This is why the trick works, it creates an extra stack location (and sets it) so that there is a valid current stack location when the completion routine is invoked.