β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5649

SRAT subtable walk advances by firmware Length with no lower bound: zero-length entry wedges srat_probe in an infinite loop (silent boot hang)

Summary

The walk advances by the firmware-supplied one-byte Length with no Length>=sizeof(header) check: a Length==0 entry spins forever at SI_BOOT2_NUMA - boot stops silently right after CPU Topology with no panic (vm_page: avg never appears). Verified 3x on the stock kernel; wedge-time QEMU pmemsave shows the walker iterating the exact crafted bytes. The MADT sibling validates exactly this (madt_iterate_entries) - the SRAT parser never got the check. madt-style extent fix validated on a rebuilt kernel (entry rejected with a message, boot proceeds).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5649 Β· 14 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
DECODE_LOG.md β€” 4.1 KB ↓ raw
gen_srat.py β€” 15.1 KB view raw
build.sh β€” 98 B view raw
run.sh β€” 164 B view raw
run.log β€” 7.6 KB view raw
run.2.log β€” 194.5 KB view raw
control_baseline.log β€” 18.4 KB view raw
wedge_page.bin β€” 4.0 KB ↓ download
kdump_srat.bin β€” 4.0 KB ↓ download
fixed_hang.log β€” 18.3 KB view raw
fix.diff β€” 2.1 KB view raw
guest_env.txt β€” 580 B view raw

DF-5649 β€” SRAT zero-length subtable: infinite loop in srat_probe (silent boot hang)

What

sys/platform/pc64/acpica/acpi_srat.c β€” the SRAT entry walk advances by the firmware-supplied one-byte mem->head.Length with no lower bound:

for (mem = (srat_entry_t *)(srat + 1);                       /* :93 */
     (char *)mem < (char *)srat + srat->Header.Length;
     mem = (srat_entry_t *)((char *)mem + mem->head.Length)) /* :95 */

A crafted SRAT containing an entry with Length == 0 (and Type != ACPI_SRAT_TYPE_MEMORY_AFFINITY) makes the increment add zero forever: srat_probe spins silently at SI_BOOT2_NUMA (0x1a5c000) β€” the boot stops between "CPU Topology: ..." (0x1a58000) and everything after, with no panic, no message, and no watchdog. The same defect exists in the inner CPU-affinity rescan (acpi_srat.c:114-117). The MADT sibling validates exactly this (madt_iterate_entries, acpi_madt.c:156-167: ent->Length < sizeof(*ent) β†’ reject); the SRAT parser simply never got the check.

Rig (real kernel-boot manifestation)

Planted-ACPI chain, DF-5588/DF-5430 family vector: loader.conf hint.acpi.0.rsdp=0x40000000 (accepted by sdt_probe, acpi_sdt.c:71, no validation) + QEMU -device loader,file=blob,addr=0x40000000,force-raw=on. The blob carries RSDP(rev2)→XSDT→{MADT(control content so SMP still boots), SRAT(scenario)}. Two rig lessons cost us hours and are written down in DECODE_LOG.md:

  1. The blob must survive until SI_BOOT2_NUMA β€” MADT rigs get away with free RAM because MADT is consumed at SI_BOOT2_PRESMP; 0x40000000 worked reliably here, the SMAP-reserved hole at 0xbffdd000 is zeroed by SeaBIOS.
  2. ACPICA table structs are #pragma pack(1) (actbl3.h:194) β€” see DECODE_LOG.md before crafting entries.

Build

python3 gen_srat.py hang /tmp/srat_hang.bin

Run

qemu-system-x86_64 -enable-kvm -m 4096 -smp 6 \
  -drive file=dfbsd-qemu/disk.qcow2,if=virtio,format=qcow2 \
  -device loader,file=/tmp/srat_hang.bin,addr=0x40000000,force-raw=on \
  -netdev user,id=n0 -device virtio-net-pci,netdev=n0 \
  -display none -monitor unix:/tmp/m.sock,server,nowait \
  -serial file:run.log
# (guest loader.conf must contain hint.acpi.0.rsdp=0x40000000)

Expected output (see run.log / run.2.log)

