DF-2804 / verdict.json
{ "finding_id": "DF-2804", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "Reproduced on the stock INVARIANTS guest: a root reader that initiates the udev queue (UDEVPROP getdevs) and closes /dev/udev without reading leaks every undelivered event dictionary permanently. 30k unprivileged pty cycles left 58.8K prop-dict allocations (~24 MB across propdict/prop_dictionary/propstng/prop_string) after close, zero reclaim after 10s idle, exactly doubled after a second cycle (117K / 22.0M + 20.2M), while the identical churn drained by an active reader leaked nothing (control flat). Root cause: the event's single ev_dict reference (sys/kern/kern_udev.c:508-512) is released only on the read path (:571); the close path reaps events with a bare objcache_put and no prop_object_release (:534-545, :721-726) and abandons events beyond the marker when the last reader leaves. Repeatable without limit -> kernel memory exhaustion; root/wheel gated (same /dev/udev 0600 gate as DF-0055), event source unprivileged.", "exploit_chain": "", "evidence": [ "run.log: S0 propdict 333 (62.4K) -> S1 58.8K (11.0M) after close-without-read cycle 1; S2 identical after 10s idle (permanent)", "run.log: S3 propdict 117K (22.0M) after cycle 2 (~2x monotone accumulation)", "run.log: control S4 propdict 117K (22.0M) flat after same churn with an active draining reader ('read 60000 events') - churn+drain does not leak", "run.log: udev malloc domain returns to ~baseline after close (event structs reaped by objcache) while propdict stays elevated - proves objcache_put without prop_object_release", "harness.c stall mode (initiate, sleep, close without reading), churn.c (posix_openpt loop as unprivileged user)" ], "kernel_refs": [ "sys/kern/kern_udev.c:508-512", "sys/kern/kern_udev.c:571", "sys/kern/kern_udev.c:534-545", "sys/kern/kern_udev.c:709-744", "sys/kern/kern_udev.c:721-726", "sys/vfs/devfs/devfs_core.c:1428", "sys/vfs/devfs/devfs_core.c:1451" ], "poc_changes": "Harness written from scratch (no seed); same bring-up fixes as DF-2803 (sigaction instead of signal for the read-mode alarm; churn arg parsing).", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 95, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -o harness harness.c -lprop && cc -O2 -Wall -o churn churn.c", "run_cmd": "sh /tmp/df2804_run.sh", "code_hash": "181b484caf4890bb7d42d37a80de710ec7ec40003203d0999f4a6364a607abdc", "notes": "Guest reset (with-src) after verification. ~400B leaked per undelivered event (~24MB per 4-second cycle). Marker protocol: reader markers are softc-embedded udev_event_kernel entries with ev_dict == NULL (skipped by the reaper predicate), so the leak set is exactly the events no reader externalized. The fix.diff release-ownership change also neutralizes DF-0055's multi-reader UAF (single-sited release in the reaper) and drains the full queue when udev_initiated_count hits 0.", "recommended_fix": "Move the single prop_object_release(ev->ev.ev_dict) from udev_event_externalize() into udev_clean_events_locked() (release-and-NULL before objcache_put), and when udev_initiated_count == 0 drain the entire remaining queue so undelivered dictionaries are released on last-reader close.", "fix_status": "not_testable", "fix_kernel_uname": "", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff authored from line-accurate root cause (release ownership single-sited in the reaper + full drain on last-reader close; also structurally resolves DF-0055). Kernel rebuild validation not performed: leak-class finding; the mandatory fix-build cycle applies to reproduced memory-corruption findings. Guest was reset to the clean-source snapshot after evidence collection.", "fix_evidence": "fix.diff" } |