Why does my kernel driver crash with BAD_POOL_CALLER?

I’m learning how to write Windows kernel drivers, however I got stuck on something not so obvious to me.

My driver uses PsSetLoadImageNotifyRoutine to register a callback that is called on every image (dll) load. Once a image load is registered I put it into a linked list, signal an event and have a user mode application retrieve the information using DeviceIoControl.

The problem I’m having is that the driver crashes with BAD_POOL_CALLER. According to windbg the problem is located in my DispatchIoctl routine. The stacktrace points to nt!ExFreePool however I don’t see who else would have freed the PIMAGE_CALLBACK_INFO2 structure.

I also tried to remove the ExFreePool call but got just another memory error.

Why does my kernel driver crash with BAD_POOL_CALLER?

VOID ImageCallback(
    IN PUNICODE_STRING FullImageName,
    IN HANDLE ProcessId,
    IN PIMAGE_INFO ImageInfo
)
{
    UNREFERENCED_PARAMETER(ImageInfo);

    PDEVICE_EXTENSION extension;
    //
    // Assign extension variable
    //
    extension = (PDEVICE_EXTENSION)g_pDeviceObject->DeviceExtension;

    PIMAGE_CALLBACK_INFO2 info = (PIMAGE_CALLBACK_INFO2)ExAllocatePoolWithTag(NonPagedPool, sizeof(IMAGE_CALLBACK_INFO2), 1);
    if (info == NULL)
    {
        DbgPrint("STATUS_INSUFFICIENT_RESOURCES");
        return;
    }
    DbgPrint("FullImageName: %wZ\n", FullImageName);
    info->FullImageName = FullImageName;

    DbgPrint("ProcessId: %d\n", ProcessId);
    info->ProcessId = ProcessId;

    PushEntryList(&SingleHead, &(info->LinkField));

    KeSetEvent(extension->ProcessEvent, 0, FALSE);
    KeClearEvent(extension->ProcessEvent);
}

NTSTATUS DispatchIoctl(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP           Irp
)
{
    NTSTATUS               ntStatus = STATUS_UNSUCCESSFUL;
    PIO_STACK_LOCATION     irpStack = IoGetCurrentIrpStackLocation(Irp);
    PDEVICE_EXTENSION      extension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
    PIMAGE_CALLBACK_INFO   pImageCallbackInfo;

    UNREFERENCED_PARAMETER(extension);

    //
    // These IOCTL handlers are the set and get interfaces between
    // the driver and the user mode app
    //
    switch (irpStack->Parameters.DeviceIoControl.IoControlCode)
    {
    case IOCTL_PROCOBSRV_ACTIVATE_MONITORING:
    {
        ntStatus = ActivateMonitoringHanlder(Irp);
        break;
    }

    case IOCTL_PROCOBSRV_GET_IMAGEINFO:
    {
        if (irpStack->Parameters.DeviceIoControl.OutputBufferLength >=
            sizeof(IMAGE_CALLBACK_INFO))
        {
            pImageCallbackInfo = (PIMAGE_CALLBACK_INFO)Irp->AssociatedIrp.SystemBuffer;

            PSINGLE_LIST_ENTRY SingleListEntry;
            SingleListEntry = PopEntryList(&SingleHead);

            if (SingleListEntry != NULL)
            {
                PIMAGE_CALLBACK_INFO2 info = (PIMAGE_CALLBACK_INFO2)CONTAINING_RECORD(SingleListEntry, IMAGE_CALLBACK_INFO2, LinkField);

                if (info->FullImageName != NULL)
                {
                    DbgPrint("FullImageName: %wZ\n", info->FullImageName);
                    RtlCopyMemory(pImageCallbackInfo->FullImageName, info->FullImageName->Buffer, info->FullImageName->Length);
                    RtlFreeUnicodeString(info->FullImageName);
                }
                pImageCallbackInfo->ProcessId = info->ProcessId;

                ExFreePool(info);
            }

            ntStatus = STATUS_SUCCESS;
        }
        break;
    }

    default:
        break;
    }

    Irp->IoStatus.Status = ntStatus;
    //
    // Set number of bytes to copy back to user-mode
    //
    if (ntStatus == STATUS_SUCCESS)
        Irp->IoStatus.Information =
        irpStack->Parameters.DeviceIoControl.OutputBufferLength;
    else
        Irp->IoStatus.Information = 0;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return ntStatus;
}

You should always post the !analyze -v output if you want help with a crash…

But in this case the failure seems pretty obvious. The FullImageName argument is not yours to free. If you want to pass it to your device control handler you need to make your own deep copy that you then subsequently free.

