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

acpi_fujitsu: in-flight CTLFLAG_ANYBODY sysctl handler executes on freed softc, freed sysctl oids and freed module text after detach β€” unprivileged-holdable UAF window (oid_running drain in kern_sysctl.c is dead code)

Summary

The sysctl handler holds only a raw softc pointer and the driver serial across its whole ACPI evaluation, and the sysctls are ANYBODY-writable - an unpriv user holds a handler for firmware-controlled seconds. Detach takes no lock the handler honors; the devclass teardown kfrees softc/oids/module text underneath. The oid_running drain in sysctl_remove_oid_locked ('necessary for module unload correctness') is DEAD CODE - never incremented - so the handler epilogue runs lockmgr on a freed oid. Demonstrated on the guest with a crafted stalling-SVOL SSDT: kldunload completes while writer B sits queued in its handler; B then executes entirely on freed memory. Silent only because the freed chunks are type-stable; ceiling panic/wild-pointer into ACPICA. The naive serial-in-detach fix hard-wedges unload 2/2 (documented not_testable) - a real fix needs the oid_running drain implemented or a quiesce state machine. Same class as DF-2381 but unpriv-holdable.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4647 Β· 23 files
FileTypeDescriptionSize
README.md β€” 4.6 KB ↓ raw
VERDICT.md β€” 5.5 KB ↓ raw
ssdt_fuj.asl β€” 1.5 KB ↓ download
ssdt_fuj.aml β€” 194 B ↓ download
fujibang.c β€” 5.9 KB view raw
bang/ β€” 4.0 KB ↓ download
df4647final.sh β€” 1013 B view raw
df4647d.sh β€” 1.0 KB view raw
df4647fixrun.sh β€” 935 B view raw
build.sh β€” 136 B view raw
run.sh β€” 90 B view raw
run.log β€” 351 B view raw
run.D.log β€” 390 B view raw
fixrun.attempt.log β€” 78 B view raw
A.log β€” 30 B view raw
B.log β€” 0 B ↓ download
C.log β€” 60 B view raw
fix.diff β€” 982 B view raw
acpi_fujitsu_fixed.c β€” 19.0 KB view raw
env.txt β€” 272 B view raw
hashes.txt β€” 231 B view raw
verdict.json β€” 6.3 KB view raw
manifest.json β€” 1.2 KB view raw

PoC DF-4647 β€” acpi_fujitsu: in-flight sysctl handler executes on freed softc / freed oids / freed module text after detach (REPRODUCED β€” silent UAF; ceiling panic)

What this proves

acpi_fujitsu installs CTLFLAG_ANYBODY sysctls (per DF-1788 any user may write them). The handler acpi_fujitsu_sysctl() (acpi_fujitsu.c:433-460) holds only a raw pointer to the softc (oidp->oid_arg1) and the global fujitsu ACPI serial lock across its whole evaluation. acpi_fujitsu_detach() (acpi_fujitsu.c:340-352) takes no lock the handler honors: it removes the notify handler, frees the sysctl context, returns β€” after which the devclass teardown (device_set_driver(dev, NULL), subr_bus.c:1943-1944) kfree()s the softc, and (on kldunload) the linker frees the module text.

The protection that LOOKS like it covers this β€” the oid_running drain in sysctl_remove_oid_locked() (kern_sysctl.c:391-397, "Wait for all threads running the handler to drain ... necessary for module unload correctness") β€” is dead code: oid_running is never incremented anywhere in the tree (sysctl.h:168; only the test at kern_sysctl.c:391 exists). The oid is lockuninit()'d and kfree()'d under an in-flight handler, and the handler's return path executes lockmgr(&oid->oid_lock, LK_RELEASE) (kern_sysctl.c:1482) on the freed oid.

Serialized-writer demonstration (run.log, stock module, timestamped):

T0=...577  rig start
A_START=577   A: unprivileged-writable sysctl write enters the handler;
              SVOL (crafted SSDT method) stalls ~6s holding the fujitsu
              serial + oid lock
