DF-5146 / verdict.json
{ "finding_id": "DF-5146", "status": "reproduced", "reproduced": true, "impact": "dos", "confidence": "certain", "verdict": "REPRODUCED in-kernel on DragonFly 6.5-DEVELOPMENT x86_64: the linux-compat hash_64() (sys/dev/drm/include/linux/hash.h:32-39) misuses hash32_buf()s seed argument and returns identically 0 for every bit-width <= 32, so on 64-bit platforms every drm_ht hash table collapses to a single bucket (verified: 1/4096 buckets non-empty, chain 8192, per-insert time growing linearly => O(n^2) fills) while correctness (find/dup/remove) is preserved. Impact is a reachable local CPU/lock-contention DoS for any user with /dev/dri/card0 (GEM mmap-offset inserts via MODE_CREATE_DUMB/MAP_DUMB + O(n) lookups under the global DRM_LOCK), not memory corruption: the bucket index is constant 0 and always in-bounds, so there is no uid=0 route. Fixed-header rebuild of the same real drm_hashtab.c spreads items over all 4096 buckets with flat insert times (~450-600x faster at the tail), validating fix.diff.", "exploit_chain": "unprivileged user opens /dev/dri/card0 -> DRM_IOCTL_MODE_CREATE_DUMB + DRM_IOCTL_MODE_MAP_DUMB repeatedly (drm_gem.c:381-406, drm_gem.c:576-577): each objects sequential alloc_unr key appends to the single bucket-0 chain => O(n) walk per insert, O(n^2) cumulative kernel CPU; every subsequent GEM mmap (drm_gem.c:1074 under global DRM_LOCK at drm_gem.c:1089) and legacy map mmap (drm_vm.c:89-91) burns O(n) under the device-global lock, stalling all GPU clients. Bounded by per-object memory cost; no privilege boundary crossed.", "evidence": "run.log (decisive lines: nonempty_buckets=1/4096 max_chain=8192 baseline vs nonempty_buckets=4096/4096 max_chain=2 fixed; per-1024-insert wall time 581 1978 3689 4919 6354 8366 9381 11403 baseline vs flat 19 18 18 17 18 17 17 18 fixed; RESULT: hash_long(key,bits)==0 for ALL 6400000 inputs userspace proof); run.2.log (same numbers, stability run); build.log; htcollapse.c compiles the REAL unmodified /usr/src/sys/dev/drm/drm_hashtab.c; VERDICT.md for the full narrative", "kernel_refs": [ "sys/dev/drm/include/linux/hash.h:32-39", "sys/sys/hash.h:44-53", "sys/dev/drm/drm_hashtab.c:80", "sys/dev/drm/drm_hashtab.c:98", "sys/dev/drm/drm_hashtab.c:117", "sys/dev/drm/drm_hashtab.c:148", "sys/dev/drm/drm_hashtab.c:150-155", "sys/dev/drm/drm_drv.c:603", "sys/dev/drm/drm_gem.c:148", "sys/dev/drm/drm_gem.c:381-406", "sys/dev/drm/drm_gem.c:576-577", "sys/dev/drm/drm_gem.c:1074", "sys/dev/drm/drm_gem.c:1089-1090", "sys/dev/drm/drm_vm.c:89-91", "sys/dev/drm/drm_bufs.c:125-127", "sys/dev/drm/amd/lib/chash.c:398" ], "poc_changes": "No seed PoC existed (finding produced by this pass-2 audit); pack written from scratch. Iterations needed: (1) module_t -> moduledata_t for DFs DECLARE_MODULE; (2) initialize t0 for -Werror=maybe-uninitialized; (3) generate bus_if.h/device_if.h/pci_if.h via kmod.mk SRCS instead of empty stubs (drmP.h pci prototypes need them); (4-5) provide local drm_err/drm_dbg shims so drm_hashtab.o links without drm.ko; (6) prepend -Ifixinc BEFORE the stock drm include dir so the fixed header actually wins the include race (first fixed run silently used the stock header).", "attempts": 6, "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": 1100, "guest_dirty": 1, "build_cmd": "cd /root/df5146 && sh build.sh (kld: env SYSDIR=/usr/src/sys make [FIX=1]; userspace: cc -O2 -o hash64_collapse hash64_collapse.c)", "run_cmd": "cd /root/df5146 && sh run.sh (kldload htcollapse baseline + fixed, dmesg capture)", "code_hash": "6150809ea09f37fac8e399f91deff0829788f00356d394cdf63a8f5aed371fa6", "notes": "Not a memory-safety bug (index constant 0, in-bounds; proven errors=0 incl. double-remove/reinsert) - no escalation chain, hence no kernel rebuild required by contract; fix validated by rebuilding the same real drm_hashtab.c with the fixed header. Guest has no GPU (no /dev/dri), so the userspace-reachable DoS ceiling is argued from drm_gem.c call paths + measured in-kernel O(n^2) behavior, not driven through card0. i386 unaffected (hash_32 path is structurally correct). Blast radius beyond drm_hashtab.c: amd/lib/chash.c:398,433 hash_64 collapses identically (same fix cures it). i915 execbuffer LUT uses hash_32 and is unaffected. Cross-file note for the drm_gem.c audit: drm_gem.c:397 insert path lacks visible serialization vs the drm_hashtab.h:64-72 contract.", "recommended_fix": "Rewrite hash_64() in sys/dev/drm/include/linux/hash.h with the canonical 64-bit Fibonacci-hashing mixer (and guard bits==0/>64) so it returns a genuinely bits-wide value; hash_32 can stay as-is.", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "fix.diff validated WITHOUT a full kernel rebuild, by compiling the same unmodified /usr/src/sys/dev/drm/drm_hashtab.c into htcollapse.ko with only the header changed (fixinc/linux/hash.h, logic identical to fix.diff): baseline build reproduces the collapse (1/4096 buckets, max_chain 8192, linearly growing insert times 581->11403 us per 1024); patched build eliminates it (4096/4096 buckets, max_chain 2, flat 17-19 us chunks) with identical functional correctness (find/dup/remove all OK, errors=0). fix_baseline_reproduced=true (bug present on baseline); fix_patched_reproduced=false (bug ABSENT on patched - desired outcome).", "fix_evidence": "run.log sections === kernel baseline === vs === kernel fixed ===; run.2.log same; fix.diff; fixinc/linux/hash.h" } |