Skip to content

Read a big-pool allocation's tag from the table that holds it - #180

Merged
glslang merged 1 commit into
mainfrom
fix/big-pool-page-ranges-carry-no-pool-header
Sep 24, 2026
Merged

glslang merged 1 commit into
mainfrom
fix/big-pool-page-ranges-carry-no-pool-header

Conversation

@glslang

@glslang glslang commented Sep 24, 2026

Copy link
Copy Markdown
Owner

ExAllocatePoolWithTag sends anything that will not fit inside a page to ExpAllocateBigPool,
which takes whole pages from the segment allocator and records the caller's tag and length in
nt!PoolBigPageTable instead of in a _POOL_HEADER. Nothing in the page range descriptor
separates one from a plain page-range allocation — both are RangeFlags 0x03 — so the walker
decoded the page as though a header were there. That reads the caller's own first sixteen bytes as
PreviousSize/BlockSize/PoolType/PoolTag, and reports the block as starting 0x10 in and 0x10
short.

Measured on a live 26100.33438 kernel:

address !pool the walk
ffffac09dd0f5000 CM25, 0x1000 bytes ..N., 4080 bytes at +0x10

..N. is 0x00004e00, out of the registry hive bin's own hbin header. Where those bytes happened
to be zero the tag came out 0x00000000, which is how this presented: as tens of thousands of
untagged allocations.

Two more defects in the same lookup

Both read out of nt (x64 26100.33438), not inferred:

  • Bit 0 of Va is the free flag, and the address stays behind it. ExpRemoveTagForBigPages
    frees an entry with lock inc qword ptr [rax], so a released allocation stays under
    address | 1 with its old tag and size until something claims the slot. Matching on Va & !1
    therefore answered for freed pool. 2,300 of this kernel's 32,768 slots were in that state.
    nt's own comparison is cmp rcx,rdi — exact — and so is this one now.
  • The probe's stop condition never fired. It stopped at Va == 0; a never-used slot reads 1.
    Every miss scanned all 32,768 entries. It stops at a never-used slot now, which is sound because
    ExpAddTagForBigPages claims the first bit-0-set slot from the hash and so cannot have walked
    past one.

The table is now read a batch at a time and kept, which is what makes asking this question of
every page range affordable rather than of large allocations only.

Measured

The same census on ctf-vm (26100.33438, 12h uptime), before and after, minutes apart:

before after
distinct tags 5,665 1,501
.... bulk 35,920 allocations / 79,166,848 B 34,489 / 51,216,352 B
allocated chunks 643,884 of 739,932 643,331 of 739,888

CM25 (4,013 allocations / 19.0 MB), CM16, EtwB, Gpbm, Obtb, ClfI, CM29, DxgK and
Pool — the big-page table's own 1 MB allocation — all appear correctly tagged where they had been
scattered across bogus tags. One of those bogus tags was 0x838bffff: the top half of a kernel
pointer.

Tests

Four new unit tests, each mutation-verified by breaking the rule it pins and checking it fails:

  • a page range the table names carries the table's tag, no header, and the requested length
  • a freed entry (address | 1) is walked past to the live one
  • a never-used slot (1) ends the chain, in one batch rather than a scan of the table
  • a second lookup inside a batch already read costs no read

The third mutation initially passed — the fixture's zero-filled tail stopped the old code for
the wrong reason — so that fixture now occupies every slot.

What is left

FOLLOWUPS.md item 99's third row survives, and this run says what it is rather than guessing: the
big-page table also names allocations served out of a VS subsegment (ffff8b836d455000, MiPm,
0xa270, inside the 17-page VS range at page 0x53), those carry no pool header either, and the walk's
VS chain in those subsegments sits 0x10 early. Not fixed here — the chain offset is measured but not
explained, and fitting the tag to it would be a guess. Filed with the evidence.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MUhLUt9rB6zd42Y25btB3h

`ExAllocatePoolWithTag` sends anything that will not fit inside a page to
`ExpAllocateBigPool`, which takes whole pages from the segment allocator and
records the caller's tag and length in `nt!PoolBigPageTable` **instead of** in a
`_POOL_HEADER`. Nothing in the page range descriptor separates one from a plain
page-range allocation -- both are `RangeFlags` `0x03` -- so the walker decoded
the page as though a header were there, which reads the caller's own first
sixteen bytes as `PreviousSize`/`BlockSize`/`PoolType`/`PoolTag` and reports the
block as starting 0x10 in and 0x10 short.

Measured on a live 26100.33438 kernel: `!pool` calls `ffffac09dd0f5000` a
0x1000-byte `CM25` allocation and the walk called it a 4080-byte block at `+0x10`
tagged `..N.` -- the bytes of the registry hive bin's own `hbin` header. Where
those bytes happened to be zero the tag came out `0x00000000`, which is how this
presented: as tens of thousands of untagged allocations.

Two more defects in the same lookup, both read out of `nt`:

  * Bit 0 of `Va` is `POOL_BIG_TABLE_ENTRY_FREE`, and the address stays behind
    it -- `ExpRemoveTagForBigPages` frees an entry with `lock inc qword ptr
    [rax]`. Matching on `Va & !1` therefore answered for freed pool with the tag
    it used to have; 2,300 of this kernel's 32,768 slots were in that state.
    `nt`'s own comparison is `cmp rcx,rdi`, and so is this one now.

  * The probe stopped at `Va == 0`, and a never-used slot reads `1`. The stop
    never fired, so every miss scanned all 32,768 entries. It stops at a
    never-used slot now, which is sound because `ExpAddTagForBigPages` claims the
    first bit-0-set slot from the hash.

The table is read a batch at a time and kept, which is what makes asking this
question of every page range affordable rather than of large allocations only.

Live on `ctf-vm` (26100.33438, 12h uptime), the same census before and after:
distinct tags **5,665 -> 1,501**, and the `....` bulk 79,166,848 -> 51,216,352
bytes. `CM25` (19.0 MB), `CM16`, `EtwB`, `Gpbm`, `Obtb`, `ClfI`, `CM29`, `DxgK`
and `Pool` -- the big-page table's own 1 MB allocation -- all appear correctly
tagged where they had been scattered across bogus tags, one of which was
`0x838bffff`, the top half of a kernel pointer.

windbg-mcp FOLLOWUPS.md item 99.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhLUt9rB6zd42Y25btB3h
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Sep 24, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 2b8e5a38-64e0-42a9-971d-6982a53d87d0


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@glslang
glslang merged commit 6a4cade into main Sep 24, 2026
7 checks passed
@glslang
glslang deleted the fix/big-pool-page-ranges-carry-no-pool-header branch September 24, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant