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

pps driver has no device_detach and never destroy_dev()s its make_dev() cdev (DF-5006/5074/5091 teardown-gap family): /dev/ppsN and the ppbus child are permanent, module unload is structurally blocked, and a future naive detach is an interrupt-context UAF

Summary

make_dev handle discarded, no detach method - the pps-specific hazard: an open pps owns the ppbus with ppsintr registered, so a future detach without an open-guard frees the softc under a live IRQ handler. Structurally non-UAF today. NOTE: the conventions' 'pps QEMU-live' is stale for this snapshot (pps is optional, not built, no parallel port). Store-cdev + guarded-detach fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5149 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 5.0 KB ↓ raw
trigger.sh β€” 1.7 KB view raw
build.sh β€” 133 B view raw
run.sh β€” 160 B view raw
build.log β€” 88 B view raw
run.log β€” 880 B view raw
env.txt β€” 807 B view raw
fix.diff β€” 1.4 KB view raw
manifest.json β€” 930 B view raw
verdict.json β€” 4.1 KB view raw

DF-5149 β€” pps driver has no device_detach and never destroys its cdev (DF-5006/5074/5091 teardown-gap family)

File: sys/dev/misc/pps/pps.c (pass 2) Severity: Low (structural; no reachable teardown path today) Β· Confidence: certain Status: not_reproduced (runtime) β€” see verdict.json / VERDICT.md

What

ppsattach() creates /dev/ppsN via make_dev() (sys/dev/misc/pps/pps.c:103-104) and the driver's method table has only identify/probe/attach β€” no device_detach (pps.c:190-197). The cdev returned by make_dev() is discarded, so no destroy_dev() can ever happen, and the ppbus child is permanent.

Why it matters

  • The /dev/ppsN node and the pps ppbus-child are pinned for the life of the kernel; kldunload of a module-built pps is structurally blocked (missing DETACH β†’ ENXIO β†’ unload aborts before any softc free) β€” the same chicken-and-egg as DF-5074/DF-5091: non-UAF today, guaranteed UAF the day someone adds a naive detach, because an open pps owns the ppbus and a registered ppsintr (pps.c:118-133).

Build / Run / Expected

./build.sh          # nothing to build (structural finding; sh -n syntax check)
./run.sh            # runs trigger.sh as root on the QEMU guest

Expected (see run.log): pps driver registered nowhere; no pps.ko in /boot/kernel (only ppbus.ko); not in X86_64_GENERIC; kldload ppbus attaches nothing (QEMU has no ISA parallel port) and unloads cleanly; no /dev/pps*; guest stays up.

Fix

fix.diff β€” add ppsdetach() that refuses to detach while pps_open (EBUSY) and otherwise destroy_dev()s the stored cdev; store the cdev in the softc. git apply --check clean against sys/ (not applied).

Artifacts

trigger.sh build.sh run.sh build.log run.log env.txt fix.diff manifest.json verdict.json VERDICT.md

VERDICT.md
↓ download raw

VERDICT β€” DF-5149 (pps no-detach / make_dev-without-destroy_dev)

status: not_reproduced | reproduced: false | impact: none | confidence: certain (structural gap) guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 ... x86_64 (X86_64_GENERIC), up, clean

What was claimed

ppsattach() discards the cdev returned by make_dev() (sys/dev/misc/pps/pps.c:103-104) and the driver method table has no device_detach (pps.c:190-197 β€” only identify/probe/attach). Consequences: /dev/ppsN and the ppbus child are kernel-lifetime objects; a module-built pps can never unload; and any future naive detach would free struct pps_data while a client has the device open β€” at which point pps owns the ppbus and has ppsintr registered (pps.c:118-133), i.e. a guaranteed UAF-on-interrupt. Family: DF-5006 (acpi_cpu), DF-5074 (acpi_acad), DF-5091 (acpi_button).

What was run (see run.log, trigger.sh)

  1. uname -a β€” DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC.
  2. kldstat -v | grep -iE "pps|ppbus" β€” empty: no pps (and no ppbus) driver registered anywhere on the baseline system.
  3. ls /boot/kernel | grep -E "^pp" β€” only ppbus.ko (the bus half). There is no pps.ko: dev/misc/pps/pps.c is optional pps (sys/conf/files:1216) and no module or kernel config on this guest requests it (grep ppbus|lpt|ppi|pps on X86_64_GENERIC matches only device ulpt, the USB printer, line 308).
  4. ls -l /dev/pps* β€” no such device nodes exist.
  5. kldload ppbus β†’ loads, but dmesg gains no attach lines and no /dev/ppi*, /dev/lpt*, /dev/pps* appear: QEMU exposes no ISA parallel port, so no ppbus bus instance ever probes. kldunload ppbus β†’ rc=0 (it owned zero devices). Guest residue: none.
  6. Guest alive (uptime), no panic.

Why not_reproduced (honest classification)

The structural gap is certain from source. The runtime half cannot even be set up on this guest: the pps driver code is present in neither the builtin kernel nor any loadable module, and even with ppbus.ko loaded there is no parallel-port hardware for a ppbus (hence pps) device to attach to. So no /dev/ppsN exists, nothing to open, nothing to unload, no fault to trigger. (Note: the audit conventions list "lpt/ppi parallel ... pps" as QEMU-live; that is stale for this snapshot β€” recorded here for the orchestrator.)

