# 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

* sys/dev/acpica/acpi_acad.c:110-116 (newstatus sentinel + garbage accepted)
* sys/dev/acpica/acpi_acad.c:277-292 (acpi_acad_get_acline re-evaluates _PSR per read)
* sys/dev/acpica/acpi_acad.c:225-236 (sysctl handler exports sc->status)
* sys/dev/acpica/acpi.c:2162-2183 (uninitialized `param` — DF-3793 root cause)
* sys/contrib/dev/acpica/source/components/namespace/nsxfeval.c:541-545
  (AE_OK with untouched ReturnBuffer->Pointer for no-return methods)

## 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.
