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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5650 Β· 12 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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:

  1. 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=%u line. Two identical runs (run.log, transcript re-run).
  2. Kernel page fault: with the table ending exactly on a page boundary, the same field read crossed past the one-page pmap_mapdev mapping and took Fatal trap 12 mid-boot at the literal gate instruction testb $0x1,0x1c(%r12) (= mem->mem.Flags & ACPI_SRAT_MEM_ENABLED, packed Flags u32@+28). Two identical runs (first capture in session transcript, run.fault.log after 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)

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 header 01 30 at +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 shows CPU Topology (line 85) then Fatal trap 12 (line 88) and Stopped 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 (od in session transcript); blob survival to kernel time proven by DF-5649's wedge_page.bin / kdump_srat.bin dumps (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.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

Same 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/
↓ fix.diffper-fix-DF-5650

Confirmed kernel references

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.