Exploit chain: none possible on any current configuration. Unprivileged users cannot kldload/kldunload (priv required); there is no devd/devctl detach operation; the missing DETACH method makes module unload abort before freeing anything β€” the same structural safety DF-5074/DF-5091 documented.

Exploitability ceiling (if a detach path is ever added)

device_detach(pps) frees struct pps_data while an open fd keeps pps_open == 1 (ppbus owned, ppsintr registered) β†’ next parallel-port IRQ executes ppsintr on freed memory (pps.c:159-174) β†’ UAF with hardware-timed trigger. The proposed fix returns EBUSY while open, which closes exactly that window, plus destroy_dev() for the leaked cdev.

Pass-2 depth recorded alongside (killed candidates)

  • ioctl size/dir forging: pps_ioctl() switches on the full cmd word (kern_clock.c:1649); mapped_ioctl() sizes the kernel buffer from IOCPARM_LEN(cmd) (sys_generic.c:668-697). A forged _IO('1',6)-style cmd falls to default: ENOTTY β€” no OOB via size confusion.
  • intr handler accumulation across open/close: killed β€” ppb_release_bus() force-tears the registration down (sys/bus/ppbus/ppbconf.c:514-521), so the ppsclose comment is accurate.
  • SETPARAMS mode bits: masked by ppscap (pps.c:76 + kern_clock.c:1656); ECHOCLEAR/CAPTURECLEAR are not in the cap set for this driver.
  • user assert/clear offsets: only ever added into this instance's timestamps (kern_clock.c:1782-1788) β€” output distortion of one's own FETCH, not memory unsafety.
  • FETCH timeout: tsleep(PCATCH) (kern_clock.c:1626) β€” killable; no callout to drain.
  • NULL softc via bogus minor: unreachable β€” devfs only exposes nodes made by make_dev (unit == attached device), no clone handler.
  • DF-2149 aggravation observed while auditing close: ppsclose() writes ppb_wdtr(0)/ppb_wctr(0) unconditionally and ignores ppb_release_bus()'s return (pps.c:149-153), so a close by a non-owning opener (possible because of the boolean open flag) writes bus registers without owning the bus. Same root cause as DF-2149 β€” recorded here, not re-filed.

Fix

fix.diff: store the cdev in the softc; add ppsdetach() returning EBUSY while pps_open, else destroy_dev(). git apply --check clean; not build-validated (nothing to A/B on this guest β€” see verdict.json fix_status: not_testable).

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

fix.diff authored and git apply --check clean against sys/ but not build-validated: the finding produced no runtime fault on the baseline guest (the pps driver is not compiled into any kernel or module here and no parallel port exists to attach a ppbus), so there is no bad behavior to A/B. Fix validation is mandatory only when a memory-corruption finding reproduces.

findings/poc/DF-5149/fix.diff (proposed, unapplied; stores pps_cdev in struct pps_data, adds ppsdetach() with the pps_open EBUSY guard and destroy_dev(), registers DEVMETHOD(device_detach, ppsdetach))
↓ fix.diffper-fix-DF-5149

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (decisive: 'kldstat -v' grep for pps/ppbus empty; 'ls /boot/kernel | grep ^pp' shows only ppbus.ko; grep on X86_64_GENERIC matches only ulpt:308; kldload ppbus adds no dmesg attach lines and no /dev/pps*|/dev/lpt*|/dev/ppi*; kldunload ppbus rc=0; guest stays up). VERDICT.md for the full path:line trace plus the pass-2 killed candidates (ioctl size forging, intr teardown, SETPARAMS cap masking, offset misuse, FETCH timeout, NULL-softc-by-minor); fix.diff for the proposed detach (git apply --check clean, not applied).

PoC changes

None -- structural finding; trigger is a plain /bin/sh runtime demonstration (driver/code/node absence + ppbus load-unload round-trip), no seed code to repair.

Verified recommended fix

Store the make_dev() cdev in the softc and add ppsdetach() that returns EBUSY while pps_open and otherwise destroy_dev()s the node -- see fix.diff.

Verdict

Structural teardown gap confirmed in source; the runtime half cannot exist on this guest. ppsattach() discards the cdev from make_dev() (sys/dev/misc/pps/pps.c:103-104) and the method table has no device_detach (pps.c:190-197), so /dev/ppsN and the ppbus child are kernel-lifetime objects and a module-built pps can never unload (missing DETACH -> ENXIO -> unload aborts before any free) -- the same structurally-non-UAF-today conclusion as DF-5006/DF-5074/DF-5091, with the added twist that an open pps owns the ppbus and a registered ppsintr (pps.c:118-133), making any future naive detach an immediate interrupt-context UAF. On the guest: no pps/ppbus drivers registered anywhere (kldstat -v), no pps.ko in /boot/kernel (dev/misc/pps/pps.c is 'optional pps', sys/conf/files:1216; X86_64_GENERIC has only ulpt), and QEMU exposes no ISA parallel port -- kldload ppbus attached nothing and unloaded cleanly, so /dev/pps* can never appear on this snapshot (the audit conventions' 'lpt/ppi/pps QEMU-live' note is stale). Low severity; cheap fix belongs in-tree.