bfs.sys BfsInsertDirectoryEntry name length overrun

bfs.sys is the Boot File System driver, reached whenever the OS needs to read or modify the blob-based boot configuration store — boot and servicing tooling, image deployment, virtual-machine setup, recovery media preparation. Anything that lets a user point the driver at a store they crafted is an entry point, and every on-disk structure the driver parses is attacker-controlled at that point.

A directory entry in bfs.sys lives in a fixed-size slot of 0x214 bytes inside a directory page; the per-page bitmap that RtlFindClearBits walks to find a free slot manages thirty of them. The slot’s variable-length name lives at slot->Name /* +0x1c */. The vulnerable build reserved the slot, zeroed its body, and memcpy’d the caller-supplied NameDesc->Length straight into slot->Name, with no upper bound on the length:

// BfsInsertDirectoryEntry - vulnerable build
// The slot is reserved in the bitmap, but the name is taken straight
// from the caller's descriptor with no width check, so the bound is
// NameDesc->Length, not the destination width.
memset(&slot->Type, 0, 0x210);                            /* +0x0c - zero the entry body */
slot->Type   = Type;                                      /* +0x0c */
slot->Parent = Parent;                                    /* +0x08 */
memcpy(slot->Name, NameDesc->Buffer, NameDesc->Length);   /* +0x1c - caller length, NO BOUND */
slot->Flags  = Flags;                                     /* +0x10 */

NameDesc->Length is the length field out of the caller’s directory-entry descriptor, which on the parse path is the on-disk store, so a name longer than the slot’s name field overruns it, runs into the next directory entry in the page, and ultimately into pool metadata. The destination width was never the authority; the caller’s claimed length was. This is the classic memcpy(dst, src, attacker_len) into a fixed-size destination, the kind of pool overflow that still ships because the length looks constrained by an outer layer that does not actually enforce it.

The patch

The patch gates the whole insert on the name actually fitting the slot’s name field:

// BfsInsertDirectoryEntry - patched
// The name must fit, or the insert is rejected before a single byte
// is written into the slot.
if (NameDesc->Length <= 0x1fe) {
    memset(&slot->Type, 0, 0x210);                         /* +0x0c - zero the entry body */
    slot->Type   = Type;                                   /* +0x0c */
    slot->Parent = Parent;                                 /* +0x08 */
    memcpy(slot->Name, NameDesc->Buffer, NameDesc->Length); /* +0x1c - now bounded */
    slot->Flags  = Flags;                                  /* +0x10 */
    /* ...allocate the entry's data block and write it out... */
} else {
    Status = 0xC0000106;                                   /* name too long for the slot */
    EtwWriteTransfer(...);
    RtlClearBits(&BitMapHeader, SlotIndex, 1);             /* release the reserved slot */
    return NULL;
}

The destination size is the authority now, not the caller’s length. The check sits right after the slot is reserved in the bitmap and before any byte is written into it; on rejection the reservation is cleared and the function returns NULL.

Attack path

flowchart TD
    A["crafted bfs store with a long-named directory entry"] --> B["BfsInsertDirectoryEntry"]
    B --> C["no length gate, slot reserved, body zeroed"]
    C --> D["memcpy NameDesc.Length bytes into slot.Name at +0x1c"]
    D --> E["length over 0x1fe overruns the slot into the next entry and pool metadata"]

memcpy(dst, src, attacker_len) into a fixed-size destination needs an explicit upper bound, and the destination size has to be the authority — the caller’s claimed length is only acceptable after it has been checked against that authority. The smell is the same everywhere this pattern appears: a length field that appears to be constrained by an outer layer, used directly as the copy bound without the destination-width check that has to back it.