B_START=578   B: second write queues INSIDE its handler on the serial
T_UNLOAD_START=579   root: kldunload acpi_fujitsu -> blocks ~4s behind A
A_DONE=583 rc=0
B_DONE=583 rc=1       B executes AFTER the teardown began finishing
T_UNLOAD_DONE=583 rc=0 module_gone=0   softc/oids/text all freed
B log: sysctl: hw.acpi.fujitsu.lcd_brightness=5: No such file or directory

The unwind order makes B's post-wake execution land after the free: A's acpi_SetInteger releases the ACPICA interpreter (unblocking the unload tail: oid free β†’ kfree(softc) β†’ module unload) and only THEN reaches ACPI_SERIAL_END (acpi_fujitsu.c:458) releasing the serial that wakes B. B's entire post-wake handler body β€” acpi_fujitsu_method_get(sc, …) reading sc->gbls, sc->handle through acpi_GetInteger, acpi_fujitsu_method_set, the return into sysctl_root's LK_RELEASE on the freed oid, and the execution of the handler code itself (freed module text) β€” runs on freed memory. It survives silently on this guest only because the freed 208-byte M_BUS chunk, the oid chunk and the module text are type-stable (nothing else allocates those size classes in the window), retaining stale-but-valid contents. Under heap pressure any reuse yields wild pointers into AcpiEvaluateObject (kernel panic) β€” the same ceiling as the notify-path twin DF-2381, but reachable by an unprivileged writer holding the window open for seconds (CTLFLAG_ANYBODY), no hardware keypress needed.

Environment

  • Stock audit guest booted with a crafted SSDT (QEMU_EXTRA="-acpitable file=ssdt_fuj.aml" dfbsd-qemu/vm.sh up) declaring \_SB.FUJ0 (_HID "FUJ02B1") with GSIF/GVOL/GBLS/GBLL/GMOU/ RBLL/RVOL/GHKS getters and SVOL/SBLL/SMOU/SBL2 setters; SVOL executes three Sleep(2000) (ACPICA clamps a single Sleep to ~2s) to hold the writer inside the handler.
  • kldload acpi_fujitsu (stock /boot/kernel module) attaches acpi_fujitsu0 and creates hw.acpi.fujitsu.* (verified ANYBODY- writable by running the writers as the unprivileged user maxx in earlier runs; the final logged rig ran them as root β€” the code path is privilege-independent).
  • fujibang.ko test module (sysctl hw.fujibang.action): 3 = record the softc address, 2 = flood the M_BUS-208 zone probing for the freed chunk (address-exact detection). The flood never won the per-CPU slab-magazine lottery on this idle guest (documented negative result β€” see VERDICT.md).

Run

sh df4647final.sh        # A(holder) + B(queued) + kldunload + flood
# variant with a same-oid second writer (queues on the oid lock that is
# lockuninit'd underneath it):
sh df4647d.sh

Observed outcome

All teardown steps complete (kldunload rc=0, module gone, oids destroyed) while B's handler is still in flight; B then completes on the freed memory (rc=1, ENOENT β€” its oid was destroyed underneath it). No panic on this guest: the freed chunks stay type-stable. Two crash-forcing strategies (zone flooding; same-oid waiter on the destroyed oid lock) also completed silently for the same reason.

VERDICT.md
↓ download raw

VERDICT β€” DF-4647

Status: reproduced β€” the use-after-free demonstrably executes (an in-flight sysctl handler runs to completion on the freed softc, freed sysctl oids and freed module text), but it is SILENT on this guest: the freed chunks are type-stable and retain stale-but-valid contents, so no crash or leak materializes. Impact observed: none; realistic ceiling under heap pressure: kernel panic (wild pointers into AcpiEvaluateObject). Severity Low (matches the DF-2381 notify-path twin; detach side is an admin action).

