Fix xHCI heap corruption: allocate the Output Device Context full size at Address Device, never swap it (found at O-C)

xhci_cmd_address_device() sized the Output Device Context for Slot + EP0
(64 bytes); xhci_cmd_configure_endpoint() then allocated a bigger copy and
repointed DCBAA[slot_id]. The controller keeps the address it was given at
Address Device (QEMU caches it), so every bulk Endpoint Context was written
past the 64-byte block into the next kmalloc header -- on every USB
mass-storage attach, every ISA. After O-C's smoke campaign reshuffled the
heap, a kfree at thumbdrive unplug walked into it: #GP in coalesce_neighbors().

Found with temporary instrumentation (load base, code bytes at RIP, event
crumbs, a heap-list checker): the corrupt word was an EP Context (Bulk IN,
Running, MPS 1024) exactly DCI 3 x 32 bytes into the 64-byte context.

Now: XHCI_DEVICE_CTX_BYTES (32 contexts x 32 bytes) allocated once per
slot at Address Device, zeroed whole, never reallocated; Configure
Endpoint just fills it. Also ends the per-attach leak of the old context.

Verified: heap checker clean from boot; campaign -> O-A -> MINT -> unplug
-> re-attach passes with 0 exceptions; 3-ISA acceptance passes, parity and
dict_hash set unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-25 10:57:43 -04:00
co-authored by Claude Opus 5.5
parent ee4c401d1c
commit df150e8626
28 changed files with 110993 additions and 24 deletions
+22 -23
View File
@@ -489,9 +489,18 @@ int xhci_cmd_address_device(xhci_dev_t *dev, uint32_t slot_id,
sizeof(xhci_ep_ctx32_t), 64);
if (!ms->input_ctx) return -1;
}
/* FABRIC-3.9.8 (found at Stage O-C, 2026-09-25): the Output Device
* Context is allocated FULL SIZE here, once -- Slot Context plus all 31
* Endpoint Contexts (XHCI_DEVICE_CTX_BYTES). It used to be sized for
* Slot + EP0 only (64 bytes) and swapped for a bigger one in
* xhci_cmd_configure_endpoint(), repointing DCBAA[slot_id]. But the
* controller takes the Output Device Context's address at Address
* Device and keeps writing there (QEMU caches it; it does not re-read
* DCBAA), so the bulk endpoints' contexts landed past the end of the
* 64-byte block -- corrupting the next kmalloc header on every mass
* storage attach, until a later kfree walked into it and #GP'd. */
if (!ms->device_ctx) {
ms->device_ctx = kmalloc_aligned(
sizeof(xhci_slot_ctx32_t) + sizeof(xhci_ep_ctx32_t), 64);
ms->device_ctx = kmalloc_aligned(XHCI_DEVICE_CTX_BYTES, 64);
if (!ms->device_ctx) return -1;
}
if (!ms->ep0_ring) {
@@ -522,7 +531,7 @@ int xhci_cmd_address_device(xhci_dev_t *dev, uint32_t slot_id,
* controller writes this on Address Device success, software must not
* pre-fill it. */
uint8_t *dctx = (uint8_t *)ms->device_ctx;
for (size_t i = 0; i < sizeof(xhci_slot_ctx32_t) + sizeof(xhci_ep_ctx32_t); i++) dctx[i] = 0;
for (size_t i = 0; i < XHCI_DEVICE_CTX_BYTES; i++) dctx[i] = 0;
((uint64_t *)dev->dcbaa)[slot_id] = (uint64_t)(uintptr_t)ms->device_ctx;
/* Input Context: Input Control Context, then Slot Context, then EP0
@@ -602,26 +611,16 @@ int xhci_cmd_configure_endpoint(xhci_dev_t *dev, uint32_t slot_id)
uint32_t out_dci = XHCI_EP_ADDR_TO_DCI(ms->bulk_out_ep_addr);
uint32_t max_dci = (in_dci > out_dci) ? in_dci : out_dci;
/* DCBAA[slot_id]'s Device Context also needs to grow to hold the new
* EP Contexts -- it's currently sized for Slot+EP0 only (from Address
* Device). Unlike the Input Context below, this one's *existing*
* content must be preserved, not zeroed: xHCI 1.2 spec section 4.6.6
* only has the controller write the DCIs actually named in this
* command's Add/Drop flags (EP0's entry here is neither), so it
* expects to find EP0's live output state (its current TR Dequeue
* Pointer in particular) still intact in the Device Context it reads
* -- swapping in a freshly zeroed buffer would hand the controller a
* blank EP0 out from under an endpoint it isn't being asked to touch. */
size_t old_device_ctx_bytes = sizeof(xhci_slot_ctx32_t) + sizeof(xhci_ep_ctx32_t);
size_t new_device_ctx_bytes = (1 + (size_t)max_dci) * sizeof(xhci_ep_ctx32_t);
void *new_device_ctx = kmalloc_aligned(new_device_ctx_bytes, 64);
if (!new_device_ctx) return -1;
uint8_t *ndctx = (uint8_t *)new_device_ctx;
for (size_t i = 0; i < new_device_ctx_bytes; i++) ndctx[i] = 0;
const uint8_t *odctx = (const uint8_t *)ms->device_ctx;
for (size_t i = 0; i < old_device_ctx_bytes; i++) ndctx[i] = odctx[i];
ms->device_ctx = new_device_ctx;
((uint64_t *)dev->dcbaa)[slot_id] = (uint64_t)(uintptr_t)ms->device_ctx;
/* The Output Device Context is NOT grown or swapped here (FABRIC-3.9.8,
* found at Stage O-C): xhci_cmd_address_device() allocated it full size
* (XHCI_DEVICE_CTX_BYTES, every DCI) and pointed DCBAA[slot_id] at it,
* and the controller keeps using that address. This used to allocate a
* bigger copy and repoint DCBAA -- the controller never saw the swap,
* wrote the new bulk EP Contexts past the end of the original 64-byte
* block, and corrupted the heap. Every DCI up to max_dci already has
* room; EP0's live output state is left exactly where the controller
* put it. */
if (max_dci >= XHCI_DEVICE_CTX_ENTRIES) return -1;
/* ms->input_ctx is reused from Address Device -- same allocation
* (96 bytes: Input Control Ctx + Slot Ctx + one EP Ctx) is too small