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

Uninitialized kernel-stack value exported to unprivileged users via world-readable hw.acpi.acline when firmware _PSR returns no object (DF-3793 root cause surfacing in acpi_acad)

Summary

AE_OK-with-no-object leaves the caller buffer untouched, so acpi_GetInteger reads its uninitialized stack ACPI_OBJECT - when residue Type==4 it copies 32 bits of stack into sc->status, served world-readable on every read (re-sampling the residue each time). LIVE-REPRODUCED on guest via injected SSDT (acline=0 where sentinel-correct is -1, read as uid 1001); same-thread kstack grooms could not vary the dword (honest impact: none). param.Type=ACPI_TYPE_ANY root-cause fix in acpi.c.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5073 Β· 17 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 5.3 KB ↓ raw
ssdt_acad_sane.asl β€” 463 B ↓ download
ssdt_acad_sane.aml β€” 80 B ↓ download
ssdt_acad_leak.asl β€” 1.2 KB ↓ download
ssdt_acad_leak.aml β€” 83 B ↓ download
groom.c β€” 2.3 KB view raw
build.sh β€” 94 B view raw
run.sh β€” 89 B view raw
build.log β€” 1.2 KB view raw
run.log β€” 1.7 KB view raw
run.2.log β€” 1.5 KB view raw
run.sane.log β€” 880 B view raw
env.txt β€” 813 B view raw
fix.diff β€” 851 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.7 KB view raw

DF-5073 β€” Uninitialized kernel-stack value exported to unprivileged users via hw.acpi.acline when the AC-adapter _PSR returns no object

What

acpi_acad.c reads the AC-line status with acpi_GetInteger(h, "_PSR", &newstatus) (sys/dev/acpica/acpi_acad.c:111). When the firmware's _PSR method evaluates successfully but returns no object (malformed/quirky AML), ACPICA returns AE_OK and leaves the caller's return buffer untouched (sys/contrib/dev/acpica/source/components/namespace/nsxfeval.c:541-545, !Info->ReturnObject β†’ ReturnBuffer->Length = 0, Pointer never written). acpi_GetInteger() (sys/dev/acpica/acpi.c:2162-2183) then inspects its uninitialized stack local ACPI_OBJECT param; if the residue byte at offset 0 happens to equal ACPI_TYPE_INTEGER (4), it copies param.Integer.Value β€” 32 bits of kernel-stack residue β€” into *number.

acpi_acad.c:110-116 uses that garbage as the authoritative line status:

