SRAT walk performs no entry-extent or per-type length validation: affinity fields read (and printed) past the declared table end β console/dmesg disclosure of adjacent physical memory, and kernel page fault when the read crosses the mapdev page
Summary
The walk condition bounds only the entry's FIRST byte while the parser reads packed fields up to +40 bytes past (Flags u32@+28 etc.) and kprintf's them: truncated-entry repro printed marker bytes planted past the table end ('MemAffinity deadbeefcafe0123 ... Prox=1397899604') to console/dmesg; the page-boundary variant (declared length ending at 0x40001000) took Fatal trap 12 at the literal gate instruction testb $0x1,0x1c(%r12) - boot brick. Both reproduced 2x on the stock kernel, both eliminated by the fix kernel. Discovered en route: ACPICA SRAT structs are #pragma pack(1) (documented for future rigs). Firmware/loader-stage threat via the DF-5588 vector; disclosure of adjacent physical RAM, no write primitive.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5650 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| DECODE_LOG.md | β | 4.1 KB | β raw | |
| gen_srat.py | β | 15.1 KB | view raw | |
| build.sh | β | 142 B | view raw | |
| run.sh | β | 173 B | view raw | |
| run.fault.log | β | 8.3 KB | view raw | |
| control_baseline.log | β | 18.4 KB | view raw | |
| fixed_l3.log | β | 18.3 KB | view raw | |
| fixed_f3.log | β | 18.3 KB | view raw | |
| fix.diff | β | 2.1 KB | view raw | |
| env.txt | β | 580 B | view raw |
DF-5650 β SRAT walk lacks entry-extent validation: OOB read past the mapped table (console info leak + early-boot kernel page fault)
What
sys/platform/pc64/acpica/acpi_srat.c walks SRAT subtables checking only
(char *)mem < (char *)srat + srat->Header.Length (:94) β i.e. merely
that the first byte of the entry header is inside the table. It never
checks that the entry fits (entry->Length vs remaining bytes) nor that the
entry is long enough for the structure whose fields it then reads:
mem->mem.Flags(:101, packed u32@+28 of the entry),mem->mem.BaseAddress/Length/ProximityDomain(:104-107, +8/+16/+2),- inner scan
cpu->cpu.Flags/ProximityDomainLo/ApicId(:121-135).
A crafted MEMORY-affinity entry header (Type=1) parked at the last two
bytes of the table (declared Length ends right after the header) makes the
kernel read and kprintf firmware-adjacent physical memory up to 40 bytes
beyond the table's declared end β bytes that were never part of the table β
to the console/dmesg. When the table's declared length ends exactly on a
page boundary, the same read crosses past the pmap_mapdev mapping
(sdt_sdth_map, acpi_sdt.c:138-151, maps exactly
roundup(Length, PAGE_SIZE), fresh KVA per pmap.c:6192-6217) and takes a
kernel page fault during early boot (Fatal trap 12).
The MADT sibling validates all of this (madt_iterate_entries,
acpi_madt.c:151-167); the SRAT parser has none of it.
Rig
Same planted-ACPI chain as DF-5649 (loader kenv hint.acpi.0.rsdp +
QEMU -device loader). Read DECODE_LOG.md (in ../DF-5649/) first β
ACPICA SRAT structs are #pragma pack(1) (actbl3.h:194), so the packed
offsets are Prox@2 / Base@8 / Length@16 / Flags@28 (proven empirically;
the faulting instruction is the flags gate itself:
testb $0x1,0x1c(%r12)).
Build
python3 gen_srat.py leak3 /tmp/srat_l3.bin # console-leak flavor python3 gen_srat.py fault3 /tmp/srat_f3.bin # page-fault flavor
Run
./run.sh leak3 # or fault3 (see run.sh; hint must be in loader.conf)
Expected output
leak3 (see run.log) β every printed value comes from bytes planted past the declared table end (table Length=66, entry header at +64, markers at +66..+85):
MemAffinity deadbeefcafe0123,1177411667286MB Prox=1397899604 (not found) vm_page: avg 893 pages per queue, 1020 queues
fault3 (see run.fault.log) β table declared length ends on the 0x40001000 page boundary; the Flags read at entry+28 faults on unmapped KVA:
Fatal trap 12: page fault while in kernel mode ... Stopped at -0x7f3de188: testb $0x1,0x1c(%r12) db>
Fix validation
With fix.diff (shared with DF-5649) built into the guest kernel, both
bloots boot cleanly past SI_BOOT2_NUMA:
srat_probe: invalid SRAT entry len 48 <- entry rejected, never read vm_page: avg 893 pages per queue, 1020 queues
No marker disclosure (fixed_l3.log), no trap (fixed_f3.log).
DF-5650 VERDICT β REPRODUCED (OOB read: console info leak + boot-time kernel page fault)
1. Bottom line
The SRAT entry walk reads affinity-structure fields with no check that the
entry's declared Length fits inside the table or covers the structure.
Both observable consequences were reproduced live on the stock kernel:
- Content disclosure: bytes physically adjacent to the table (planted
markers at +0..+19 past the declared table end) were read by the kernel
and printed to the console/dmesg as the
MemAffinity %016jx,%ldMB Prox=%uline. Two identical runs (run.log, transcript re-run). - Kernel page fault: with the table ending exactly on a page boundary,
the same field read crossed past the one-page
pmap_mapdevmapping and tookFatal trap 12mid-boot at the literal gate instructiontestb $0x1,0x1c(%r12)(=mem->mem.Flags & ACPI_SRAT_MEM_ENABLED, packed Flags u32@+28). Two identical runs (first capture in session transcript,run.fault.logafter guest reset).
Both are gone on a kernel rebuilt with fix.diff (entries rejected with
srat_probe: invalid SRAT entry len 48 before any field read;
fixed_l3.log, fixed_f3.log).
2. Root cause (path:line)
sys/platform/pc64/acpica/acpi_srat.c:93-95β walk condition only bounds the entry's first byte; advance byhead.Length; noLength <= remainingcheck.sys/platform/pc64/acpica/acpi_srat.c:99-107βmem->mem.Flagsread (packed +28) andBaseAddress/Length/ProximityDomain(+8/+16/+2) read and printed with noLength >= sizeof(struct)check.sys/platform/pc64/acpica/acpi_srat.c:114-135β inner CPU scan readscpu->cpu.Flags/ProximityDomainLo/ApicIdwith the same absence of checks.sys/platform/pc64/acpica/acpi_sdt.c:138-151βsdt_sdth_mapmaps exactlyHeader.Lengthbytes (page-rounded); reads past it hit adjacent physical RAM (leak flavor) or unmapped KVA (fault flavor;pmap_mapdev_attrallocates fresh KVA,pmap.c:6192-6217).- Sibling contrast:
sys/platform/pc64/acpica/acpi_madt.c:151-167βmadt_iterate_entriesenforces header-minimum, remaining-extent, and per-type minimum lengths. None of this exists for SRAT.
3. Threat model / impact ceiling
Firmware/loader-stage attacker (planted RSDP chain β DF-5588 vector; malicious VM host; compromised loader config), same class as DF-5430..5433/5583. The disclosed bytes are physical memory adjacent to the (SRAM/ROM-resident) ACPI table β on real firmware, neighboring firmware tables/EBDA content; not kernel heap. The fault flavor is a deterministic boot DoS when the adjacency condition holds. No write primitive, no unprivileged in-guest path β Medium.
4. Reproduction evidence
run.log(leak3): declared SRAT Length=66; entry header01 30at +64; markers planted at table_end+0..3 (54 41 52 53β Prox=1397899604), +6..13 (deadbeefcafe0123β Base), +14..21 (1122334455667788β 1177411667286 MB). Every printed value originates past the declared table end.run.fault.log(fault3): declared Length=0x800 ending on the 0x40001000 boundary; serial showsCPU Topology(line 85) thenFatal trap 12(line 88) andStopped at -0x7f3de188: testb $0x1,0x1c(%r12)β the flags gate reading entry+28 = 26 bytes past the mapped page. Boot dies at SI_BOOT2_NUMA (current process 0, no APs started).- Marker bytes verified in the planted blob (
odin session transcript); blob survival to kernel time proven by DF-5649'swedge_page.bin/kdump_srat.bindumps (same placement).
5. Fix validation (baseline vs patched)
Same fix.diff as DF-5649 (adds header-minimum, remaining-extent, and
per-type minimum checks before any field read). Patched kernel
(make nativekernel, BUILD_OK, installed):
| flavor | baseline | patched |
|---|---|---|
| leak3 | MemAffinity deadbeefcafe0123,... (OOB bytes disclosed) |
srat_probe: invalid SRAT entry len 48, no disclosure |
| fault3 | Fatal trap 12 at testb $0x1,0x1c(%r12) in srat_probe |
srat_probe: invalid SRAT entry len 48, no trap |
Both patched runs proceed past SI_BOOT2_NUMA (vm_page: avg line 87) to
the rig's normal endpoint (the DSDT-less AcpiTbLoadNamespace trap at
line ~352 β rig artifact present in every healthy boot of this rig,
see control_baseline.log line 351).
6. Rig notes
See ../DF-5649/DECODE_LOG.md β packed struct offsets (the original
generator used aligned offsets and every crafted entry was silently
skipped), blob placement lifetime, and the boot-marker line map.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
DECODE_LOG β empirical notes for the crafted-SRAT rig (DF-5649/DF-5650)
Everything below was learned live on the audit QEMU guest (DragonFly 6.5-DEVELOPMENT, kernel built Thu Jul 2 06:02:54 2026, X86_64_GENERIC). Read this before touching the blob generator.
1. ACPICA SRAT structs are PACKED (#pragma pack(1))
sys/contrib/dev/acpica/source/include/actbl3.h:194 opens a #pragma
pack(1) region that contains all SRAT structures (closes at :1042). The
runtime layout (empirically decoded, see Β§3) is therefore the ACPI-spec
byte layout, NOT the C-naturally-aligned layout:
ACPI_SRAT_CPU_AFFINITY (16 bytes): Type u8@0, Length u8@1, ProximityDomainLo u8@2, ApicId u8@3, Flags u32@4, LocalSapicEid u8@8, ProximityDomainHi[3]@9, ClockDomain u32@12 ACPI_SRAT_MEM_AFFINITY (40 bytes): Type u8@0, Length u8@1, ProximityDomain u32@2, Reserved u16@6, BaseAddress u64@8, Length u64@16, Reserved1 u32@24, Flags u32@28, Reserved2 u64@32
Our first generator used naturally-aligned offsets (Prox@4, Base@16, Len@24, Flags@36, size 48) β every crafted MEMORY entry was then silently skipped (its "Flags" read hit our BaseAddress marker bytes, bit0 clear), while the walk still iterated and advanced correctly. This produced hours of apparent "silent parser" confusion. Fix: use the packed offsets above.
2. Blob placement
- 0x40000000 (free RAM per SMAP): planted by
-device loaderat reset and observed intact from SeaBIOS throughSI_SUB_CONFIGURE(pmemsave dumps at loader-menu time, wedge time, and DSDT-trap time all matched). 10+/10 reliable. - 0xbffdd000 (inside the SMAP type=02 hole below 1GB): ZEROED by SeaBIOS's own table/allocation zone before the loader runs. Do not use.
- The MADT family rigs (DF-5430..) only need the blob to survive to
SI_BOOT2_PRESMP; the SRAT parser runs later (SI_BOOT2_NUMA = 0x1a5c000), so "worked for MADT" proves nothing for SRAT.
3. How the packed layout was proven (transcript excerpts)
fireworks(one entry per Type 0..14, all-0xFF bodies) printed exactly one line:MemAffinity ffffffffffffffff,17592186044415MB Prox=4294967295 (not found)β the Type=1 entry; proves the print path works and the dispatch constant is 1.map(FF@[2,10) + markers 0x1111..@10, 0x2222..@18, 0x3333..@26, 0x4444..@34, 0x5555..@40):MemAffinity 111111111111ffff,2345624805922MB Prox=4294967295β Base@8 (bytes ff ff + 116), Length@16 (bytes 112 + 22*6 β 2345624805922 == 0x2222222222221111 >> 20 exactly), Prox@2 (FF FF FF FF).bitprobe(46 entries, Prox@2 = i, single FF byte at 2+i): exactly one entry printed β Prox=26, i.e. the FF byte at +28: the gate ismem->mem.Flags(u32@28) & bit0.- The faulting instruction of DF-5650's fault flavor is the gate itself:
Stopped at -0x7f3de188: testb $0x1,0x1c(%r12)βtestb $1, +28(%r12).
4. Boot-path markers around the bug (stock kernel, this rig)
line 85 CPU Topology: ... (SI_BOOT2_CPU_TOPOLOGY 0x1a58000) line 86 [MemAffinity lines] (srat_probe walk, SI_BOOT2_NUMA 0x1a5c000) line 87 vm_page: avg 893 pages per queue, 1020 queues (finalize) ... line 331 ACPI: RSDP/RSDT/APIC/SRAT table listing (acpi0, SI_SUB_CONFIGURE) line 351 Fatal trap 12 in AcpiTbLoadNamespace (rig artifact: no DSDT in the planted chain)
The line-351 trap is the normal END STATE of every healthy boot under this rig (the planted chain has no DSDT); it is reached ~2s after line 87. A wedged (hang) run stops at line 85 forever; a faulting (fault3) run traps at line 88 inside srat_probe instead.
5. Misc
hint.acpi.0.rsdpsurvives only in the loader.conf of the active qcow2 layer;vm.sh reset with-srcreverts it β re-append before planted runs.- Boots without the planted blob but with the hint fall back to the mptable
(8 phantom cores) and then WEDGE during mptable SMP bringup β no ssh.
To get ssh with the hint present, drive the loader prompt over a pipe
serial and
unset hint.acpi.0.rsdpbeforeboot(boot_rig.py). - HAMMER2 compresses /usr/src on the guest disk β kernel .rodata strings are findable in a raw disk dump, source file text is not.
Fix verification
fixedSame fix.diff as DF-5649 (entry-extent validation), kernel rebuilt in-guest (make -j6 nativekernel KERNCONF=X86_64_GENERIC, BUILD_OK) and installed. leak3 re-run: no MemAffinity disclosure, 'srat_probe: invalid SRAT entry len 48' instead. fault3 re-run: no Fatal trap in srat_probe, same rejection, boot proceeds past SI_BOOT2_NUMA. Both patched boots terminate at the rig-artifact AcpiTbLoadNamespace trap (present in every healthy boot of this rig, control_baseline.log line 351).
fixed_l3.log, fixed_f3.log (lines 85-87 rejection + finalize, line ~352 rig artifact) vs run.log / run.fault.log; fix.diff git apply --check passes vs read-only sys/
Confirmed kernel references
- sys/platform/pc64/acpica/acpi_srat.c:93
- sys/platform/pc64/acpica/acpi_srat.c:94
- sys/platform/pc64/acpica/acpi_srat.c:101
- sys/platform/pc64/acpica/acpi_srat.c:104
- sys/platform/pc64/acpica/acpi_srat.c:121
- sys/platform/pc64/acpica/acpi_sdt.c:138
- sys/platform/pc64/x86_64/pmap.c:6192
- sys/platform/pc64/acpica/acpi_madt.c:151
- sys/contrib/dev/acpica/source/include/actbl3.h:194
Detail
Evidence (decisive lines)
run.log: 'MemAffinity deadbeefcafe0123,1177411667286MB Prox=1397899604 (not found)' - every value is a marker planted past the declared SRAT Length (66). run.fault.log: 'Fatal trap 12: page fault while in kernel mode' + 'Stopped at -0x7f3de188: testb $0x1,0x1c(%r12)' (flags gate at packed entry+28 past the mapped page). fixed_l3.log/fixed_f3.log: 'srat_probe: invalid SRAT entry len 48' + vm_page finalize, boot proceeds.
PoC changes
No seed PoC. gen_srat.py authored from scratch. Key discovery (documented in DECODE_LOG.md, essential for future SRAT rigs): ACPICA SRAT structs are #pragma pack(1) - runtime offsets Prox@2/Base@8/Length@16/Flags@28 (proved by marker prints, a 46-entry single-FF-byte bisection, and the faulting instruction testb $0x1,0x1c). Initial generator used C-aligned offsets and its entries were silently skipped, which also masked the bug during the first control runs.
Verified recommended fix
Validate entry extents before any field read, mirroring madt_iterate_entries (acpi_madt.c:151-167): reject Length < sizeof(header), Length > remaining, and per-type minimums (shared fix.diff with DF-5649, validated by kernel rebuild).
Verdict
The SRAT walk (acpi_srat.c:93-107) never validates that an entry's declared Length fits inside the table or covers the structure whose fields it reads; only the entry's first byte is bounds-checked. Two live manifestations on the stock kernel: (1) leak3 - a Type=1 entry header parked at table_end-2 makes the kernel read Prox@+2/Base@+8/Length@+16/Flags@+28 (packed offsets, actbl3.h:194 #pragma pack(1)) entirely past the declared table end and kprintf them; planted markers (deadbeefcafe0123 etc.) appeared on the console as 'MemAffinity deadbeefcafe0123,1177411667286MB Prox=1397899604' - disclosure of table-adjacent physical memory to console/dmesg. (2) fault3 - with the declared table length ending exactly on a page boundary, the Flags read crossed past the one-page pmap_mapdev mapping (sdt_sdth_map maps exactly Header.Length page-rounded; pmap_mapdev_attr allocates fresh KVA) and took 'Fatal trap 12' mid-boot at the gate instruction itself ('Stopped at testb $0x1,0x1c(%r12)'). Both eliminated by fix.diff (entry-extent validation mirroring madt_iterate_entries acpi_madt.c:151-167): patched kernel rejects the entries with 'srat_probe: invalid SRAT entry len 48' and boots past SI_BOOT2_NUMA. Firmware/loader-stage vector (DF-5588 family); leaked bytes are firmware-table-adjacent physical RAM, not kernel heap; no write primitive - Medium.
No comments yet.