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

acpi_GetInteger() reads uninitialized stack ACPI_OBJECT when AML evaluation succeeds but returns no object

Summary

acpi_GetInteger() passes a stack ACPI_OBJECT to AcpiEvaluateObject with a fixed buffer and, on ACPI_SUCCESS, reads param.Type/param.Integer.Value. When the named object exists, evaluates cleanly, but yields no return object, ACPICA sets ReturnBuffer->Length=0, leaves the caller buffer untouched, and returns AE_OK - so the helper consumes stale kernel stack. REPRODUCED ON THE GUEST via a kernel module evaluating a live-DSDT no-Return method with a pre-poisoned caller buffer: 'status=AE_OK buf.Length_after=0 ... param.Value=0xdeadbeefcafebabe <== BUFFER UNTOUCHED, AE_OK' and the real acpi_GetInteger result varied with stack history. With the default interpreter-slack tunable most no-Return methods gain an implicit return (masking); with debug.acpi.interpreter_slack=0 - a supported configuration - every no-Return method triggers it during ordinary probing. Firmware controls which probed methods return nothing; stale kernel stack then decides device presence (_STA), lid state, power-resource state, thermal/EC values - partially exposed via sysctls. No unprivileged runtime trigger on a stock system; no corruption. Fix validated on a patched kernel.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3793 Β· 10 files
FileTypeDescriptionSize
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
df3793.c β€” 5.1 KB view raw
build.sh β€” 80 B view raw
run.sh β€” 169 B view raw
run.log β€” 637 B view raw
run_patched.log β€” 652 B view raw
env.txt β€” 451 B view raw
fix.diff β€” 811 B view raw
verdict.json β€” 3.1 KB view raw

DF-3793 β€” acpi_GetInteger() reads an uninitialized stack ACPI_OBJECT

  • File: sys/dev/acpica/acpi.c (pass 2)
  • Lines: 2161–2204 (read of param.Type / param.Integer.Value at 2179–2180)
  • Class: CWE-457 (use of uninitialized memory)
  • Severity: Low Β· Confidence: certain (code + runtime proven on guest)

What

acpi_GetInteger() hands ACPICA a caller-supplied (stack) buffer:

ACPI_OBJECT param;
...
buf.Pointer = &param;
buf.Length = sizeof(param);
status = AcpiEvaluateObject(handle, path, NULL, &buf);
if (ACPI_SUCCESS(status)) {
    if (param.Type == ACPI_TYPE_INTEGER)     /* <-- uninitialized if the
                                                object returned no value */
        *number = param.Integer.Value;
    else
        status = AE_TYPE;
}

When the named AML object exists, evaluates without error, but produces no return object, ACPICA (nsxfeval.c, AcpiEvaluateObject) sets ReturnBuffer->Length = 0, never writes the caller's buffer, and returns AE_OK. acpi_GetInteger then inspects param.Type β€” stale stack data β€” and, if the garbage happens to equal ACPI_TYPE_INTEGER (1), propagates param.Integer.Value (stale stack garbage) into *number.

On DragonFly's default configuration the interpreter-slack "implicit return" (AcpiGbl_EnableInterpreterSlack = 1, set at acpi.c:538 from debug.acpi.interpreter_slack, default 1) makes most value-less methods return their last operator result, which masks the bug for typical BIOS AML. With slack disabled β€” a supported boot tunable β€” every method without an explicit Return() hits the uninitialized read. Slack-independent triggers exist too: empty method bodies, bare Return(), or bodies whose operators produce no result (only If/Notify/Sleep).

Reachability / threat

  • The DSDT/SSDT AML is firmware-provided; a defective or malicious BIOS (or a VM owner injecting tables) controls whether probed methods return nothing.
  • In-kernel consumers of acpi_GetInteger include _STA (acpi_DeviceIsPresent acpi.c:2014, acpi_BatteryIsPresent acpi.c:2039), _LID (acpi_lid.c:161), power-resource _STA (acpi_powerres.c:668/712), EC / thermal / video paths. Consequences: device-present decisions, lid state, thermal thresholds and battery values derived from stale kernel stack data β€” including values that are user-readable through sysctls (limited stack-content disclosure) and state decisions (availability).
  • No unprivileged runtime trigger found on a stock system (DO_MCALL does not use this helper), hence Low severity.

How it was proven (guest, see VERDICT.md)

Kernel module df3793.c calls AcpiEvaluateObject() with a pre-poisoned caller buffer on a no-Return method of the live DSDT (\_SB.PCI0.PRES._INI, body CSEL = Zero):

DF3793: path=\_SB_.PCI0.PRES._INI status=AE_OK buf.Length_after=0
DF3793:   param.Type=1 param.Value=0xdeadbeefcafebabe  <== BUFFER UNTOUCHED, AE_OK
DF3793:   acpi_GetInteger -> AE_TYPE number=0x00000000  (frame layout differs; see Part A)

with debug.acpi.interpreter_slack=0. AE_OK, buffer untouched β‡’ inside acpi_GetInteger the same call reads uninitialized stack. The helper's outcome (AE_TYPE here, AE_OK+garbage number in other runs β€” observed varying across runs before/after the tunable) is decided by stale stack contents.

Reproduce

  1. debug.acpi.interpreter_slack=0 in /boot/loader.conf.local, reboot.
  2. Build: make in this directory (Makefile below; needs /usr/obj/usr/src/sys/X86_64_GENERIC/opt_acpi.h + opt_global.h copied in).
  3. kldload ./df3793.ko && dmesg | grep DF3793.