...
Bits within APICID: logical_CPU_bits: 0; core_bits: 3
CPU Topology: cores_per_chip: 6; threads_per_core: 1; chips_per_package: 1;
<serial output stops here forever β€” the kernel is spinning in srat_probe>

Control (scenario control, well-formed entries) prints vm_page: avg ... pages per queue (from vm_numa_organize_finalize, vm_page.c:673) within ~1s of the "CPU Topology" line and the boot continues. The hang run never emits it β€” proof the walk never terminates.

Proof the walker is on our bytes

While the guest was wedged, pmemsave 1073741824 4096 wedge_page.bin (QEMU monitor) shows the SRAT region intact with the crafted 00 10 (cpu entry) at +48 and the 00 00 (Type=0, Length=0) entry at +64 β€” the exact bytes the infinite loop is iterating (od in VERDICT.md).

Fix validation

fix.diff (mirror of madt_iterate_entries checks) applied to the guest's /usr/src, kernel rebuilt (make nativekernel), re-run with the same blob:

srat_probe: invalid SRAT entry len 0      <- new rejection message
vm_page: avg 893 pages per queue, 1020 queues   <- walk terminated
(boot proceeds; see fixed_hang.log)
VERDICT.md
↓ download raw

DF-5649 VERDICT β€” REPRODUCED (boot-time DoS, hang)

1. Bottom line

A crafted SRAT with a zero-length subtable entry wedges the kernel in an infinite loop inside srat_probe() (sys/platform/pc64/acpica/acpi_srat.c:95 outer walk, :117 inner CPU rescan) at SI_BOOT2_NUMA. The boot silently stops between the SI_BOOT2_CPU_TOPOLOGY messages and everything after; there is no panic, no console output, and no way to progress. Reproduced 3 times on the stock Jul-2 with-src kernel; gone (clean rejection message, boot proceeds) on a kernel rebuilt with fix.diff.

2. Root cause (path:line)

Threat model: firmware/loader-stage attacker (planted RSDP chain, malicious VM host, or compromised bootloader config) β€” identical to the DF-5430..5433/5583/5588 family. No unprivileged in-guest path; severity Low per family calibration (boot-time DoS brick).

3. Reproduction evidence

  • run.log (= run2_hang.log) and run.2.log (independent boots): serial output ends at CPU Topology: cores_per_chip: 6; ... (line 85) and never advances; no vm_page: avg line; no login. QEMU stays alive burning CPU.
  • control_baseline.log: identical rig with a well-formed SRAT β€” vm_page: avg 893 pages per queue, 1020 queues (line 86, ~1s after CPU Topology) and the boot continues to the (rig-artifact) DSDT trap at AcpiTbLoadNamespace.
  • wedge_page.bin: physical memory dump taken via the QEMU monitor while wedged. Bytes at +0x430: 00 10 00 00 01 00 00 00 ... (cpu affinity entry, Length=16) and at +0x440: 00 00 ... β€” the crafted zero-length entry the loop is spinning on: 000430 00 10 00 00 01 00 00 00 00 00 00 00 00 00 00 00 000440 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  • Trigger table (gen_srat.py scenario hang): SRAT Length=66 = 48-byte header + 16-byte cpu entry + 2-byte {Type=0, Length=0}.

4. Fix validation (baseline vs patched)

fix.diff applied inside the guest (/usr/src/sys/platform/pc64/acpica/ acpi_srat.c, md5 c289f1c2bbea4ab3369eace0ae0c72ff), kernel rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC (BUILD_OK), installed, same blob re-run:

baseline (stock) patched
serial after "CPU Topology" nothing, forever srat_probe: invalid SRAT entry len 0
vm_page: avg line never line 87
boot proceeds past SI_BOOT2_NUMA no yes (to the rig-artifact DSDT trap, same as control)

(fixed_hang.log lines 85-87 and 352.)

Note: the Fatal trap 12 at line ~352 of the patched run is AcpiTbLoadNamespace tripping on the DSDT-less planted table chain β€” a rig artifact present in EVERY boot with this rig (including the healthy control, control_baseline.log line 351); it occurs long after SI_BOOT2_NUMA and is unrelated to the SRAT fix.

