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)
PoC verification
Evidence pack
findings/poc/DF-3793 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.Valueat 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 = ¶m;
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_GetIntegerinclude_STA(acpi_DeviceIsPresentacpi.c:2014,acpi_BatteryIsPresentacpi.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
debug.acpi.interpreter_slack=0in/boot/loader.conf.local, reboot.- Build:
makein this directory (Makefile below; needs/usr/obj/usr/src/sys/X86_64_GENERIC/opt_acpi.h+opt_global.hcopied in). 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.
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 withType=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_OKAE_OK+Length=0+ poison intact proves ACPICA did not write the caller's buffer β insideacpi_GetInteger()(acpi.c:2175-2182) the identical call leavesparamuninitialized andACPI_SUCCESS(status)gates the read ofparam.Type/param.Integer.Value. -
Part B (demonstrates stack-history dependence) β
acpi_GetInteger()on the same paths: with default slack the helper returnedAE_OKwithnumber=0(implicit-return object); with slack off it returnedAE_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*numberreceiving 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
fixedPatched 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.
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).
No comments yet.