Read a big-pool allocation's tag from the table that holds it - #180
Merged
Merged
Conversation
`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
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 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. Comment |
This was referenced Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ExAllocatePoolWithTagsends anything that will not fit inside a page toExpAllocateBigPool,which takes whole pages from the segment allocator and records the caller's tag and length in
nt!PoolBigPageTableinstead of in a_POOL_HEADER. Nothing in the page range descriptorseparates one from a plain page-range allocation — both are
RangeFlags0x03— so the walkerdecoded 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 0x10short.
Measured on a live 26100.33438 kernel:
!poolffffac09dd0f5000CM25, 0x1000 bytes..N., 4080 bytes at+0x10..N.is0x00004e00, out of the registry hive bin's ownhbinheader. Where those bytes happenedto be zero the tag came out
0x00000000, which is how this presented: as tens of thousands ofuntagged allocations.
Two more defects in the same lookup
Both read out of
nt(x64 26100.33438), not inferred:Vais the free flag, and the address stays behind it.ExpRemoveTagForBigPagesfrees an entry with
lock inc qword ptr [rax], so a released allocation stays underaddress | 1with its old tag and size until something claims the slot. Matching onVa & !1therefore answered for freed pool. 2,300 of this kernel's 32,768 slots were in that state.
nt's own comparison iscmp rcx,rdi— exact — and so is this one now.Va == 0; a never-used slot reads1.Every miss scanned all 32,768 entries. It stops at a never-used slot now, which is sound because
ExpAddTagForBigPagesclaims the first bit-0-set slot from the hash and so cannot have walkedpast 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:....bulkCM25(4,013 allocations / 19.0 MB),CM16,EtwB,Gpbm,Obtb,ClfI,CM29,DxgKandPool— the big-page table's own 1 MB allocation — all appear correctly tagged where they had beenscattered across bogus tags. One of those bogus tags was
0x838bffff: the top half of a kernelpointer.
Tests
Four new unit tests, each mutation-verified by breaking the rule it pins and checking it fails:
address | 1) is walked past to the live one1) ends the chain, in one batch rather than a scan of the tableThe 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.mditem 99's third row survives, and this run says what it is rather than guessing: thebig-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