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)
PoC verification
Evidence pack
findings/poc/DF-4647 Β· 23 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 threeSleep(2000)(ACPICA clamps a single Sleep to ~2s) to hold the writer inside the handler. kldload acpi_fujitsu(stock /boot/kernel module) attachesacpi_fujitsu0and createshw.acpi.fujitsu.*(verified ANYBODY- writable by running the writers as the unprivileged usermaxxin earlier runs; the final logged rig ran them as root β the code path is privilege-independent).fujibang.kotest module (sysctlhw.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 β 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
acpi_fujitsu_sysctl()(acpi_fujitsu.c:433-460) runs holding onlyoidp->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 withSleep()s in a crafted SSDT).acpi_fujitsu_detach()(acpi_fujitsu.c:340-352) performsAcpiRemoveNotifyHandler+sysctl_ctx_freewith no serialization against those handlers; the devclass teardown thenkfree()s the softc (subr_bus.c:1943-1944) andkldunloadfrees the module text.- The
oid_runningdrain 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. Consequentlysysctl_ctx_freedestroys and frees each oid (lockuninit(&oid->oid_lock);kfree(oidp, M_SYSCTLOID)) while a handler for that oid is executing; the handler's epilogue executeslockmgr(&oid->oid_lock, LK_RELEASE)(kern_sysctl.c:1482) on freed memory. - Unwind order guarantees the in-flight handler lands after the free:
A (holder) releases the ACPICA interpreter inside
acpi_SetIntegerBEFORE reachingACPI_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
- sys/dev/acpica/acpi_fujitsu/acpi_fujitsu.c:340-352 (detach β no handler serialization)
- sys/dev/acpica/acpi_fujitsu/acpi_fujitsu.c:415-420 (CTLFLAG_ANYBODY proc sysctls)
- sys/dev/acpica/acpi_fujitsu/acpi_fujitsu.c:433-460 (handler: raw softc pointer, serial held across evaluation)
- sys/kern/kern_sysctl.c:388-397 (dead oid_running drain β never incremented)
- sys/kern/kern_sysctl.c:1455-1483 (oid_lock held across handler; LK_RELEASE on possibly-freed oid)
- sys/kern/subr_bus.c:1943-1944 (softc kfree at device_set_driver)
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_testableCandidate 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.
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.
No comments yet.