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

acpi_dock: deferred AcpiOsExecute(acpi_dock_attach_later) callbacks jump into freed module text after kldunload β€” OSL has no drain (AcpiOsWaitEventsComplete unimplemented/stubbed)

Summary

attach_later is queued per _EJD-dependent on every dock insert; DF's AcpiOsExecute is fire-and-forget with no module refcount and AcpiOsWaitEventsComplete is #define-stubbed out (XXX TBI). kldunload frees module text while callbacks/notify handler are pending -> jump into unmapped KVA. Distinct sink from DF-1905/2380 (RemoveNotifyHandler alone does not close it). REPRODUCED deterministically with a crafted dock SSDT: Fatal trap 12 at RIP == freed acpi_dock.ko+0x57e; fix (OSL drain counter + unstub + detach RemoveNotifyHandler/drain) validated on full kernel rebuild - same trigger survives, unload blocks ~1s draining. Also live-verified the DF-1905/2380 mechanism for the first time.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4823 Β· 14 files
FileTypeDescriptionSize
ssdt_dock.dsl trigger-src crafted SSDT: dock station w/ 3s-sleep _STA, 96 _EJD dependents, DFNT notify trigger 24.5 KB ↓ download
ssdt_dock.aml trigger-blob compiled AML (iasl 20251212, guest) 4.4 KB ↓ download
dfnt.c trigger-src kld module evaluating \\_SB_.DFNT at MOD_LOAD (fires the dock notify) 1.6 KB view raw
run_dockrace.sh run-script deterministic trigger sequence 1.5 KB view raw
dockrace.c trigger-src brute-force kldload(2)/kldunload(2) racer (survived 3000 cycles; superseded) 917 B view raw
run.log run-log full serial-console capture of the decisive baseline run incl. kldstat module addresses 12.4 KB view raw
panic.txt panic-proof Fatal trap 12, RIP=freed acpi_dock.ko+0x57e, DDB wedge 1.3 KB view raw
run.fixed.log run-log same trigger on fix.diff-patched kernel: SURVIVED, no fatal 642 B view raw
build.log build-log dfnt.ko module build output 1.2 KB view raw
fix_build.log fix-log kernel build completion lines (nativekernel, 0 errors) 184 B view raw
fix_install.log fix-log installkernel output 83.9 KB view raw
fix.diff suggested-fix OSL drain + platform-header unstub + acpi_dock_detach RemoveNotifyHandler+drain 2.9 KB view raw
env.txt env guest uname, versions 661 B view raw
mechanism_check_df1906/ negative-result live refutation of DF-1906's sysctl-vs-unload UAF mechanism (pcpu sysctllock drain blocks kldunload 4s) 4.0 KB ↓ download

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (OsdSchedule.c drain + acdragonfly.h unstub + acpi_dock_detach RemoveNotifyHandler+drain) applied in guest /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC (0 errors) + installkernel + reboot; exact same trigger re-run: NO panic, guest alive, kldunload blocks ~1s while draining the parked handler + 96 attach_later callbacks, then completes; follow-up kldload/kldunload cycles clean. Baseline (stock kernel) same trigger: Fatal trap 12 into freed module text + DDB wedge.

fix.diff; fix_build.log (kernel build completed 0 errors); fix_install.log; run.fixed.log (SURVIVED, no fatal); timing rerun in VERDICT.md (kldunload blocked_for=1s then completed, NO-FATAL)
↓ fix.diffper-fix-DF-4823

Confirmed kernel references

Detail

Exploit chain

root: kldload acpi_dock (docks via crafted SSDT, queues 96 attach_later) -> sysctl hw.acpi.dock.status=0 -> kldload dfnt.ko (fires Notify; kthread parks 3s inside acpi_dock_notify_handler via _STA Sleep) -> kldunload acpi_dock completes during the stall (detach takes no drain) -> module text freed -> kthread resumes/jumps into freed KVA -> Fatal trap 12 (instruction fetch, page not present) -> unrecoverable DDB wedge. No uid=0 chain: trigger requires root, primitive is a jump to unmapped module text (offset not attacker-chosen); class is correctness/robustness, same bar as DF-1905.

Evidence (decisive lines)

run.log (full serial console: kldstat module base 0xffffffff82600000 -> Fatal trap 12 RIP 0xffffffff8260057e); panic.txt (trap dump + db> wedge); run.fixed.log (patched kernel: SURVIVED, no fatal); fix_build.log/fix_install.log (nativekernel 0 errors, install completed); mechanism_check_df1906/run.log (adjacent DF-1906 refutation: kldunload blocked 4s by pcpu sysctllock drain)

PoC changes

Crafted the whole harness (no seed existed): SSDT with dock station (PNP0C15 + _DCK + 3s-sleep _STA) + 96 _EJD dependents (fixed 5-char ACPI NameSegs -> 4-char DFxx) + DFNT Notify method; dfnt.ko trigger module (minimal AcpiEvaluateObject extern -- full ACPICA include maze fails in standalone kmod builds); brute-force racers (300 shell + 3000 tight C kldload(2)/kldunload(2) cycles) survived, superseded by the deterministic _STA-Sleep stall trigger.

Verified recommended fix

Implement AcpiOsWaitEventsComplete() in OsdSchedule.c (pending-callback counter + drain), remove the acdragonfly.h XXX-TBI stub, and make acpi_dock_detach() call AcpiRemoveNotifyHandler + AcpiOsWaitEventsComplete before sysctl_ctx_free (fix.diff, validated).

Verdict

REPRODUCED on the DF 6.5-DEVELOPMENT guest with a crafted dock SSDT: acpi_dock_insert_child (acpi_dock.c:237) queues AcpiOsExecute(OSL_NOTIFY_HANDLER, acpi_dock_attach_later, dev) with zero lifetime protection -- DF's OSL has no AcpiOsWaitEventsComplete (platform header stubs it out) and no module refcount -- so kldunload acpi_dock frees module text while deferred callbacks (and the mid-execution notify handler, DF-1905/2380 family) still reference it. Deterministic trigger (3s-sleeping _STA parks the acpi_task kthread inside the handler; kldunload lands in the stall) yields Fatal trap 12 with RIP == 0xffffffff8260057e, +0x57e into the freed acpi_dock.ko mapping (kldstat: 0xffffffff82600000), DDB 'Stopped at -0x7d9ffa82'. Root-gated (kldload/kldunload + ACPI event) => Low severity, memcorrupt family. Fix (OSL drain + AcpiRemoveNotifyHandler + drain-in-detach) validated by full kernel rebuild: same trigger SURVIVES, kldunload blocks ~1s while draining then completes, follow-up cycles clean.