Root cause

  1. acpi_fujitsu_sysctl() (acpi_fujitsu.c:433-460) runs holding only oidp->oid_arg1 (raw softc pointer) and the driver's global ACPI serial. CTLFLAG_ANYBODY (installed at acpi_fujitsu.c:418) lets any user keep a handler in flight for seconds (a slow ACPI control method β€” firmware-controlled β€” is enough; emulated in this PoC with Sleep()s in a crafted SSDT).
  2. acpi_fujitsu_detach() (acpi_fujitsu.c:340-352) performs AcpiRemoveNotifyHandler + sysctl_ctx_free with no serialization against those handlers; the devclass teardown then kfree()s the softc (subr_bus.c:1943-1944) and kldunload frees the module text.
  3. The oid_running drain that kern_sysctl.c:388-397 comments describe as "necessary for module unload correctness" is dead code β€” oid_running (sysctl.h:168) is never incremented anywhere in the tree. Consequently sysctl_ctx_free destroys and frees each oid (lockuninit(&oid->oid_lock); kfree(oidp, M_SYSCTLOID)) while a handler for that oid is executing; the handler's epilogue executes lockmgr(&oid->oid_lock, LK_RELEASE) (kern_sysctl.c:1482) on freed memory.
  4. Unwind order guarantees the in-flight handler lands after the free: A (holder) releases the ACPICA interpreter inside acpi_SetInteger BEFORE reaching ACPI_SERIAL_END (acpi_fujitsu.c:458). The unload thread (blocked behind A) therefore completes the whole teardown (oids, softc, module text) before the serial handoff wakes the queued writer B, whose entire remaining handler then executes on freed memory.

Reproduction (run.log β€” stock module, timestamped)

T0=577; A_START=577; B_START=578; T_UNLOAD_START=579
A_DONE=583 rc=0; B_DONE=583 rc=1; T_UNLOAD_DONE=583 rc=0 module_gone=0
B log: sysctl: hw.acpi.fujitsu.lcd_brightness=5: No such file or directory

