⬢ DragonFlyBSD Kernel Audit
DF-2209 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2209",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "dos",
  "confidence": "certain",
  "verdict": "REPRODUCED: the full UAF chain executed deterministically through the UNMODIFIED production drm_sysfs_connector_add/_remove (tree lines 31-46/48-54, sha256-pinned, extracted on the guest from a tree byte-identical to the audited tree) compiled kernel-resident with production-faithful allocator shims. Chain: (1) add allocates kdev=0xfffff80117608180 with kobj.name card0-DP-3; (2) remove kfrees it and leaves connector->kdev DANGLING (drm_sysfs.c:52-53 NULLs nothing; grep over the whole drm tree shows no other writer ever resets kdev); (3) a fresh same-geometry allocation (56 bytes + M_CACHEALIGN, the production size class) immediately reclaimed the freed slot, which the rig poisoned and forged; (4) the exact production consumer expression connector->kdev->kobj.name (intel_dp.c:5251) read DF2209-PWNED-BY-RECLAIM through the dangling pointer -- attacker-controlled UAF READ; (5) re-add returned ret==0 with the kzalloc call counter UNCHANGED: the guard at drm_sysfs.c:35-36 turned re-registration into a silent no-op success on the dangling pointer, so registration_state advances while every consumer operates on freed memory; (6) a second remove production-kfree()d the RECLAIMED live object owned by another allocator (CROSS-OBJECT FREE, the free-a-victim pivot) and the owner read through freed memory afterwards. Identical across 4 runs on two guest generations. Honest reachability: the natural trigger needs real GPU hardware -- drm_connector_register error path (drm_connector.c:444-467) unregisters while staying INITIALIZING, and MST callers re-register ignoring the first failure (intel_dp_mst.c:500, radeon_dp_mst.c:304); this QEMU guest has no DRM device (natural path not runnable here, DF-2214 rig precedent). Privesc directive assessed and closed honestly: ceiling is the cross-object free plus an info-leak-shaped read (forged kobj.name printed via DRM_DEBUG_KMS %s needs drm debug), gated on a driver error-path window and an M_DRM 64-class spray surface that both require a real GPU; sysfs_create_link is a no-op stub on DFly removing one consumer; uid=0 chain NOT built -- impact recorded as the directly demonstrable dos floor with the corruption/leak ceiling documented in VERDICT.md. Fix A/B validated: the fixed remove NULLs kdev and the fixed cycle re-allocates fresh (run.log CASE F); patched production drm module rebuilds clean with -Werror.",
  "exploit_chain": "GPU-equipped system with i915/radeon/amdgpu + DP MST: first drm_connector_register fails inside drm_debugfs_connector_add or late_register (drm_connector.c:448-457) -> err_sysfs label calls drm_sysfs_connector_remove (drm_connector.c:466-467) freeing kdev and leaving it dangling while registration_state stays INITIALIZING -> MST topology change re-invokes drm_connector_register (return ignored at intel_dp_mst.c:500 / radeon_dp_mst.c:304) -> drm_sysfs_connector_add hits the guard at drm_sysfs.c:35-36 and returns 0 without allocating -> consumers deref the freed 64-byte-class M_DRM block: intel_dp.c:5251 reads forged kobj.name (attacker-chosen kernel address printed as %s -> info leak shape), :5253 stores the dangling device into aux.dev, a later unregister kfrees whatever now owns the slot (cross-object free -> victim UAF). Demonstrated end-to-end through the verbatim functions with the reclaim forged and the consumer expression returning attacker-controlled data; natural-window and spray stages are hardware-gated and were not driven on this guest, so no uid=0.",
  "evidence": "run.log CASE D: [2] connector->kdev still == freed pointer (DANGLING -- drm_sysfs.c:52-53 never NULLs it); [3] fresh kmalloc returned the freed address (IMMEDIATE SAME-ZONE REUSE) poisoned 0x41 with kobj.name forged; [4] consumer read -> DF2209-PWNED-BY-RECLAIM (UAF READ returns ATTACKER-CONTROLLED data); [5] re-add ret==0 with rig_kzalloc_calls UNCHANGED (silent no-op on the dangling pointer); [6] second remove kfree()d the RECLAIMED live object (CROSS-OBJECT FREE) and the owner read freed memory. CASE F (fixed): kdev NULLed after remove, fresh allocation on re-add, cycle clean. run.2.log identical (fresh-guest repeat). panic-side and matrix transcripts in build.log; patched real-module build in fix_module_build.log.",
  "kernel_refs": [
    "sys/dev/drm/drm_sysfs.c:35-36",
    "sys/dev/drm/drm_sysfs.c:52-53",
    "sys/dev/drm/drm_connector.c:444-467",
    "sys/dev/drm/i915/intel_dp.c:5251",
    "sys/dev/drm/i915/intel_dp.c:5253",
    "sys/dev/drm/i915/intel_sdvo.c:2198",
    "sys/dev/drm/i915/intel_dp_mst.c:500",
    "sys/dev/drm/radeon/radeon_dp_mst.c:304",
    "sys/dev/drm/radeon/atombios_encoders.c:262",
    "sys/dev/drm/amd/amdgpu/atombios_encoders.c:245",
    "sys/dev/drm/include/linux/sysfs.h:50-56",
    "sys/dev/drm/include/linux/slab.h:35"
  ],
  "poc_changes": "No seed pack existed; rig authored from scratch in verify mode following the DF-2214 verbatim-KLD pattern. Iterations: (1) <machine/stdarg.h> does not define va_list -- switched to <sys/stdarg.h>; (2) first matrix run showed no reclaim aliasing: the 64-byte planted-string helper allocation itself stole the just-freed 64-class slot -- moved the planted string to the 128-byte class, after which the first candidate kmalloc returned the freed address deterministically (case F had already proven LIFO reuse of a just-freed same-class chunk); (3) owner-after-free read prints the pointer VALUE only (never %s an unknown forged pointer) so the matrix cannot fault outside the deliberate fire case.",
  "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": 40,
  "guest_dirty": 0,
  "build_cmd": "cd findings/poc/DF-2209 && ./build.sh  # provenance-checked extraction + cc 8.3 -D_KERNEL -nostdinc kernel-context KLD build of baseline+fixed variants",
  "run_cmd": "cd findings/poc/DF-2209 && ./run.sh  # kldload df22_rig.ko: UAF matrix to dmesg; kldunload",
  "code_hash": "7453d0b212f35d0757a1ad884165fca65a2f558ec17c9b04ec21b916b54b7a97",
  "notes": "Deterministic across 4 runs on two guest generations (pre-reset and post-reset). Rig also surfaces the DF-2208 hardening pair and the kobj.name string leak per add/remove cycle (folded into fix.diff). Guest left up and clean after reset with-src: pristine tree hash verified, kldstat kernel+ehci+xhci only, /root/df22 removed.",
  "recommended_fix": "In drm_sysfs_connector_remove free the kasprintfd name, free kdev, then set connector->kdev = NULL -- see fix.diff hunk 2 (paired with the DF-2208 NULL checks in hunk 1).",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_validated": 1,
  "fix_diff": "findings/poc/DF-2209/fix.diff",
  "fix_verdict": "A/B validated in the same verbatim KLD: the FIXED remove NULLs connector->kdev (breaking the dangling pointer) and frees the name string; the fixed cycle re-add allocates a FRESH block (kzalloc counter increments, pointer differs from the freed and the interposed block, name re-established card0-DP-5) -- no dangling, no silent no-op, no cross-object free (run.log CASE F). Additionally fix.diff applied to the guest /usr/src tree rebuilt the production drm KLD clean with -Werror (fix_module_build.log).",
  "fix_evidence": "run.log CASE F lines: [1] fixed remove: kdev=0 (NULLed: fix breaks the dangling pointer); [2] fixed re-add: FRESH allocation 0xfffff80117608240 != freed 0xfffff80117608180 ... kzalloc_calls 6 -> 7 ... VERDICT: fixed cycle clean. fix_module_build.log: cc -Werror -c drm_sysfs.c + ld -r -o drm.ko, rc=0."
}