newstatus = -1;
acpi_GetInteger(h, "_PSR", &newstatus);     /* may write stack residue */
if (newstatus != -1 && sc->status != newstatus) {
        sc->status = newstatus;             /* garbage persisted */
        ...

and the world-readable sysctl hw.acpi.acline returns it to any unprivileged user on every read (acpi_acad.c:289 β†’ acpi_acad.c:233-234). Every sysctl read re-evaluates _PSR, i.e. re-samples the stack residue.

Root cause is DF-3793 (filed pass 1 against acpi.c); this finding is the call-site export primitive in acpi_acad.c (previously unaudited file).

Reproduce (guest, no privilege needed for the read side)

  1. Build the two SSDTs with the guest iasl (see build.sh): * ssdt_acad_sane.asl β€” control: _PSR returns One. * ssdt_acad_leak.asl β€” trigger: _PSR falls off the end (no Return).
  2. Control boot: cd dfbsd-qemu && QEMU_EXTRA="-acpitable file=ssdt_acad_sane.aml" ./vm.sh up 300 * sysctl hw.acpi.acline β†’ 1 (root and uid 1001), acpi_acad0 attaches.
  3. Trigger boot: QEMU_EXTRA="-acpitable file=ssdt_acad_leak.aml" ./vm.sh up 300 * Expected on a correct kernel: hw.acpi.acline = -1 (evaluation has no return value). * Observed: hw.acpi.acline = 0 β€” reachable only via the uninitialized param (Type=4 residue, Value low dword 0). See run.log, run.2.log.
  4. Grooming (groom.c): same-thread deep syscalls (socket/connect, kern.proc.all sysctl, file reads) followed by immediate acline reads by uid 1001 β€” 21 reads, value stayed 0 on this build.

Files

  • ssdt_acad_sane.asl / ssdt_acad_sane.aml β€” control firmware
  • ssdt_acad_leak.asl / ssdt_acad_leak.aml β€” trigger firmware
  • groom.c β€” same-thread kstack groom + unprivileged acline reader
  • build.sh / run.sh β€” exact commands
  • build.log β€” iasl + cc output (full)
  • run.log β€” decisive groom run (uid 1001, 21 reads)
  • run.2.log β€” leak boot transcript incl. sanity control
  • run.sane.log β€” boot A control transcript
  • env.txt β€” guest environment
  • fix.diff β€” root-cause one-liner in acpi_GetInteger (acpi.c; DF-3793's root cause), authored after verification, never applied to sys/
  • VERDICT.md, manifest.json, verdict.json

Expected output (trigger boot, unprivileged)

$ sysctl hw.acpi.acline
hw.acpi.acline: 0          <- should be -1; 0 proves uninitialized export
VERDICT.md
↓ download raw

DF-5073 β€” VERDICT

Status: reproduced (mechanism) / impact: none (no kernel-secret content demonstrated) / confidence: certain (that uninitialized kernel stack is exported to unprivileged users).

What was run

Three boots of the audit QEMU guest (DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC):

  1. Stock boot β€” no ACPI0003 device exists in QEMU's DSDT; hw.acpi.acline is absent (sysctl: unknown oid). The driver is inert on stock hardware here.
  2. Control boot (-acpitable file=ssdt_acad_sane.aml): ACPI0003 device with a well-formed _PSR returning One. Result: acpi_acad0 attaches, hw.acpi.acline = 1 for root and for uid 1001. This proves the whole chain: injected SSDT β†’ acpi_acad probe/attach β†’ _PSR AML evaluation β†’ world-readable sysctl. (run.sane.log)
  3. Trigger boot (-acpitable file=ssdt_acad_leak.aml): same device, but _PSR evaluates successfully and returns no object (iasl warnings 3115/3107 confirm the malformed shape).

What was observed (trigger boot)

sysctl hw.acpi.acline = 0, read by uid 1001 and root, repeatedly.

0 is only reachable through the uninitialized path:

  • acpi_acad.c:110 sets newstatus = -1 (0xFFFFFFFF);
  • the only other writer of newstatus is acpi_GetInteger's *number = param.Integer.Value (acpi.c:2180);
  • AcpiEvaluateObject for a no-return method returns AE_OK without writing the caller's buffer (nsxfeval.c:541-545), so param is uninitialized stack;
  • therefore the residue at param.Type read as ACPI_TYPE_INTEGER (4) and the residue at param.Integer.Value's low dword read as 0.

A correct kernel leaves newstatus == -1; the sysctl would show -1 (and the boot-time init would print "tried 6 times" under verbose ACPI). We observed 0. The uninitialized-stack export is proven live.

Why impact = none (honest ceiling)

To qualify as an info leak, the disclosed bytes must carry kernel-memory content. I attempted to vary the leaked dword with same-thread kernel-stack grooming before each read (groom.c): connect(2) (deep TCP path), 8Γ— sysctl(KERN_PROC_ALL) (deep vm/proc walk), 32Γ— block reads of /kernel, and process-level grooms (sysctl -a, ls -lR /usr/include, find /). All 21+ unprivileged reads returned the same 0 (run.log, run.2.log).

Explanation: the leaked slot is acpi_GetInteger's own param local, at a depth that only other ACPI evaluation frames occupy. The residue {Type=4, Value=0} is a stale ACPI_OBJECT{INTEGER, 0} left by some earlier zero-returning AML evaluation on the same kstack region at boot (AML Zero returns are ubiquitous); the sysctl read path re-derives the same value every time, and ordinary syscalls never dirty that exact slot. On this build the disclosed content is therefore constant and non-useful. I could not demonstrate disclosure of pointers, secrets, or any variable kernel memory β€” so the honest impact is none, not leak.

The class (uninitialized kernel stack exported to unprivileged users) is real and live, hence severity Low (kernleak): trigger requires malformed firmware (_PSR without return β€” iasl explicitly warns "Reserved method must return a value"), reader side needs no privilege.

Root cause & fix

Root cause was already identified by DF-3793 (pass 1, acpi.c): acpi_GetInteger reads param after an AE_OK evaluation that returned no object. This finding adds the call-site impact in acpi_acad.c: the garbage becomes persistent driver state (sc->status, acpi_acad.c:116) and is served world-readably (acpi_acad.c:289,233-234); it also drives power_profile_set_state() (acpi_acad.c:118).

fix.diff (authored after verification, never applied to sys/) fixes the root cause with one line: pre-set param.Type = ACPI_TYPE_ANY before AcpiEvaluateObject, so a no-return evaluation yields AE_TYPE and leaves *number (i.e. newstatus == -1) untouched. With the fix, the trigger boot must read hw.acpi.acline = -1 instead of 0.

Fix validation was not performed: the finding is Low severity, impact none, and not memory corruption (contract requires fix builds for reproduced memory-corruption findings). The 1-line fix is mechanically obvious and directly targets the proven path.

Threat model

  • Trigger: firmware/boot-time AML only (injected SSDT on QEMU; malformed _PSR in the wild). No unprivileged user can create the trigger on a healthy machine.
  • Read side: fully unprivileged (hw.acpi.acline is CTLFLAG_RD, world readable; /dev/acpi ioctls are not needed).

Kernel references

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

not_testable

Fix validation skipped per contract (mandatory only for reproduced memory-corruption findings; this is Low severity, impact none). fix.diff is a 1-line root-cause fix in acpi.c authored after verification; with it, the trigger boot must read hw.acpi.acline = -1 instead of 0. Never applied to sys/.

fix_baseline_reproduced=1: trigger boot on stock kernel reads acline=0 (run.2.log). Patched-kernel run not performed.
↓ fix.diffper-fix-DF-5073

Confirmed kernel references

Detail

Evidence (decisive lines)

run.sane.log (control boot: acline=1 for uid 1001 proves injected-SSDT->acpi_acad attach->_PSR->world-readable sysctl chain); run.2.log (trigger boot: acline=0 instead of -1); run.log (groom.c: 21 unprivileged reads after deep same-thread grooms, constant 0); VERDICT.md (full why-0-only-via-uninit-path derivation and honest impact=none reasoning); fix.diff (1-line root-cause fix in acpi_GetInteger)

PoC changes

No seed PoC existed (new pass-2 finding). Wrote both SSDTs from scratch; had to model the no-return _PSR as 'If (LGreater (0,1)) {Return(One)}' so iasl accepts it while still falling off the end (iasl warns 3115/3107, which is exactly the malformed-firmware shape). First groom attempt (process-level sysctl -a / ls -lR between reads) failed to move the value because each command is a separate process with a different kstack; rewrote groom.c to groom and read in the SAME thread.

Verified recommended fix

In acpi_GetInteger (sys/dev/acpica/acpi.c), pre-set param.Type = ACPI_TYPE_ANY before AcpiEvaluateObject so a no-return evaluation yields AE_TYPE without reading uninitialized stack (fix.diff); defensively, acpi_acad.c:111 could also treat acpi_GetInteger failure as 'unknown' explicitly.

Verdict

Mechanism reproduced on the audit guest with an injected SSDT: an ACPI0003 AC-adapter whose _PSR evaluates OK but returns NO object makes hw.acpi.acline read 0 (for root and uid 1001) where the sentinel-correct value is -1. 0 is reachable only through acpi_GetInteger's uninitialized stack ACPI_OBJECT (AcpiEvaluateObject leaves ReturnBuffer->Pointer untouched on AE_OK for no-return methods, nsxfeval.c:541-545), whose residue here reads {Type=ACPI_TYPE_INTEGER, Value=0}; acpi_acad.c:111/116 persists it into sc->status and acpi_acad.c:289/233 exports it through the world-readable sysctl on every read. Impact honestly bounded to 'none': 21 same-thread kernel-stack grooms (connect(2), KERN_PROC_ALL sysctls, block reads) could not vary the leaked dword from 0 β€” the slot is owned by ACPI-evaluation frames, so no pointer-bearing kernel memory was demonstrated disclosed. Trigger is firmware-shape (boot-time AML) only; reader side is fully unprivileged. Severity Low (kernleak class).