Expected: status=AE_OK buf.Length_after=0 with the poison value intact (<== BUFFER UNTOUCHED, AE_OK), and acpi_GetInteger returning a stack-history-dependent result instead of a defined "no value" status.

Fix

See fix.diff: zero param and convert the "AE_OK but no object" case into AE_AML_NO_RETURN_VALUE.

VERDICT.md
↓ download raw

DF-3793 VERDICT

Status: reproduced (primitive proven at runtime; impact ceiling bounded)

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC), x86_64. ACPI live, debug.acpi.interpreter_slack=0 (supported boot tunable) for the decisive run.

What was run

df3793.kmo (kmod, source in this pack) evaluates no-Return() methods of the live QEMU DSDT twice:

  • Part A (decisive) β€” direct AcpiEvaluateObject() with a caller buffer pre-poisoned with Type=ACPI_TYPE_INTEGER, Value=0xDEADBEEFCAFEBABE: DF3793: path=\_SB_.PCI0.PRES._INI status=AE_OK buf.Length_after=0 DF3793: param.Type=1 param.Value=0xdeadbeefcafebabe <== BUFFER UNTOUCHED, AE_OK DF3793: path=\_SB_.CPUS.CTFY status=AE_OK buf.Length_after=0 DF3793: param.Type=1 param.Value=0xdeadbeefcafebabe <== BUFFER UNTOUCHED, AE_OK AE_OK + Length=0 + poison intact proves ACPICA did not write the caller's buffer β€” inside acpi_GetInteger() (acpi.c:2175-2182) the identical call leaves param uninitialized and ACPI_SUCCESS(status) gates the read of param.Type / param.Integer.Value.

  • Part B (demonstrates stack-history dependence) β€” acpi_GetInteger() on the same paths: with default slack the helper returned AE_OK with number=0 (implicit-return object); with slack off it returned AE_TYPE (garbage Type β‰  1). Same input, different outcome driven by stale stack contents. (The deliberate spray pattern did not land in the helper's frame, so *number receiving the sprayed value was not observed directly; Part A + source lines 2177-2182 make the read unavoidable.)

Why only "Low" severity

The trigger is firmware AML (method that returns no object). On stock config (slack on) most such methods gain an implicit return; slack off β€” a documented supported configuration β€” makes every no-Return method a trigger during ordinary probing. No unprivileged runtime path to feed acpi_GetInteger attacker-chosen paths exists (DO_MCALL does not use it). Worst realistic impact: stale kernel stack data deciding device presence / lid state / thermal limits, and numbers derived from stack garbage being exposed through sysctls (limited disclosure). No corruption, no control flow influence found.

Exploit chain

None (class does not permit one here): uninit read of a stack ACPI_OBJECT influences boolean/integer outputs only.

Fix validation

fix.diff (zero-init param + map the "AE_OK, no object" case to AE_AML_NO_RETURN_VALUE) was applied to the guest's /usr/src copy, kernel rebuilt (make nativekernel) and booted; the kmod re-run then showed acpi_GetInteger -> AE_AML_NO_RETURN_VALUE (defined status, no stack-data dependence). See run_patched.log and fix.diff.

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

Patched kernel (fix.diff applied in guest /usr/src, nativekernel rebuild, reboot) turns the undefined outcome into a defined AE_AML_NO_RETURN_VALUE; no uninit-dependent behavior remains.

run_patched.log: 'acpi_GetInteger -> AE_AML_NO_RETURN_VALUE' for both no-Return paths on the patched kernel.
↓ fix.diffper-fix-DF-3793

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log lines 'BUFFER UNTOUCHED, AE_OK' (AE_OK + Length 0 + poison intact for both no-Return methods); acpi_GetInteger returning AE_TYPE (garbage Type) with slack off vs AE_OK/0 with slack on β€” same input, stack-history-dependent outcome. Source: sys/dev/acpica/acpi.c:2161-2204; ACPICA nsxfeval.c (!Info->ReturnObject -> ReturnBuffer->Length = 0, buffer untouched).

PoC changes

kmod written from scratch (no seed): probes live-DSDT no-Return methods with a poisoned caller buffer, plus stack-spray + real acpi_GetInteger call; needed opt_acpi.h/opt_global.h copied from /usr/obj kernel dir and the acpica Makefile.inc include paths.

Verified recommended fix

Zero-initialize param and map the AE_OK-with-no-object case to AE_AML_NO_RETURN_VALUE instead of reading the untouched buffer.

Verdict

acpi_GetInteger() reads an uninitialized stack ACPI_OBJECT whenever an AML object evaluates successfully but returns no value: ACPICA leaves the caller buffer untouched (Length=0) and returns AE_OK, after which acpi.c:2177-2182 reads param.Type/param.Integer.Value from stale stack. Proven on the guest by evaluating a no-Return method of the live DSDT (_SB.PCI0.PRES._INI and _SB.CPUS.CTFY) with a poisoned caller buffer under debug.acpi.interpreter_slack=0: AE_OK with the poison intact, and acpi_GetInteger's outcome (AE_TYPE vs AE_OK+garbage) varying with stack history. Impact ceiling is limited: stale kernel stack data influences device-present/lid/thermal decisions and values exposed via sysctls (limited disclosure); no corruption, no unprivileged trigger on a stock system (firmware-driven).