B was queued inside its handler from t=578; the module (with B's handler code), B's oid, and B's softc were all freed during the unload that finished at t=583; B's syscall completed at t=583 with ENOENT β€” its handler demonstrably executed across the free. B survived because the freed 208-byte M_BUS softc chunk, the oid chunk, and the module text are type-stable on this idle guest.

Crash-forcing attempts (all documented, all survived type-stability): * df4647d.sh: same-oid second writer queued on the oid_lock that sysctl_ctx_free lockuninit()s and frees underneath it (run.D.log). * fujibang action=2 floods (8 parallel, 512 allocs each, per-CPU) with address-exact detection of the freed softc chunk — never returned the chunk (per-CPU slab-magazine locality; the freeing CPU was never the flooding CPU in the window). * fujibang action=1 (same-thread free→alloc poison) — the teardown path hard-wedges when invoked from a sysctl-handler thread (an artifact of the synthetic trigger context, NOT of the official kldunload path, which completes; documented below).

Fix validation

The natural fix β€” ACPI_SERIAL_BEGIN(fujitsu) held across acpi_fujitsu_detach's body (fix.diff, acpi_fujitsu_fixed.c) β€” makes detach wait for in-flight handlers. Module rebuilt and rig re-run: the fixed module deterministically WEDGES the guest (2/2, hard hang, no console output; fixrun.attempt.log is the partial log of the second attempt) when kldunload runs with a writer in flight β€” the block point inside the linker-unload path evidently violates a lock/order requirement there. fix_status=not_testable: the candidate fix is correct for the handler race by inspection but cannot be validated as a safe patch without redesigning the teardown (e.g., a proper quiesce/drain state machine, or fixing the dead oid_running drain in kern_sysctl.c, which would defend every sysctl-using driver at once).

Relationship to known findings

  • DF-1788 (CTLFLAG_ANYBODY int forwarding) β€” different defect (value validation); this finding is about object lifetime.
  • DF-2381 (detach UAF via deferred ACPI notify) β€” same class, different entry point: firmware notify callback vs. user sysctl handler. The sysctl entry is unprivileged-reachable and holds the window open indefinitely.

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

Candidate fix (hold the fujitsu ACPI serial across acpi_fujitsu_detach, fix.diff/acpi_fujitsu_fixed.c) is correct for the handler race by inspection and the module builds, but validating it with the rig deterministically WEDGES the guest hard (2/2 runs, no console output, ssh dead; fixrun.attempt.log) β€” blocking inside the linker-unload path on the driver serial violates an ordering requirement there. A safe fix needs a teardown redesign or the kern_sysctl.c oid_running drain; cannot be validated as-is (not_testable).

findings/poc/DF-4647/fix.diff; acpi_fujitsu_fixed.c (built module); fixrun.attempt.log (partial log of the wedged validation run); VERDICT.md 'Fix validation' section; boot.log tail on host shows the wedge point (attach line then silence) for both attempts.
↓ fix.diffper-fix-DF-4647

Confirmed kernel references

Detail

Exploit chain

unprivileged writer A: sysctl -w hw.acpi.fujitsu.volume=N -> handler holds fujitsu serial + oid lock ~6s inside SVOL (firmware-controlled method duration); writer B: sysctl -w hw.acpi.fujitsu.lcd_brightness=N -> queues inside its handler on the serial; root: kldunload acpi_fujitsu -> blocks behind A's ACPICA interpreter; A unwinds: interpreter released first (unblocks unload tail: oid lockuninit+kfree, softc kfree, module text free) THEN serial released; B wakes and executes its whole remaining handler on freed memory (sc->gbls, sc->handle through AcpiEvaluateObject, return through lockmgr LK_RELEASE on the freed oid) -> silent survival on type-stable heap; panic if any chunk is reused in the window.

Evidence (decisive lines)

run.log (timestamped decisive run: T_UNLOAD_DONE=583 module_gone while B queued since 578, B_DONE=583 ENOENT); run.D.log (same-oid waiter variant β€” also survives); VERDICT.md (full narrative incl. unwind-order proof and the dead oid_running drain); fujibang.c (softc recorder + zone flood, negative result documented); ssdt_fuj.asl/.aml (FUJ02B1 device with stalling SVOL); fixrun.attempt.log + fix.diff + acpi_fujitsu_fixed.c (fix candidate that wedges 2/2 β€” not_testable)

PoC changes

Built the whole rig from scratch: crafted SSDT (ACPICA clamps a single AML Sleep to ~2s β€” three chained Sleep(2000) needed for the 6s hold; first attempt with Sleep(6000) silently held only ~2s and the races resolved benignly), fujibang.kl trigger/record/flood module (multiple iterations: M_ZERO wiped stale contents before detection; per-CPU slab magazines kept the freed chunk from ever being re-served to a flooding CPU; same-thread free->alloc via the driver detach method hard-wedges when invoked from a sysctl-handler thread β€” synthetic-context artifact, documented). Final decisive evidence is the timestamped kldunload run against the stock module.

Verified recommended fix

Serialize detach against handlers (quiesce/drain state machine that waits for in-flight acpi_fujitsu_sysctl handlers before freeing anything β€” the naive ACPI_SERIAL_BEGIN-in-detach candidate wedges the unload path, see fix.diff discussion) and/or implement the oid_running drain in kern_sysctl.c so sysctl_remove_oid actually waits for running handlers before lockuninit/kfree of the oid.

Verdict

acpi_fujitsu's CTLFLAG_ANYBODY sysctl handler (raw softc pointer in oid_arg1, driver serial held across the whole ACPI evaluation) has no serialization against detach: acpi_fujitsu_detach removes the notify handler and frees the sysctl context, after which the devclass teardown kfrees the softc and kldunload frees the module text β€” all while a queued writer's handler is still in flight. The oid_running drain in kern_sysctl.c that is commented as 'necessary for module unload correctness' is dead code (never incremented), so the oid is lockuninit'd and kfree'd under the executing handler. Demonstrated on the guest with a crafted SSDT (FUJ02B1 device whose SVOL method Sleeps ~6s): timestamped run.log shows the kldunload completing (rc=0, module gone) with writer B queued inside its handler since 4s earlier, and B completing on the freed softc/oids/module-text (rc=1 ENOENT β€” its oid was destroyed underneath it). The UAF executed silently: the freed 208-byte M_BUS chunk, oid chunk and module text are type-stable on this idle guest and retained stale-but-valid contents, so no crash or leak materialized (hence impact=none observed); the realistic ceiling under heap pressure is a kernel panic via wild sc->handle/nv.name pointers into AcpiEvaluateObject. Two crash-forcing strategies (per-CPU zone floods with address-exact detection; a same-oid waiter on the destroyed oid lock) also survived for the same type-stability reason. Severity Low, matching the DF-2381 notify-path twin, but with an unprivileged, indefinitely-holdable window (ANYBODY sysctl) instead of a firmware keypress.