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

AcpiOsGetRootPointer() accepts hint.acpi.0.rsdp with zero validation β€” second kenv RSDP consumer feeding ACPICA; FADT-less crafted chain panics boot at AcpiTbLoadNamespace+0x36 (OOB read of Tables[0xFFFFFFFF])

Summary

The kenv branch caches whatever u_long the loader puts in the hint with no address bound, no signature/checksum/revision check - while the same function's own firmware-scan fallback validates every candidate via AcpiTbValidateRsdp. This is the SECOND independent acceptance of the hint (the native walk's is DF-5588, whose fix does not cure this one): a FADT-less chain leaves AcpiGbl_DsdtIndex=0xFFFFFFFF and AcpiTbLoadNamespace computes &Tables[0xFFFFFFFF] then derefs the signature -> ~200GB-offset OOB read, Fatal trap 12. Reproduced on the stock kernel with the planted rig (control MADT lets SMP come up; acpi0 attaches; then the trap at the DSDT compare); a FULLY-signatured+checksummed chain faults identically, proving RSDP-level validation alone is insufficient (the upstream DsdtIndex guard is needed too); zeroed-RAM hints wedge. Boot-owner boundary (same as DF-5588/DF-1113): one loader.conf line -> deterministic boot brick + total control of ACPICA's table namespace (arbitrary in-kernel AML). validate-hint-with-fallback + upstream DsdtIndex-guard fix validated on a rebuilt kernel (all three scenarios cure: firmware fallback, graceful AE_NO_ACPI_TABLES, end-to-end zeroed-hint wedge cured with the sdt fix).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5668 Β· 20 files
FileTypeDescriptionSize
README.md β€” 4.5 KB ↓ raw
VERDICT.md β€” 9.6 KB ↓ raw
gen_chain.py β€” 4.2 KB view raw
nosig_fadtless.bin β€” 768 B ↓ download
valid_fadtless.bin β€” 768 B ↓ download
build.sh β€” 190 B view raw
run.sh β€” 201 B view raw
build.log β€” 249 B view raw
run.S1_nosig.boot.log β€” 15.3 KB view raw
panic.txt β€” 683 B view raw
run.S2_validfadtless.boot.log β€” 15.2 KB view raw
run.S3_zeroed_wedge.boot.log β€” 7.5 KB view raw
fixrun.F1_nosig.boot.log β€” 35.0 KB view raw
fixrun.F2_validfadtless.boot.log β€” 30.4 KB view raw
fixrun.F3_zeroedhint.boot.log β€” 40.3 KB view raw
fix.diff β€” 3.1 KB view raw
fix_build.log β€” 517 B view raw
env.txt β€” 700 B view raw
manifest.json β€” 2.0 KB view raw
verdict.json β€” 7.9 KB view raw

DF-5668 β€” AcpiOsGetRootPointer() accepts hint.acpi.0.rsdp with zero validation: second kenv RSDP consumer feeding ACPICA (boot panic at AcpiTbLoadNamespace+0x36)

What

sys/platform/pc64/acpica/OsdEnvironment.c:72-73 β€” AcpiOsGetRootPointer()'s kenv branch caches whatever u_long the loader passes in hint.acpi.0.rsdp into acpi_root_phys and returns it to ACPICA β€” no address bound, no "RSD PTR " signature, no checksum, no Revision sanity. This is the second, independent acceptance of the same hint (the native walk's acceptance at acpi_sdt.c:71 is DF-5588; that fix does not cure this one).

Downstream, unvalidated:

  • AcpiInitializeTables (tbxface.c:257) β†’ AcpiTbParseRootTable (tbutils.c:376) maps the RSDP and reads Revision / Xsdt/RsdtPhysicalAddress directly β€” AcpiTbValidateRsdp (tbxfroot.c:211-243, sig+checksum) only runs on the scan path (AcpiFindRootPointer), never on the host-supplied-pointer path;
  • acpi_probe (acpi.c:462-478) derefs the same cache (Revision, XSDT/RSDT paddr, RSDT header) with no checks;
  • acpi_attach β†’ AcpiLoadTables (acpi.c:553) β†’ AcpiTbLoadNamespace (tbxfload.c:275-285) computes &Tables[AcpiGbl_DsdtIndex] and derefs Table->Signature β€” with a FADT-less crafted chain AcpiGbl_DsdtIndex is still ACPI_INVALID_TABLE_INDEX (0xFFFFFFFF, acglobal.h:171; only AcpiTbParseFadt tbfadt.c:490 ever sets it) β†’ OOB read β†’ Fatal trap 12 ... Stopped at AcpiTbLoadNamespace+0x36: cmpl $0x54445344,0x14(%rbx) (the 'DSDT' compare).