Nice catch! That solved that problem. I removed the RtlFreeUnicodeString call. Now I get another one.

*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

BAD_POOL_HEADER (19)
The pool is already corrupt at the time of the current request.
This may or may not be due to the caller.
The internal pool links must be walked to figure out a possible cause of
the problem, and then special pool applied to the suspect tags or the driver
verifier to a suspect driver.
Arguments:
Arg1: 0000000000000003, the pool freelist is corrupt.
Arg2: fffff80003c582d0, the pool entry being checked.
Arg3: 0000000000000000, the read back flink freelist value (should be the same as 2).
Arg4: 6c734d46020c0008, the read back blink freelist value (should be the same as 2).

Debugging Details:
------------------


KEY_VALUES_STRING: 1


STACKHASH_ANALYSIS: 1

TIMELINE_ANALYSIS: 1


DUMP_CLASS: 1

DUMP_QUALIFIER: 401

BUILD_VERSION_STRING:  7601.24354.amd64fre.win7sp1_ldr_escrow.190108-1700

DUMP_TYPE:  1

BUGCHECK_P1: 3

BUGCHECK_P2: fffff80003c582d0

BUGCHECK_P3: 0

BUGCHECK_P4: 6c734d46020c0008

BUGCHECK_STR:  0x19_3

DEFAULT_BUCKET_ID:  WIN7_DRIVER_FAULT

CURRENT_IRQL:  2

ANALYSIS_VERSION: 10.0.17763.1 amd64fre

LAST_CONTROL_TRANSFER:  from fffff80003c38253 to fffff80003af0ba0

STACK_TEXT:  
fffff880`0c281778 fffff800`03c38253 : 00000000`00000019 00000000`00000003 fffff800`03c582d0 00000000`00000000 : nt!KeBugCheckEx
fffff880`0c281780 fffff800`03a9dc55 : 00000000`00000000 00000000`0000000c 00000000`00000001 fffffa80`00000000 : nt!ExFreePool+0x4fb
fffff880`0c281870 fffff800`03f1615f : fffffa80`06dfb810 00000000`00000000 fffffa80`06de9070 fffff880`03300180 : nt!ExAllocatePoolWithQuotaTag+0x55
fffff880`0c2818c0 fffff800`03da7cc6 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!IopXxxControlFile+0xadf
fffff880`0c281a00 fffff800`03afebd3 : fffffa80`06e256d0 00000000`1c60e288 fffff880`0c281a88 00000000`1c60dd78 : nt!NtDeviceIoControlFile+0x56
fffff880`0c281a70 00000000`77b098fa : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x13
00000000`1c60dd28 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : 0x77b098fa


THREAD_SHA1_HASH_MOD_FUNC:  b66b709f62a2d09a97da274ebc94b18351cb84bc

THREAD_SHA1_HASH_MOD_FUNC_OFFSET:  48cf334b3ecd1316141b830817191d5780668ba8

THREAD_SHA1_HASH_MOD:  ee8fcf1fb60cb6e3e2f60ddbed2ec02b5748a693

FOLLOWUP_IP: 
nt!ExFreePool+4fb
fffff800`03c38253 cc              int     3

FAULT_INSTR_CODE:  d634ccc

SYMBOL_STACK_INDEX:  1

SYMBOL_NAME:  nt!ExFreePool+4fb

FOLLOWUP_NAME:  Pool_corruption

IMAGE_NAME:  Pool_Corruption

DEBUG_FLR_IMAGE_TIMESTAMP:  0

IMAGE_VERSION:  6.1.7601.24354

MODULE_NAME: Pool_Corruption

STACK_COMMAND:  .thread ; .cxr ; kb

FAILURE_BUCKET_ID:  X64_0x19_3_nt!ExFreePool+4fb

BUCKET_ID:  X64_0x19_3_nt!ExFreePool+4fb

PRIMARY_PROBLEM_CLASS:  X64_0x19_3_nt!ExFreePool+4fb

TARGET_TIME:  2019-03-04T00:20:12.000Z

OSBUILD:  7601

OSSERVICEPACK:  1000

SERVICEPACK_NUMBER: 0

OS_REVISION: 0

SUITE_MASK:  272

PRODUCT_TYPE:  1

OSPLATFORM_TYPE:  x64

OSNAME:  Windows 7

OSEDITION:  Windows 7 WinNt (Service Pack 1) TerminalServer SingleUserTS

OS_LOCALE:  

USER_LCID:  0

OSBUILD_TIMESTAMP:  2019-01-09 03:35:55

