⬢ DragonFlyBSD Kernel Audit
DF-5033 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5033",
  "status": "inconclusive",
  "verdict": "Kernel-path end-to-end reproduction was impossible on the audit guest (amdgpu.ko present but no AMD GPU; /dev/dri absent), so the in-kernel chain stays unproven at runtime. The defect itself is statically certain: amdgpu_bo_list_get() calls idr_find() under rcu_read_lock() only (amdgpu_bo_list.c:167-176, reached from amdgpu_cs.c:591 without bo_list_lock), while DragonFlys idr_find() takes no lock (sys/libkern/linux_idr.c:516-529) and idr_grow(), invoked synchronously from the CREATE ioctls idr_alloc() (amdgpu_bo_list.c:283 -> linux_idr.c:358 -> :421), swaps idp->idr_nodes (:454) and kfree()s the old array (:458) with no RCU deferral - DF rcu_read_lock() cannot protect it. On the guest, against a byte-identical copy of linux_idr.c, a deterministic oracle reproduced both bad interleavings: (V1) preemption between the nodes-pointer load and the nodes[id] dereference returns the freed chunks contents as a data pointer - the exact value the kernel hands to kref_get_unless_zero() (amdgpu_bo_list.c:170), an atomic RMW on freed/garbage memory; (V2) compiler-permitted load reorder (new idr_count, old nodes) yields an OOB read up to 2320 bytes past the freed 255-entry (4080-byte) chunk. Serializing idr_find (fix) makes both variants clean. Ceiling: kernel panic from a garbage kref_get, or with grooming a refcount corruption -> premature/double free of an amdgpu_bo_list; requires amdgpu render-node access, 255+ live BO lists to force the first growth, and winning a ~2-cycle window (or the reorder) - rated Medium, AV:L/AC:H.",
  "reproduced": false,
  "impact": "none",
  "confidence": "certain",
  "exploit_chain": "unprivileged render-node user -> fd A: create 255+ BO lists (AMDGPU_BO_LIST_OP_CREATE, amdgpu_bo_list.c:276-291) -> idr grows per-fd bo_list_handles past 255 -> idr_grow() kfree(old nodes array); concurrently fd A thread B: AMDGPU_CS ioctl with a bo-list handle (amdgpu_cs.c:591 -> amdgpu_bo_list_get) -> idr_find() reads the freed/reused array (V1 preemption-in-window, or V2 count/nodes load reorder) -> stale/garbage amdgpu_bo_list* returned -> kref_get_unless_zero() atomically increments freed or foreign memory -> balanced amdgpu_bo_list_put() later drops an uncounted reference -> premature free of bo list + entries array (holds ttm_bo refs) -> UAF; realistic near-term outcome: panic. Not executed in-kernel (no GPU); demonstrated at algorithm level on the guest (oracle).",
  "evidence": "oracle_baseline.log: V1 and V2 lines show allocated=dddddddd data=dddddddddddddddd -> USE-AFTER-FREE READ / OOB READ past freed 255-entry chunk (exit 1); oracle_fixed.log: same interleavings clean, exit 0; race_baseline.log / race_fixed.log: 400 grow phases x 4 idr_grow kfree events, hits=0 both (natural catch needs preemption in the ~2-cycle window; no false positives); idr_test_core.c is byte-identical to sys/libkern/linux_idr.c below the USERLAND_TEST header (diff-verified at packaging); env.txt: guest uname + cc 8.3 + hw.ncpu=6",
  "kernel_refs": [
    "sys/dev/drm/amd/amdgpu/amdgpu_bo_list.c:167",
    "sys/dev/drm/amd/amdgpu/amdgpu_bo_list.c:170",
    "sys/dev/drm/amd/amdgpu/amdgpu_bo_list.c:283",
    "sys/dev/drm/amd/amdgpu/amdgpu_cs.c:591",
    "sys/libkern/linux_idr.c:516",
    "sys/libkern/linux_idr.c:358",
    "sys/libkern/linux_idr.c:421",
    "sys/libkern/linux_idr.c:454",
    "sys/libkern/linux_idr.c:458",
    "sys/dev/drm/linux_rcu.c:187"
  ],
  "poc_changes": "No PoC existed (pass-2 finding, authored fresh). Built: gen_core.py generator (replaces only the USERLAND_TEST header block of sys/libkern/linux_idr.c; algorithm byte-identical), poison-on-free instrumented allocators, 4-thread concurrent harness mirroring amdgpu_bo_list_get vs CREATE-ioctl growth, and a deterministic interleaving oracle for the two bad interleavings; IDR_FIXED_BUILD variant validates serializing idr_find.",
  "attempts": 4,
  "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": 560,
  "guest_dirty": 0,
  "build_cmd": "cc -O2 -g -Wall -pthread -I inc -o oracle oracle.c && cc -O2 -g -Wall -pthread -DIDR_FIXED_BUILD -I inc -o oracle_fixed oracle.c && cc -O2 -g -Wall -pthread -I inc -o idr_race idr_race.c && cc -O2 -g -Wall -pthread -DIDR_FIXED_BUILD -I inc -o idr_race_fixed idr_race.c",
  "run_cmd": "sh run.sh  (oracle; oracle_fixed; idr_race; idr_race_fixed)",
  "code_hash": "9abb0d626259f7bb704d2814efca839d3eabab0d024ee94618bf6da9124b6fb2",
  "notes": "The object half of the Linux pattern is safe on DF (amdgpu_bo_list_free -> call_rcu -> kvfree deferred >=1s, sys/dev/drm/linux_rcu.c); only the idr-array half is broken. idr_destroy-vs-find is NOT kernel-raceable (drm core holds the drm_file during in-flight ioctls), so the harness parks readers across destroy/init; all 1600 concurrent frees counted were idr_grow events. Known findings DF-2053/DF-2054 (create error paths) were not re-reported. Guest /tmp scratch cleaned; guest left up, no panic.",
  "recommended_fix": "Take fpriv->bo_list_lock around idr_find()+kref_get_unless_zero in amdgpu_bo_list_get() (see fix.diff); systemic alternative: make DragonFly idr_find() acquire the idr lwkt_token (validated by oracle_fixed).",
  "fix_status": "not_testable",
  "fix_baseline_reproduced": false,
  "fix_patched_reproduced": false,
  "fix_verdict": "Kernel-build fix validation impossible on this guest (no AMD GPU; amdgpu never binds, /dev/dri absent). Algorithm-level validation: baseline oracle reproduces both UAF/OOB interleavings (oracle_baseline.log, exit 1); with idr_find() serialized (IDR_FIXED_BUILD) the identical interleavings are clean (oracle_fixed.log, exit 0), and the serialized concurrent harness completes the same 1600-free workload with zero anomalies (race_fixed.log). fix.diff applies the equivalent serialization at the call site.",
  "fix_evidence": "oracle_baseline.log vs oracle_fixed.log (exit 1 vs exit 0); race_baseline.log vs race_fixed.log (hits=0/0 with same workload, fix regression-free); fix.diff"
}