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

Deprecated ACPIIO_SETSLPSTATE ioctl bypasses the pc64 suspend-support gate and drives the unimplemented S2-S4 suspend path (stubbed machdep half), returning false success on failure

Summary

The supported entry point refuses S1-S4 with EOPNOTSUPP because the pc64 machdep half is stubbed - but the deprecated SETSLPSTATE ioctl still calls acpi_SetSleepState directly, bypassing the gate. Guest-reproduced 4/4 as root: ioctl(S3/S4) walks the entire device-tree suspend machinery (dmesg 'device_suspend failed' per call, with bus_generic_suspend's inline PARTIAL RESUME of already-suspended siblings) and returns 0 - false success - because status stays AE_OK through the failure break. On suspend-clean hardware the same path executes a phantom sleep: _PTS -> no-op acpi_sleep_machdep -> SLEPT marked anyway -> _WAK + full DEVICE_RESUME on a machine that never slept, with icu_reinit/elcr_resume never invoked. Root-only ceiling: privileged DoS/driver-state corruption. route-through-ReqSleepState fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5669 Β· 13 files
FileTypeDescriptionSize
setslp.c β€” 1.9 KB view raw
build.sh β€” 190 B view raw
run.sh β€” 189 B view raw
build.log β€” 67 B view raw
run.log β€” 828 B view raw
run.unpriv.log β€” 129 B view raw
run.reqstate.log β€” 385 B view raw
run.s4.log β€” 259 B view raw
dmesg.txt β€” 257 B view raw
env.txt β€” 410 B view raw
fix.diff β€” 501 B view raw
VERDICT.md β€” 4.4 KB ↓ raw
verdict.json β€” 4.5 KB view raw
VERDICT.md
↓ download raw

DF-5669 β€” VERDICT

status: reproduced (privileged local; impact ceiling: dos) β€” confidence: certain for everything demonstrated below.

What reproduced (guest-live, 4/4 deterministic runs)

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), QEMU i440FX, hw.acpi.supported_sleep_state: S3 S4 S5 (see env.txt).

  1. Unprivileged boundary holds β€” open("/dev/acpi", O_RDWR) as uid 1001 β†’ Permission denied (run.unpriv.log). Device is root:wheel 0644 (acpi.c:730), FWRITE checked at acpi.c:3424-3429.
  2. The supported entry point is gated β€” ACPIIO_REQSLPSTATE S3 as root β†’ errno 45 (Operation not supported) (run.reqstate.log), exactly the platform gate at sys/dev/acpica/acpi.c:2417-2420.
  3. The deprecated ioctl bypasses the gate and lies. ACPIIO_SETSLPSTATE S3/S4 as root β†’ ioctl() = 0, errno = 0 while dmesg gains acpi0: warning: acpi_SetSleepState() deprecated... + acpi0: device_suspend failed for every call (4 lines after 4 calls β€” run.log, dmesg.txt). So the kernel walked the whole device tree suspend path (with bus_generic_suspend's inline partial RESUME of already- suspended siblings, sys/kern/subr_bus.c:2894-2908) and then returned success for an operation that (a) is not supported on this platform and (b) failed.

Root cause of the false success: in acpi_EnterSleepState() (sys/dev/acpica/acpi.c:2557), status is set to AE_OK by AcpiGetSleepTypeData() (acpi.c:2597-2605); the DEVICE_SUSPEND != 0 branch only prints and breaks (acpi.c:2624-2627) without setting an error, so return_ACPI_STATUS(status) (acpi.c:2698) yields AE_OK β†’ error = 0 at acpi.c:3455-3456.

What the deeper path would do (code-cited; blocked on this guest)

On this guest a driver deterministically vetoes DEVICE_SUSPEND (silent error; identity not attributed), so the flow stops at stage 2 of 5 (ACPI_SS_GPE_SET). On suspend-clean hardware the same ioctl continues:

  • AcpiEnterSleepStatePrep(state) β€” _PTS + SLP_TYP written to PM1 control (acpi.c:2630-2636);
  • acpi_sleep_machdep(sc, state) β€” no-op stub on pc64 (sys/platform/pc64/acpica/acpi_wakeup.c:51-54); its return value is ignored. On real FreeBSD this function is the actual sleep transition (wake APs, save CPU state, AcpiEnterSleepState from the trampoline); here nothing sleeps;
  • slp_state = ACPI_SS_SLEPT unconditionally (acpi.c:2653);
  • backout: AcpiLeaveSleepState(state) (PM1-control writes, WAKE_STATUS clears, _WAK β€” sys/contrib/dev/acpica/source/components/hardware/ hwsleep.c:408-479) and DEVICE_RESUME(root_bus) (acpi.c:2669-2673) on a machine that never slept;
  • ICU/ELCR/LAPIC state is never resynced after the phantom cycle: icu_reinit() (sys/platform/pc64/icu/icu.c:128) and elcr_resume() (sys/platform/pc64/icu/elcr.c:121) have zero callers (the latter is already tracked as DF-5654 β€” not re-reported here).

This is a full device-tree teardown/rebuild driven through an entry point the platform explicitly declares unsupported β€” unvalidated on pc64 by design of the stubs.

Why not uid0 / not a panic

Not memory corruption β€” a control-flow/logic defect with a privileged-only entry. No unprivileged route exists (proven). On this guest the driver veto prevents the phantom stages, so no wedge/panic was observable; the impact ceiling on capable hardware is a wedged/half-resumed system (privileged DoS).

Fix

Route the deprecated ioctl through the same gate as the modern one (fix.diff): acpi_ReqSleepState(sc, state) already implements the platform policy (S5 allowed β†’ poweroff; S1–S4 β†’ EOPNOTSUPP; range β†’ EINVAL). Also worth a follow-up in acpi_EnterSleepState(): set status = AE_ERROR on the device_suspend failed break so failures are never masked.

Fix validation: not tested β€” this is a Low-severity logic finding, not a reproduced memory-corruption bug, so the single-fix kernel rebuild cycle was not warranted per the audit contract. The diff is trivially reviewable against acpi.c:3451-3456.

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
baseline reproduced→ patch + rebuild →patched clean

fix.diff authored and line-verified against acpi.c:3451-3456 (routes the deprecated ioctl through the acpi_ReqSleepState gate, keeping S5 poweroff working); not kernel-rebuilt/validated because this is a Low-severity logic finding, not reproduced memory corruption, and the audit contract requires the single-fix kernel cycle only for memory-corruption findings.

fix.diff (git-apply-able vs sys/dev/acpica/acpi.c); baseline behavior in run.log/dmesg.txt
↓ fix.diffper-fix-DF-5669

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: 'ioctl() = 0, errno = 0' for the deprecated ioctl vs run.reqstate.log: 'errno = 45 (Operation not supported)' for the gated one; dmesg.txt: one 'acpi0: device_suspend failed' per deprecated-ioctl call (4 lines, 4 calls); run.unpriv.log: unpriv open denied; VERDICT.md: full path trace with the guest-specific driver-veto blocker for the deeper phantom stages

PoC changes

n/a β€” trigger authored fresh for this file's audit (no orchestrator seed existed); ACPIIO_SETSLPSTATE/_IOW('P',3,int) defined inline from sys/dev/acpica/acpiio.h:38 to avoid include-path issues

Verified recommended fix

Route ACPIIO_SETSLPSTATE through acpi_ReqSleepState() so the platform's EOPNOTSUPP gate applies; optionally also set status=AE_ERROR on the device_suspend-failure break in acpi_EnterSleepState.

Verdict

The deprecated-but-live ACPIIO_SETSLPSTATE ioctl (acpi.c:3451) bypasses the platform's explicit 'suspend not supported' gate (acpi_ReqSleepState, acpi.c:2417-2420 -> EOPNOTSUPP) and drives the pc64 suspend machinery whose machdep half is stubbed out (acpi_wakeup.c:51-59, invoked from acpi_machdep.c:60). Reproduced 4/4 on the guest: as root, ioctl(SETSLPSTATE, S3/S4) returns 0 (success) while dmesg records 'device_suspend failed' per call β€” the entire device tree is walked through suspend with bus_generic_suspend's inline partial resume (subr_bus.c:2894-2908), and the failure is masked because acpi_EnterSleepState leaves status=AE_OK from AcpiGetSleepTypeData through the device_suspend-failure break (acpi.c:2624-2627, 2698). Unprivileged users are correctly denied (EPERM on O_RDWR open of root:wheel 0644 /dev/acpi). On suspend-clean hardware the same path continues into a phantom sleep cycle (AcpiEnterSleepStatePrep/_PTS -> no-op acpi_sleep_machdep -> ACPI_SS_SLEPT -> AcpiLeaveSleepState/_WAK + DEVICE_RESUME on a machine that never slept, with icu_reinit/elcr_resume never called β€” zero callers), an unvalidated full device-tree teardown/rebuild on a platform that declares the operation unsupported. Impact ceiling: privileged local DoS / kernel driver-state corruption; no unprivileged escalation path exists.