BUILDDATESTAMP_STR:  190108-1700

BUILDLAB_STR:  win7sp1_ldr_escrow

BUILDOSVER_STR:  6.1.7601.24354.amd64fre.win7sp1_ldr_escrow.190108-1700

ANALYSIS_SESSION_ELAPSED_TIME:  904

ANALYSIS_SOURCE:  KM

FAILURE_ID_HASH_STRING:  km:x64_0x19_3_nt!exfreepool+4fb

FAILURE_ID_HASH:  {508e5570-3f70-aa7e-0a8b-e9a016213682}

Followup:     Pool_corruption
---------

1: kd> !pool fffff80003c582d0
unable to get nt!ExpHeapBackedPoolEnabledState
Pool page fffff80003c582d0 region is Unknown
*fffff80003c58000 size:  c50 previous size:    0  (Allocated) *.... (Protected)
		Owning component : Unknown (update pooltag.txt)

fffff80003c58c50 doesn't look like a valid small pool allocation, checking to see
if the entire page is actually part of a large page allocation...

fffff80003c58c50 is not a valid large pool allocation, checking large session pool...
fffff80003c58c50 is not valid pool. Checking for freed (or corrupt) pool
Bad allocation size @fffff80003c58c50, zero is invalid

Is deleting the RtlFreeUnicodeString the ONLY change you made? There are several possible problems here. Remember, Scott advised you to make a copy of the string. If you’re just copying the pointer to the string, you have no guarantee that the pointer is going to remain valid. If the process table changes, it’s quite possible that pointer could now be pointing into random memory.

Second, where are you copying the string to? What’s the definition of FullImageName in IMAGE_CALLBACK_INFO2? Is that a fixed-length array? You aren’t checking that there is enough room in that array before doing your copy. Or, even worse, is it defines as a wchar_t *? Because that means you are using a user-mode address without any validation, and that’s always dangerous.

@Tim_Roberts said:
Is deleting the RtlFreeUnicodeString the ONLY change you made?
Initially that was the only thing I did, hence the !analyze output above. I realised my mistake though and used RtlInitUnicodeString(&info->FullImageName, FullImageName->Buffer);. Is RtlInitUnicodeString enough to copy a string or should I allocate a buffer with ExAllocatePoolWithTag? I tried the former and freed it with RtlFreeUnicodeString(&info->FullImageName); just to end up with another corrupt pool crash.

@Tim_Roberts said:
Second, where are you copying the string to? What’s the definition of FullImageName in IMAGE_CALLBACK_INFO2? Is that a fixed-length array? You aren’t checking that there is enough room in that array before doing your copy. Or, even worse, is it defines as a wchar_t *? Because that means you are using a user-mode address without any validation, and that’s always dangerous.

typedef struct _ImageCallbackInfo2
{
	UNICODE_STRING FullImageName;
	HANDLE ProcessId;
	SINGLE_LIST_ENTRY LinkField;
} IMAGE_CALLBACK_INFO2, *PIMAGE_CALLBACK_INFO2;

You need to actually allocate a buffer so that you can free it. Something like:

dest.Length = 0;
dest.MaximumLength = src->Length;
dest.Buffer = ExAllocatePoolWithTag(src.Length);

RtlCopyUnicodeString(&dest, src);

You should then free dest.Buffer with ExFreePool.

user555 wrote:

typedef struct _ImageCallbackInfo2 {
UNICODE_STRING FullImageName;
HANDLE ProcessId;
SINGLE_LIST_ENTRY LinkField;
} IMAGE_CALLBACK_INFO2, *PIMAGE_CALLBACK_INFO2;

OK, but you’re passing an IMAGE_CALLBACK_INFO structure from your
user-mode app, and that also contains a UNICODE_STRING field, which
contains a pointer.  Who is setting that up?  What kind of a pointer are
you going to get?  If the user-mode app is setting up that structure
with a pointer to a user-mode  buffer, then you have a problem.  You
can’t allow your kernel driver to use unprotected user-mode addresses
without validation.  And, of course, you can’t hand a kernel-mode
pointer back to a user-mode app.

The better solution would be:

    typedef struct {
        HANDLE ProcessId;
        wchar_t FullImageName[1024];
    } IMAGE_CALLBACK_INFO;

Now, you don’t have any pointers at all.  The memory access is all
handled by the I/O manager, so it is known to be valid when you get into
your ioctl handler.  You’ll want to make sure the image name fits before
doing the copy, and you can return a STATUS_BUFFER_OVERFLOW to tell the
app to hand you a bigger buffer.