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)
PoC verification
Evidence pack
findings/poc/DF-5149 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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/ppsNnode and the pps ppbus-child are pinned for the life of the kernel;kldunloadof 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 registeredppsintr(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 β 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)
uname -aβ DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC.kldstat -v | grep -iE "pps|ppbus"β empty: no pps (and no ppbus) driver registered anywhere on the baseline system.ls /boot/kernel | grep -E "^pp"β onlyppbus.ko(the bus half). There is no pps.ko:dev/misc/pps/pps.cisoptional pps(sys/conf/files:1216) and no module or kernel config on this guest requests it (grep ppbus|lpt|ppi|ppson X86_64_GENERIC matches onlydevice ulpt, the USB printer, line 308).ls -l /dev/pps*β no such device nodes exist.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.- 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 todefault: 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()writesppb_wdtr(0)/ppb_wctr(0)unconditionally and ignoresppb_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_testablefix.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))
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.
No comments yet.