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

sdt_probe() accepts hint.acpi.0.rsdp with zero validation: total-acceptance injection vector for the crafted-ACPI family (DF-5430..5433/5583) and one-line boot brick

Summary

The kenv branch maps whatever 64-bit value the hint holds and consumes it as an RSDP with NO validation - not even the 'RSD PTR ' signature the scan path checks: a deadbeef-signature RSDP with wrong checksum was accepted and its RSDT->MADT chain consumed (lapic_count=3 on a 6-vCPU guest), the same invalid RSDP plus DF-5430's bsp_omit MADT panicked madt_lapic_pass2, and hint=0x800000000000 with no blob panics immediately at the rsdp->Revision read (reserved PTE bit -> vm_fault: fault on nofault entry) - one loader.conf line bricks the boot. The entire planted-ACPI family (DF-5430/5431/5432/5433/5583) hangs off this unvalidated if. Bound+signature+revision validation with firmware-scan fallback validated on a rebuilt kernel (falls back cleanly, lapic_count=6, bsp panic gone). Cross-file: OsdEnvironment.c:72 AcpiOsGetRootPointer is a second unvalidated consumer of the same hint (live fault at AcpiTbLoadNamespace+0x36). Boot-owner trust boundary.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5588 Β· 17 files
FileTypeDescriptionSize
gen_rsdp_inject.py β€” 4.5 KB view raw
valid_control.bin β€” 768 B ↓ download
nosig_control.bin β€” 768 B ↓ download
nosig_bsp_omit.bin β€” 768 B ↓ download
build.sh β€” 92 B view raw
run.sh β€” 326 B view raw
run.nosig_control.boot.log β€” 15.3 KB view raw
run.nosig_bsp_omit.boot.log β€” 7.7 KB view raw
run.bogus_addr.panic.log β€” 1.1 KB view raw
panic.txt β€” 436 B view raw
fix_run.log β€” 18.5 KB view raw
fix_build.log β€” 980 B view raw
fix.diff β€” 1.2 KB view raw
VERDICT.md β€” 7.9 KB ↓ raw
env.txt β€” 963 B view raw
manifest.json β€” 1.7 KB view raw
verdict.json β€” 6.0 KB view raw
VERDICT.md
↓ download raw

DF-5588 VERDICT β€” kenv RSDP acceptance in sdt_probe (pass 2)

Status: REPRODUCED (certain). Impact: panic (boot). File: sys/platform/pc64/acpica/acpi_sdt.c:71-77, deref at :109-115.

1. Root cause

sdt_probe() (SYSINIT SI_BOOT2_PRESMP SI_ORDER_FIRST, acpi_sdt.c:63-118) has three RSDP discovery paths:

  1. kenv hint (acpi_sdt.c:71-77): kgetenv_ulong("hint.acpi.0.rsdp") β†’ pmap_mapdev(rsdp_paddr, PAGE_SIZE) β†’ goto found_rsdp. No validation of any kind β€” not the 8-byte "RSD PTR " signature, not the checksum, not Length, not Revision, and not the address itself.
  2. EBDA scan (acpi_sdt.c:79-94) β€” signature checked (acpi_sdt.c:130).
  3. E0000-FFFFF scan (acpi_sdt.c:96-106) β€” signature checked.

At found_rsdp the mapped bytes are consumed wholesale (acpi_sdt.c:109-115): rsdp->Revision picks RSDT-vs-XSDT and RsdtPhysicalAddress/XsdtPhysicalAddress become the root of every sdt_search() walk used by madt_probe (acpi_madt.c:96), fadt_probe (acpi_fadt.c:95) and srat_probe (acpi_srat.c:75).