Rig (proven planted-ACPI family rig)

loader.conf hint.acpi.0.rsdp=0x40000000 + QEMU -device loader,file=<blob>,addr=0x40000000,force-raw=on planting an RSDP→RSDT→MADT chain (BSP+2AP+real-KVM-IOAPIC control MADT — DF-5588's proven content, so the native walk survives and boot reaches acpi0 attach).

Build

./build.sh     # gen_chain.py -> nosig_fadtless.bin, valid_fadtless.bin

Run (each phase boots the guest; see run.sh)

  • S1 (stock kernel, invalid-sig chain): native walk consumes crafted MADT (lapic_count=3), acpi0: <DFBSD > (crafted RSDT OEM id read by acpi_probe), then Fatal trap 12 ... AcpiTbLoadNamespace+0x36 β€” run.S1_nosig.boot.log, panic.txt.
  • S2 (stock kernel, FULLY valid sig+checksums, FADT-less): identical fatal trap β€” a deliberate boot-owner gets past any RSDP-level validation; the ACPICA-side index guard is needed too β€” run.S2_validfadtless.boot.log.
  • S3 (hint β†’ zeroed RAM, no blob): stock sdt wedges pre-SMP (RSDT paddr == 0, serial frozen) β€” run.S3_zeroed_wedge.boot.log; on a DF-5588-patched kernel the wedge moves into ACPICA's walk (observed in DF-5588 fix_run.log tail: fault at AcpiTbLoadNamespace+0x36 / late-boot freeze) β€” cured here in F3.
  • F1 (fix.diff, invalid-sig chain): AcpiOsGetRootPointer: kenv RSDP failed validation, falling back to firmware scan β†’ firmware RSDP 0xF59A0 β†’ acpi0: <BOCHS BXPC> [attached!], machdep.acpi_root=0xF59A0, boot completes, NO panic β€” fixrun.F1_nosig.boot.log.
  • F2 (fix.diff, fully-valid chain): RSDP validation passes (chain is genuinely valid), contrib guard catches the FADT-less walk β†’ acpi0: Could not enable ACPI: AE_NO_ACPI_TABLES (graceful, AcpiLoadTables swallows the load failure at tbxfload.c:213) β†’ boot completes, NO panic β€” fixrun.F2_validfadtless.boot.log.
  • F3 (fix.diff + DF-5588 fix.diff, zeroed hint): BOTH acceptances reject β†’ firmware tables β†’ lapic_count=6, acpi0 [attached!], clean boot β€” the wedge is gone end-to-end β€” fixrun.F3_zeroedhint.boot.log.

Fix

fix.diff = (a) OsdEnvironment.c: bound the hinted address against ptoa(Maxmem), require "RSD PTR " + Revision <= 2 + v1 checksum (+ extended checksum for rev β‰₯ 2) before caching; on failure log and fall back to AcpiFindRootPointer (firmware scan) in both the no-hint and invalid-hint cases; (b) tbxfload.c belt-and-suspenders guard: treat AcpiGbl_DsdtIndex == ACPI_INVALID_TABLE_INDEX as AE_NO_ACPI_TABLES before indexing Tables[] (upstream-ACPICA hardening; reachable via any fully-valid FADT-less chain regardless of how the root pointer was obtained β€” real broken firmware included).

Trust boundary

The hint is set by the loader (/boot/loader.conf or the loader prompt): a boot-owner (root / boot-disk write / console). No unprivileged in-guest path reaches it: AcpiOsGetRootPointer is only called during cold boot (acpi_Startup/acpi_probe) and caches its result; runtime kenv changes cannot affect it. Severity Medium, consistent with DF-5588/DF-1113.

VERDICT.md
↓ download raw

DF-5668 VERDICT β€” AcpiOsGetRootPointer(): second unvalidated kenv RSDP consumer (pass 2)

Status: REPRODUCED (certain). Impact: panic (deterministic boot-time kernel OOB read / boot brick; boot-owner trust boundary β†’ Medium). File: sys/platform/pc64/acpica/OsdEnvironment.c:59-81, acceptance at :72-73.

1. Root cause

AcpiOsGetRootPointer() (OsdEnvironment.c:59-81) has two RSDP discovery paths:

  1. kenv hint (:72-73): kgetenv_ulong("hint.acpi.0.rsdp") β†’ cached in acpi_root_phys and returned. No validation of any kind β€” not the address, not the "RSD PTR " signature, not the checksum, not Revision.
  2. Firmware scan (:75): AcpiFindRootPointer β†’ every candidate passes AcpiTbValidateRsdp (tbxfroot.c:211-243: signature + v1 checksum + extended checksum for rev β‰₯ 2).

The validated path and the unvalidated path live in the same function; the kenv branch skips everything the scan branch enforces.

This is the second, independent acceptance of the same loader hint. The first (native walk, acpi_sdt.c:71) is DF-5588; that fix does not cure this one β€” observed live in DF-5588's own fix validation (fix_run.log tail: fault at AcpiTbLoadNamespace+0x36 on the crafted chain; late-boot freeze on the zeroed hint) and re-proven here on the stock kernel.

2. Who consumes the unvalidated pointer, and what trusts it

  • AcpiInitializeTables (tbxface.c:257) β†’ AcpiTbParseRootTable (tbutils.c:376): maps the RSDP (:392), reads Rsdp->Revision (:410) and XsdtPhysicalAddress/RsdtPhysicalAddress (:416/:423) directly. AcpiTbValidateRsdp is never called on this path β€” stock ACPICA trusts the host-OS-supplied root pointer. The RSDT/XSDT gets a length+checksum check (tbutils.c:460-466) but no signature check.
  • acpi_probe (acpi.c:462-478): reads the cached pointer, maps the RSDP, derefs Revision/XsdtPhysicalAddress, maps the RSDT header β€” no checks at all. Its device description (acpi0: <DFBSD >) is printed straight from the crafted RSDT's OEM id β€” a visible acceptance marker.
  • acpi_attach β†’ AcpiLoadTables (acpi.c:553) β†’ AcpiTbLoadNamespace (tbxfload.c:207, body :255+): line 275 computes &AcpiGbl_RootTableList.Tables[AcpiGbl_DsdtIndex] and line 279-280 derefs Table->Signature against ACPI_SIG_DSDT before any FADT presence check. With a FADT-less chain, AcpiGbl_DsdtIndex is still ACPI_INVALID_TABLE_INDEX = 0xFFFFFFFF (acglobal.h:171; only AcpiTbParseFadt, tbfadt.c:490, ever sets it) β†’ Tables[0xFFFFFFFF].Signature β†’ OOB read ~200 GB past the array β†’ kernel page fault. In-kernel consequence: ACPICA additionally executes crafted DSDT/SSDT AML in supervisor mode (boot-owner-equivalent primitive; AML can only be stopped by not feeding it invalid roots).

3. Reproduction (stock kernel, DragonFly 6.5-DEVELOPMENT #0 Jul 2 2026)

Proven planted-ACPI rig (DF-5430..5433/5583/5588): loader.conf hint.acpi.0.rsdp=0x40000000 + QEMU -device loader,file=blob, addr=0x40000000,force-raw=on. The planted MADT is the family control MADT (BSP+2 APs+real IOAPIC), so the native walk survives and boot reaches acpi0 attach where ACPICA's own walk of the SAME hint faults.

  • S1 β€” invalid-signature chain (nosig_fadtless.bin: signature de ad be ef de ad be ef, wrong v1 checksum 0xAA, correct-checksummed RSDTβ†’MADT, no FADT): ACPI SDT: RSDP in kenv hint β†’ madt_lapic_probe: lapic_count=3 β†’ acpi0: <DFBSD > (acpi_probe consumed crafted RSDT) β†’ Fatal trap 12: page fault while in kernel mode … Stopped at AcpiTbLoadNamespace+0x36: cmpl $0x54445344,0x14(%rbx) (0x54445344 = 'DSDT'; fault VA 0xfffff8208d6e27f4, supervisor read, page not present) β€” run.S1_nosig.boot.log, panic.txt. Identical to the fault DF-5588 observed on their patched kernel (their fix_run.log): the two acceptances are independent and this one needs its own fix.
  • S2 β€” FULLY valid chain (valid_fadtless.bin: genuine "RSD PTR ", correct v1 checksum, correct RSDT/MADT checksums, still FADT-less): identical fatal trap (run.S2_validfadtless.boot.log). Consequence: RSDP-level validation alone cannot stop a deliberate boot-owner; the AcpiGbl_DsdtIndex guard in ACPICA is needed as well.
  • S3 β€” zeroed hint (hint β†’ 0x40000000, nothing planted): on the stock kernel the native acceptance wedges first, pre-SMP (sdt_search_rsdt: RSDT paddr == 0, serial frozen β€” run.S3_zeroed_wedge.boot.log). On a DF-5588-patched kernel the same config drives ACPICA's walk of the zeroed page (DF-5588 fix_run.log tail: late-boot freeze) β€” cured end-to-end in F3 below.

Attempts: 6 distinct boots (S1 twice β€” first run had a builder byte-shift bug in the blob, fixed; S2; accidental S3; F1 twice β€” v1 of the fix only degraded gracefully, v2 adds the firmware fallback; F3).

4. Why Medium (not higher)

The hint is set from /boot/loader.conf / the loader prompt β€” a boot-owner (equivalent to root / boot-disk write / console). No unprivileged in-guest path reaches it: AcpiOsGetRootPointer runs only during cold boot (acpi_Startup/acpi_probe) and caches acpi_root_phys; runtime kenv writes cannot influence it, and after boot it is never consulted again. Direct impact: one loader.conf line β†’ deterministic boot brick (fatal trap / pre-SMP wedge), plus full control of ACPICA's table namespace (arbitrary in-kernel AML execution) for the same boot-owner. Consistent with the family precedent (DF-5588 Medium, DF-1113 Medium, DF-1114 Low).

5. Fix validation

fix.diff (git apply --check -p0 clean against the read-only sys/ tree):

(a) OsdEnvironment.c β€” acpi_validate_rsdp_paddr(): reject the hint unless 0 < paddr < ptoa(Maxmem) (an out-of-range address faults through pmap_mapdev's reserved-bit PTEs the moment it is read β€” DF-5588's R4 mechanism, which applies to AcpiOsMapMemory at OsdMemory.c:85 just the same) and the mapped bytes carry "RSD PTR ", Revision <= 2, a zero v1 checksum and (rev β‰₯ 2) a zero extended checksum β€” exactly AcpiTbValidateRsdp's rules. On failure: log AcpiOsGetRootPointer: kenv RSDP failed validation, falling back to firmware scan and run AcpiFindRootPointer in both the no-hint and invalid-hint cases (fix v1 fell back only on absent hint β€” rebuilt). (b) tbxfload.c β€” guard AcpiGbl_DsdtIndex == ACPI_INVALID_TABLE_INDEX β†’ AE_NO_ACPI_TABLES before indexing Tables[] (upstream-ACPICA hardening: any fully-valid FADT-less chain β€” crafted or broken firmware β€” otherwise OOB-reads; note AcpiLoadTables swallows the failure at tbxfload.c:213, so the visible symptom is a graceful acpi0: Could not enable ACPI: AE_NO_ACPI_TABLES).

Patched builds: make -j8 nativekernel KERNCONF=X86_64_GENERIC clean, 0 errors (fix_build.log; F3 build 18:23 UTC 2026).

Patched-kernel runs (exact S-scenario reruns):

  • F1 (invalid-sig chain): AcpiOsGetRootPointer: kenv RSDP failed validation, falling back to firmware scan β†’ firmware RSDP 0xF59A0 (located independently via the QEMU monitor; sig+checksum valid) β†’ acpi0: <BOCHS BXPC> [attached!], machdep.acpi_root: 1005984 (= 0xF59A0), boot completes, no panic (fixrun.F1_nosig.boot.log).
  • F2 (fully-valid chain): RSDP validation passes (chain is genuinely valid), ACPICA consumes it, the tbxfload guard fires β†’ acpi0: Could not enable ACPI: AE_NO_ACPI_TABLES β†’ graceful attach failure, boot completes, no panic (fixrun.F2_validfadtless.boot.log).
  • F3 (fix.diff + DF-5588's validated sdt fix; zeroed hint, no blob): sdt_probe: kenv RSDP failed validation… and AcpiOsGetRootPointer: kenv RSDP failed validation… β†’ firmware tables everywhere β†’ madt_lapic_probe: lapic_count=6, hw.ncpu=6, acpi0: <BOCHS BXPC> [attached!] β€” the stock pre-SMP wedge (run.S3_zeroed_wedge.boot.log) is gone end-to-end (fixrun.F3_zeroedhint.boot.log).

Behavior change worth noting: a present-but-invalid hint (including a typo or a hint of 0) previously either crashed, wedged, or silently poisoned ACPICA; it now degrades to the firmware tables β€” the same state as having no hint at all.

6. Negative results (file-level, pass 2)

  • AcpiOsInitialize/AcpiOsTerminate (OsdEnvironment.c:47-57): inert AE_OK stubs; callers in ACPICA init/shutdown β€” no state, no issue.
  • machdep.acpi_root SYSCTL_ULONG CTLFLAG_RD (:44): world-readable exposure of the RSDP physical address only (firmware table location, not kernel memory) β€” negligible; not filed.
  • acpi_root_phys cache: written only during cold boot (single-threaded identify/probe context), read-only afterwards; no runtime mutation path, no concurrency issue. The value-0 hint corner (cache never populated β†’ ACPICA silently disabled, masking the firmware fallback) is subsumed by the main finding's fix.
  • No copyin/copyout/uiomove, no ioctls, no locks, no length arithmetic in this file; u_long == ACPI_PHYSICAL_ADDRESS width on pc64 (no truncation).
  • hint.acpi.0.rsdp consumers repo-wide: exactly two (acpi_sdt.c:71, OsdEnvironment.c:72 β€” both now covered by validated fixes).

7. Post-run state

Guest reset to the clean-source snapshot after validation (fix kernels discarded); sys/ never modified (fix.diff authored via mechanical diff of pristine vs patched copies, git apply --check only).

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

Baseline (stock): S1/S2 fatal trap at AcpiTbLoadNamespace+0x36, S3 pre-SMP wedge. Patched (fix.diff = OsdEnvironment.c validation+fallback + tbxfload.c DsdtIndex guard): F1 identical-to-S1 scenario boots clean on firmware tables (acpi0 attached, machdep.acpi_root=0xF59A0, no panic); F2 fully-valid-chain scenario degrades gracefully (acpi0: Could not enable ACPI: AE_NO_ACPI_TABLES, boot completes, no panic); F3 with DF-5588's sdt fix additionally cures the zeroed-hint wedge end-to-end (lapic_count=6, hw.ncpu=6, acpi0 attached). The previously-observed bad behavior is gone in every scenario; kernel built clean with 0 errors.

Baseline: run.S1_nosig.boot.log + panic.txt + run.S2_validfadtless.boot.log + run.S3_zeroed_wedge.boot.log. Patched: fixrun.F1_nosig.boot.log (reject+fallback lines), fixrun.F2_validfadtless.boot.log (graceful AE_NO_ACPI_TABLES), fixrun.F3_zeroedhint.boot.log (both acceptances reject, 6-CPU clean boot); fix_build.log (two clean nativekernel installs, 18:05 and 18:23 UTC 2026); fix.diff applies via git apply --check -p0 against the pristine tree.
↓ fix.diffper-fix-DF-5668

Confirmed kernel references

Detail

Exploit chain

boot-owner (root/boot-disk write or loader console) sets hint.acpi.0.rsdp=0x40000000 in /boot/loader.conf; QEMU -device loader (or any boot-disk RAM planting) places a crafted RSDP->RSDT->MADT chain at that address; native sdt_probe consumes the control MADT so SMP bring-up succeeds; acpi_Startup -> AcpiInitializeTables -> AcpiOsGetRootPointer returns the unvalidated hint; AcpiTbParseRootTable walks the crafted chain (no RSDP validation on the host-supplied-pointer path); acpi_attach -> AcpiLoadTables -> AcpiTbLoadNamespace indexes Tables[0xFFFFFFFF] -> Fatal trap 12 (boot panic). Out-of-RAM hints fault earlier via AcpiOsMapMemory->pmap_mapdev reserved-bit PTEs; zeroed-RAM hints wedge the walk (sdt-side on stock, ACPICA-side once sdt is patched). Full ACPICA namespace control (arbitrary in-kernel AML) is available to the same boot-owner β€” equivalent privileges, no unprivileged escalation path exists.

Evidence (decisive lines)

run.S1_nosig.boot.log (stock, invalid-sig chain: lapic_count=3, acpi0: <DFBSD >, Fatal trap 12, Stopped at AcpiTbLoadNamespace+0x36); panic.txt (full trap block, fault VA 0xfffff8208d6e27f4, cmpl $0x54445344); run.S2_validfadtless.boot.log (fully-valid chain, identical trap); run.S3_zeroed_wedge.boot.log (zeroed hint: RSDT paddr == 0, pre-SMP wedge); fixrun.F1_nosig.boot.log ('AcpiOsGetRootPointer: kenv RSDP failed validation, falling back to firmware scan', acpi0: <BOCHS BXPC> [attached!], machdep.acpi_root=1005984=0xF59A0); fixrun.F2_validfadtless.boot.log (graceful 'acpi0: Could not enable ACPI: AE_NO_ACPI_TABLES', boot completes); fixrun.F3_zeroedhint.boot.log (both acceptances reject, lapic_count=6, acpi0 attached, wedge gone); fix.diff; fix_build.log; env.txt; VERDICT.md sections 2-5.

PoC changes

Adapted DF-5588's proven generator (gen_rsdp_inject.py) into gen_chain.py with two scenarios: nosig_fadtless (invalid signature + wrong checksum) and valid_fadtless (genuine 'RSD PTR ' + correct v1 checksum + correct RSDT/MADT checksums, still FADT-less β€” proves RSDP-level validation alone is insufficient). First S1 attempt failed due to a builder bug (5-byte value assigned into a 4-byte bytearray slice shifted every table field by one; kernel correctly complained 'madt_iterate_entries: invalid MADT entry len 0') β€” fixed to b'P568'. Fix v1 fell back to AcpiFindRootPointer only when the hint was absent; after observing AE_NOT_FOUND on QEMU (SeaBIOS RSDP verified present+valid at 0xF59A0 via the QEMU monitor), restructured so the firmware-scan fallback runs for both absent and invalid hints (v2).

Verified recommended fix

Validate the kenv RSDP hint in AcpiOsGetRootPointer (bound against ptoa(Maxmem), require 'RSD PTR ' + Revision<=2 + zero v1/x checksums) and fall back to AcpiFindRootPointer on failure; guard AcpiTbLoadNamespace against AcpiGbl_DsdtIndex == ACPI_INVALID_TABLE_INDEX before indexing Tables[] (see fix.diff).

Verdict

AcpiOsGetRootPointer (OsdEnvironment.c:72-73) accepts hint.acpi.0.rsdp with zero validation (no address bound, no RSDP signature/checksum/Revision check) and feeds it to ACPICA, while the same function's own AcpiFindRootPointer fallback path validates every candidate via AcpiTbValidateRsdp β€” a second, independent acceptance of the same loader hint that DF-5588's acpi_sdt fix does not cure. Reproduced on the stock kernel: a crafted RSDP->RSDT->MADT chain (FADT-less) planted at 0x40000000 boots through the native walk and reaches acpi0 attach, where AcpiTbLoadNamespace (tbxfload.c:275-285) indexes Tables[AcpiGbl_DsdtIndex = 0xFFFFFFFF] and faults: Fatal trap 12, Stopped at AcpiTbLoadNamespace+0x36 (cmpl $0x54445344 = 'DSDT'), deterministic boot brick. Fully-signatured+checksummed chains fault identically (S2), proving the contrib-side DsdtIndex guard is also required. Trust boundary is boot-owner (loader.conf/loader prompt), consistent with the DF-5588/DF-1113 Medium precedent; no unprivileged in-guest path reaches the hint (cold-boot-only consumer, cached). fix.diff validates: RSDP validation + firmware fallback in AcpiOsGetRootPointer, plus the tbxfload.c ACPI_INVALID_TABLE_INDEX guard β€” F1 boots on firmware tables (acpi0 attached), F2 degrades gracefully, F3 (with DF-5588's sdt fix) cures the zeroed-hint wedge end-to-end (6 CPUs, clean boot).