5. Rig hardening notes (see DECODE_LOG.md)

  • The blob must live at a physical address stable through SI_BOOT2_NUMA; 0x40000000 worked 10+/10 times, the SMAP-reserved hole 0xbffdd000 is zeroed by SeaBIOS before the loader runs.
  • ACPICA SRAT structs are packed (actbl3.h:194 #pragma pack(1)): MEM_AFFINITY = Header(2) + Prox u32@2 + Rsvd u16@6 + Base u64@8 + Length u64@16 + Rsvd u32@24 + Flags u32@28, size 40. Entry-crafting code must use these offsets (DECODE_LOG.md documents the empirical decode that proved it, including the faulting instruction testb $0x1,0x1c(%r12) = mem->mem.Flags & ACPI_SRAT_MEM_ENABLED).

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.md
↓ download raw

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 loader at reset and observed intact from SeaBIOS through SI_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)

  1. 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.
  2. 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).
  3. 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 is mem->mem.Flags (u32@28) & bit0.
  4. 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.rsdp survives only in the loader.conf of the active qcow2 layer; vm.sh reset with-src reverts 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.rsdp before boot (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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src (md5 c289f1c2bbea4ab3369eace0ae0c72ff), kernel rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC (BUILD_OK) and installed. Re-run of the exact hang blob: 'srat_probe: invalid SRAT entry len 0' then 'vm_page: avg 893 pages per queue' and the boot proceeds past SI_BOOT2_NUMA to the rig's normal endpoint - the wedge is gone. The patched-run Fatal trap at AcpiTbLoadNamespace (fixed_hang.log line 352) is the DSDT-less planted-chain rig artifact present in every healthy boot of this rig (control_baseline.log line 351), unrelated to the SRAT fix.

fix.diff (git apply --check passes vs read-only sys/); fixed_hang.log lines 85-87 vs run.log; guest /tmp/build.log tail 'BUILD_OK' captured in session transcript
↓ fix.diffper-fix-DF-5649

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log/run.2.log: serial ends at 'CPU Topology: cores_per_chip: 6' with no 'vm_page: avg' (compare control_baseline.log line 86). wedge_page.bin: +0x430 '00 10' cpu entry, +0x440 '00 00' zero-length entry the loop spins on. fixed_hang.log: 'srat_probe: invalid SRAT entry len 0' + 'vm_page: avg' + boot proceeds under fix.diff kernel.

PoC changes

No seed PoC. gen_srat.py authored from scratch (RSDP rev2 -> XSDT -> {control MADT + crafted SRAT}). Two rig pitfalls solved and documented in DECODE_LOG.md: blob address must survive to SI_BOOT2_NUMA (0x40000000 works; the 0xbffdd000 SMAP hole is zeroed by SeaBIOS), and ACPICA SRAT structs are #pragma pack(1) (actbl3.h:194) so entry fields sit at spec offsets (Prox@2, Base@8, Length@16, Flags@28), not C-aligned ones.

Verified recommended fix

Mirror madt_iterate_entries: reject entries with Length < sizeof(ACPI_SUBTABLE_HEADER) or extending past the table end before touching any field (fix.diff, validated by rebuild).

Verdict

Crafted SRAT with a zero-length subtable entry wedges srat_probe() in an infinite loop at SI_BOOT2_NUMA (acpi_srat.c:95 advance adds firmware Length=0 forever; same at :117 inner walk). Boot output stops permanently right after the SI_BOOT2_CPU_TOPOLOGY messages with no panic and no further progress; vm_numa_organize_finalize (whose 'vm_page: avg' kprintf appears ~1s later on healthy boots) never runs. Reproduced 3 times on the stock Jul-2 kernel via the planted-RSDP chain (hint.acpi.0.rsdp + QEMU -device loader blob); wedge-time pmemsave (wedge_page.bin) shows the walker iterating the exact crafted bytes. The MADT sibling validates exactly this (acpi_madt.c:156-167); the SRAT parser lacks the check. Firmware/loader-stage vector only (DF-5588 family) - no unprivileged in-guest path, hence Low severity per family calibration.