Pass-1 boundaries: DF-1114 filed the scanned RSDP (signature-only match in sdt_rsdp_search, missing checksum/Length); DF-1113 filed the unbounded table-Length KVA panic. Neither covers the kenv branch, which is a separate acceptance that performs less validation than either (it skips even the signature match DF-1114's path has). This finding files that gap and demonstrates it live.

2. Why the pmap path faults on a bad address (R4)

pmap_mapdev_attr() (sys/platform/pc64/x86_64/pmap.c:6192-6219) allocates KVA with kmem_alloc_nofault (pmap.c:6201) and writes raw pa into PTEs (pmap.c:6208). With hint.acpi.0.rsdp=0x800000000000 on the qemu64 CPU model (physical address bits = 40), PTE bits β‰₯ 40 are reserved β†’ the first read of the mapping (rsdp->Revision, acpi_sdt.c:109) takes a kernel #PF on a nofault-entry KVA β†’ panic: vm_fault: fault on nofault entry (observed in run.bogus_addr.panic.log, immediately after the bootverbose line "ACPI SDT: RSDP in kenv hint").

3. R1/R2 β€” acceptance is total (evidence)

  • R1 baseline (valid "RSD PTR " + control MADT: BSP+2 APs+IOAPIC): ACPI SDT: RSDP in kenv hint, madt_lapic_probe: lapic_count=3, IOAPIC configured from the crafted table.
  • R2 (signature = de ad be ef de ad be ef, v1 checksum = 0xAA, same chain): identical β€” lapic_count=3, crafted IOAPIC consumed (run.nosig_control.boot.log). The 8 signature bytes and the checksum are never consulted on this path.

Guest has 6 vCPUs; a 3-CPU enumeration proves the crafted MADT (not the firmware one) was consumed.

4. R3 β€” the family keystone (evidence)

Same invalid-signature RSDP + DF-5430's bsp_omit MADT β†’ panic: assertion "arg.bsp_found" failed in madt_lapic_pass2 (run.nosig_bsp_omit.boot.log, panic.txt). I.e. every crafted-table bug in DF-5430..5433 and DF-5583 is reachable through an RSDP that fails every ACPI validity check β€” this if is the keystone of the planted-ACPI rig.

5. Threat model (why Medium, not higher)

The hint is set from /boot/loader.conf / the loader prompt β€” a boot-owner (equivalent to root/boot-disk write or console access). No unprivileged in-guest path reaches it (sdt_probe runs at SI_BOOT2_PRESMP; runtime kenv cannot affect it). Consistent with DF-1113 (Medium, same trust boundary): the direct impact is a deterministic boot brick / crafted-table injection from one line, plus the enabling role for the whole family.

6. Fix validation + the cross-file residual

fix.diff (validated git apply --check -p0 against the read-only sys/ tree): bound rsdp_paddr < ptoa(Maxmem) before mapping (Maxmem is set in getmemsize() during init386, machdep.c:2284/2899, i.e. before any SYSINIT) and require signature + Revision <= 2 before accepting; on failure unmap and fall through to the firmware scan.

Patched-kernel build: clean, 358 s, 0 errors (fix_build.log excerpt; /usr/src copy, make -j8 nativekernel KERNCONF=X86_64_GENERIC).

Patched-kernel runs: * invalid-RSDP + bsp_omit blob β†’ sdt_probe: kenv RSDP failed validation, falling back to firmware scan β†’ RSDP not in EBDA β†’ RSDP in BIOS mem β†’ madt_lapic_probe: lapic_count=6 (real firmware MADT) β€” the bsp_found panic is GONE (fix_run.log). * hint β†’ zeroed RAM (the "typo" case): previously the zero RSDP was accepted, RSDT paddr == 0 printed, and the boot wedged before SMP; on the patched kernel it is rejected and full 6-CPU SMP bring-up proceeds.

Residual (separate file, do not fix here): AcpiOsGetRootPointer() (sys/platform/pc64/acpica/OsdEnvironment.c:66-81) hands the same unvalidated hint to ACPICA. Observed live: (a) patched kernel, crafted chain β†’ ACPICA consumed it and faulted at AcpiTbLoadNamespace+0x36 (fix_run.log tail); (b) patched kernel, zeroed hint β†’ ACPICA table walk hangs late in boot (serial log frozen after "ACPI: S? 0x0 ... F000FF53"). The complete remediation needs the same validation in that acceptance (or one shared validator). This matches the orchestrator's existing "acpi.c leads: unvalidated RSDP kenv" item; R4-style out-of-RAM hints would also fault inside ACPICA's mapping until that sibling is patched, which is why fix_patched_reproduced=false is reported for end-to-end boot while the audited file's own behavior (R3 scenario) is verified fixed.

7. Negative results (classes checked and killed in this file)

  • sdt_rsdp_search (acpi_sdt.c:120-135): scan bound i < size - 36 keeps the 36-byte struct inside the mapped window; 16-byte step is spec- compliant; signature checked. (Checksum gap = DF-1114, not re-filed.)
  • Walk array bounds (acpi_sdt.c:194-214, 255-274): entry sizes correct (XSDT u64 / RSDT u32 via sizeof); nent = (Length-36)/entry cannot overflow int (max 1073741814); entries at byte 36+8i/36+4i always lie inside the Length-sized mapping β†’ no OOB relative to the mapping.
  • Mapping lifecycle (acpi_sdt.c:137-157): map/unmap size pairs are consistent (same Length re-read from a still-mapped, boot-time-static table); header probe map/unmap of 36 bytes matches pmap_unmapdev's rounding; children mapped/unmapped one at a time; no refcounts to leak; all not-found paths unmap. sdt_sdth_map rejects Length<36, and the walk only returns tables it successfully mapped, so consumer KKASSERT(madt != NULL)-style asserts cannot fire.
  • Consumers all guard sdt_search()==0 (acpi_madt.c:97, acpi_fadt.c:96, acpi_srat.c:76-78) β†’ no accidental page-0 mapping.
  • ebda_addr (machdep.c:2275-2276, BIOS BDA word<<4) is bounded <1 MB; DF-1114-class firmware input only.
  • The AcpiOsTableOverride DSDT replacement (sys/dev/acpica/Osd/OsdTable.c:63-82) is loader-blob-fed, different file β€” cross-referenced, not filed here.
  • Bonus observation (contrib ACPICA, out of tree scope): a valid-RSDT chain with no FADT/DSDT makes the stock kernel fault at AcpiTbLoadNamespace+0x36 (cmpl $0x54445344,0x14(%rbx)) during acpi0 attach β€” see run.valid/β€Œnosig boot logs; reportable against the acpica-family lead, not acpi_sdt.c.

8. Reproduce summary

./build.sh
vm.sh reset with-src
vm.sh run_root 'printf "hint.acpi.0.rsdp=0x40000000\nboot_verbose=YES\n" >> /boot/loader.conf; sync'
vm.sh down
QEMU_EXTRA="-device loader,file=$PWD/nosig_bsp_omit.bin,addr=0x40000000,force-raw=on" vm.sh up 150
vm.sh log    # -> panic: assertion "arg.bsp_found" ...  (through an INVALID-signature RSDP)

# R4: loader.conf hint.acpi.0.rsdp=0x800000000000, boot with no blob:
#     -> panic: vm_fault: fault on nofault entry

Guest left clean: vm.sh reset with-src after all runs (verified: Jul 2 snapshot kernel active, no hint in loader.conf, real-firmware lapic_count=6).

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (Maxmem bound + signature/revision validation + firmware-scan fallback) applied to the guest /usr/src copy, kernel rebuilt clean (358s, 0 errors) and installed. Patched-kernel runs: (a) invalid-RSDP + bsp_omit blob -> 'sdt_probe: kenv RSDP failed validation, falling back to firmware scan' -> real firmware RSDT -> 'madt_lapic_probe: lapic_count=6' and the bsp_found panic is GONE (fix_run.log); (b) zeroed-hint boot that previously wedged pre-SMP now completes 6-CPU SMP bring-up. fix_patched_reproduced=false because end-to-end boot on the patched kernel still does not complete: the same unvalidated hint is separately consumed by AcpiOsGetRootPointer (OsdEnvironment.c:72) whose ACPICA walk faults (AcpiTbLoadNamespace+0x36) on the crafted chain or hangs on the zeroed hint - a residual in a different file that needs the same validation; the audited file's own behavior is verified fixed.

findings/poc/DF-5588/fix.diff (git apply --check -p0 passes); fix_run.log lines: 'sdt_probe: kenv RSDP failed validation, falling back to firmware scan', 'ACPI SDT: RSDP in BIOS mem', 'madt_lapic_probe: lapic_count=6', absence of 'panic: assertion arg.bsp_found'; fix_build.log (build banner, 358.21 real, 0 errors); residual analysis VERDICT.md section 6
↓ fix.diffper-fix-DF-5588

Confirmed kernel references

Detail

Evidence (decisive lines)

run.nosig_control.boot.log: 'ACPI SDT: RSDP in kenv hint' + 'madt_lapic_probe: lapic_count=3' with deadbeef RSDP signature (acceptance proof); run.nosig_bsp_omit.boot.log / panic.txt: 'panic: assertion "arg.bsp_found" failed in madt_lapic_pass2 at acpi_madt.c:375' reached through the invalid RSDP; run.bogus_addr.panic.log: 'panic: vm_fault: fault on nofault entry, addr: 0xfffff8004f062000' directly after 'ACPI SDT: RSDP in kenv hint' (hint 0x800000000000, no blob); fix_run.log: patched kernel prints 'sdt_probe: kenv RSDP failed validation, falling back to firmware scan', RSDP found in BIOS mem, lapic_count=6 (real firmware MADT), bsp panic GONE

PoC changes

No seed PoC. gen_rsdp_inject.py authored from scratch (DF-5430's chain builder extended): scenarios parameterize the RSDP signature (valid 'RSD PTR ' vs de ad be ef de ad be ef) and v1 checksum (wrong 0xAA) over a fixed RSDT->MADT chain, plus the DF-5430 bsp_omit MADT payload and the no-blob bogus-address variant.

Verified recommended fix

In sdt_probe()'s kenv branch require rsdp_paddr < ptoa(Maxmem) before mapping and validate the RSDP signature + Revision<=2 before installing it as the table root; on failure unmap and fall back to the firmware scan (fix.diff); apply the same validation in AcpiOsGetRootPointer (OsdEnvironment.c).

Verdict

sdt_probe()'s kenv branch (acpi_sdt.c:71-77) installs hint.acpi.0.rsdp as the ACPI table root with ZERO validation - not the 8-byte 'RSD PTR ' signature that even the scan path checks (acpi_sdt.c:130), not checksum/Length/Revision, and not the address itself. Verified live on the KVM rig three ways: (R2) an RSDP with signature de ad be ef de ad be ef and wrong checksum is accepted and its RSDT->MADT chain consumed ('RSDP in kenv hint', madt_lapic_probe lapic_count=3 on a 6-vCPU guest, crafted IOAPIC configured); (R3) the same invalid RSDP plus DF-5430's bsp_omit MADT panics the boot at madt_lapic_pass2 - the whole crafted-table family (DF-5430..5433, DF-5583) hangs off this single unvalidated if; (R4) hint.acpi.0.rsdp=0x800000000000 with no blob planted at all panics immediately ('vm_fault: fault on nofault entry') because pmap_mapdev_attr (pmap.c:6192-6219) writes the raw 64-bit address into PTEs and qemu64's 40 phys bits make bit 47 reserved - one loader.conf line bricks the boot. Boot-owner-gated (loader.conf/loader prompt), so Medium like DF-1113, not an unprivileged path. Pass-1 boundary respected: DF-1114 covered the SCANNED RSDP's missing checksum/Length; DF-1113 the unbounded table Length; the kenv branch is a separate acceptance with less validation than either.