β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2220

kobject_release leaks kobj->name string allocated by kobject_init_and_add

Summary

kobject_init_and_add() allocates kobj->name via kvasprintf(M_WAITOK fmt ap) storing separately-mallocd string. kobject_release() only invokes kobj->ktype->release(kobj) never frees kobj->name. Upstream Linux frees it in kobject_cleanup() via kfree_const(kobj->name) after type-specific release. None of in-tree release callbacks (ttm_bo_global_kobj_release ttm_mem_zone_kobj_release ttm_mem_global_kobj_release ttm_pool_kobj_release x2 ttm_drm_class_device_release) free name either they only kfree() container struct which contains kobject by value so separately-allocated name pointer is lost on every release. ~5-7 kobjects per DRM subsystem init each full load/unload cycle leaks ~100-200 bytes M_DRM. Format strings are kernel literals producing ~15-30 byte strings. No UAF: leaked name buffer never re-accessed after embedding kobject freed (pointer simply lost). Required position: root kldload/kldunload cycles or GPU hotplug. Slow unbounded kernel memory growth availability only no confidentiality/integrity impact. [VERIFIED pass2: reproduced - 16384 production-shape cycles through real kobject_put kref_put kobject_release leak exactly 40 bytes per cycle in M_DRM accounting, single-shot probe shows name allocation live after release; save-name-then-free-after fix-validated delta 0]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2220 Β· 15 files
FileTypeDescriptionSize
rig_head.c rig-source shared batch rig head 2.0 KB view raw
rig_mid.c rig-source verbatim production includes + fixed prototypes 1.1 KB view raw
rig_tail.c rig-source meter + test sections incl. production-shape kobject cycles and non-freeing semantics probe 18.1 KB view raw
build.sh build-script provenance chain + fixed-variant extraction + kernel-compile 158 B view raw
run.sh run-script sysctl trigger + dmesg extraction 172 B view raw
fix.diff suggested-fix save kobj->name before release callback, kfree(name, M_DRM) after (upstream kobject_cleanup semantics) 790 B view raw
build.log build-log FULL untrimmed nativekernel DFV22X build 5.5 MB ↓ download
run.log run-log FULL decisive rig output 7.8 KB view raw
run_section.log run-log DF-2220 lines only 892 B view raw
run.first_meterbug.log run-log first iteration meter artifact record 7.5 KB view raw
env.txt environment guest env + hashes 2.1 KB view raw
VERDICT.md verdict full narrative 4.1 KB ↓ raw
README.md readme build/run/expected 3.4 KB ↓ raw
manifest.json manifest this file 2.5 KB view raw
verdict.json verdict machine verdict for persist_poc.py 3.3 KB view raw
README.md readme build/run/expected
↓ download raw

DF-2220 β€” kobject_release() leaks kobj->name allocated by kobject_init_and_add()

Status: REPRODUCED (verbatim-code, kernel-compiled rig) β€” impact class: slow unbounded kernel memory growth (M_DRM) = availability only, root-gated triggers. Fix: A/B-validated in the same harness kernel (fix.diff, name saved before the release callback and kfreed after β€” mirroring upstream kobject_cleanup).

Part of the DF-22X batch (DF-2216 / DF-2220 / DF-2223 / DF-2224) verified in ONE harness kernel; this pack documents the DF-2220 section.

What this pack contains

Same layout as the DF-2216 pack (shared rig sources rig_head.c/rig_mid.c/ rig_tail.c, build.sh, run.sh, env.txt, full logs); finding-specific: fix.diff (against sys/dev/drm/linux_kobject.c), run_section.log (DF-2220 lines), run.first_meterbug.log (meter engineering record).

How to reproduce

Identical to the DF-2216 pack (same batch build + run); see that README or build.sh / run.sh headers.

Provenance

  1. guest tree linux_kobject.c sha256 50b635ee... == audited tree.
  2. kobject_init_and_add / kobject_release compiled into the kernel verbatim via #include <dev/drm/linux_kobject.c>; the kvasprintf they call is the verbatim drm_dragonfly.c:67-84 body (kvasprintf_verbatim.c sha256 12efc1be...) β€” the symbol lives in the drm module, which is not (and cannot be) loaded on this guest.
  3. kobject_release_fixed = same bytes + fix.diff, def renamed (fixed_kobject.c sha256 c53ad2be...).
  4. the kobject_put β†’ kref_put β†’ kobject_release chain is the real header inline chain (linux/kobject.h), not a re-implementation.

Expected output (baseline = unmodified production code)

DF2220 CONTROL  paired kmalloc/kfree x16384: memuse delta=0 (expect 0)
DF2220 BASELINE kobject_init_and_add+kobject_put x16384: memuse delta=655360 kmalloc-calls delta=32768 releases=16384 leaked-bytes-per-cycle=40
DF2220 BASELINE init:+32 after_release:+32 name ptr still valid, content=[leaked_name_probe_0123456789] len=28 (allocation SURVIVED release)
DF2220 FIXED    init:+32 after_release_fixed:+0 (name freed by the patch)
DF2220 FIXED    init+release_fixed x16384: memuse delta=0 kmalloc-calls delta=32768 releases=16384
DF2220 VERDICT: baseline leaked 655360 bytes in 16384 cycles (control 0, fixed 0) -> LEAK CONFIRMED, FIX VALID
  • CONTROL proves the meter returns to zero for correctly paired alloc/free.
  • BASELINE cycles are production-shaped: container kmalloc (freed by the release callback, like ttm_bo_global_kobj_release), name via the verbatim kvasprintf (linux_kobject.c:39), teardown through the real kobject_put chain. releases=16384 proves the release path executed.
  • The single-shot probe shows the name allocation still live and intact AFTER kobject_release() returned β€” the pointer is simply lost with the container.

Attacker input / reachability

In-tree callers: ttm_bo.c:1565 (ttm_bo_global_init), ttm_memory.c:257/300/321 (zones + global), ttm_page_alloc.c:838, ttm_page_alloc_dma.c:1177 β€” every drm subsystem lifecycle creates ~5-7 kobjects and destroys them via kobject_put. Trigger = drm module load/unload cycles (root kldload/kldunload) or GPU attach/detach; ~40 bytes leaked per release. No UAF (buffer never re-accessed), no corruption, no info disclosure β†’ no privesc route; ceiling is unbounded slow kernel memory growth (DoS).

VERDICT.md verdict full narrative
↓ download raw

DF-2220 β€” VERDICT

REPRODUCED β€” verbatim-code, kernel-compiled rig; leak quantified per cycle with the kernel's own malloc accounting; fix A/B-validated in the same kernel. Impact: slow unbounded kernel memory growth (M_DRM) β€” availability only, root-gated triggers. No privesc route (see below).

Rig class and why

Same batch harness kernel as DF-2216 (DFV22X); the production sys/dev/drm/linux_kobject.c is #included verbatim (sha-gated 50b635ee...). kobject_init_and_add/kobject_release in the kernel are the production functions. The kvasprintf() they call lives in the drm module (drm_dragonfly.c:67-84), which cannot be loaded on this guest β€” so the rig carries that body verbatim too (kvasprintf_verbatim.c, sha 12efc1be..., extracted+hashed by build.sh). The teardown chain kobject_put β†’ kref_put β†’ kobject_release is the real header inline chain (linux/kobject.h:85-90), not a re-implementation.

Line-accurate confirmation

linux_kobject.c:39: kobj->name = kvasprintf(M_WAITOK, fmt, ap) β€” a separately-malloc'd string (kmalloc size+1, M_DRM, drm_dragonfly.c:76). linux_kobject.c:45-51: kobject_release() invokes kobj->ktype->release(kobj) and nothing else β€” kobj->name is never freed. Upstream Linux frees it in kobject_cleanup() (kfree_const(kobj->name) after the type release). The in-tree release callbacks (ttm_bo_global_kobj_release, ttm_mem_zone_kobj_release, ttm_mem_global_kobj_release, ttm_pool_kobj_release Γ—2, ttm_drm_class_device_release) all kfree() the container embedding the kobject, so the name pointer dies with the container on every release. No UAF: the buffer is simply lost.

Mechanical reproduction (decisive run, run.log)

phase result
CONTROL: 16384 paired kmalloc(32)/kfree delta = 0
BASELINE: 16384 production-shape cycles (kmalloc container β†’ verbatim kobject_init_and_add β†’ real kobject_put chain β†’ release kfrees container) delta = 655,360 B = 40 B/cycle; releases=16384 proves the release path ran
BASELINE single-shot (non-freeing release) init:+32 after_release:+32, name content [leaked_name_probe_0123456789] read back intact after kobject_release() returned
FIXED single-shot init:+32 after_release_fixed:+0
FIXED loop (patched release) delta = 0

40 = slab-chunk-quantized 33-byte name ("rig_kobj_%06d_aaaa..." β†’ 32 chars + NUL). Both return-shape and accounting prove the leak and its elimination.

Fix validation (A/B, same kernel)

fix.diff saves kobj->name into a local before the release callback (the callbacks free the container β€” saving after would be a UAF; this mirrors upstream kobject_cleanup ordering exactly), then kfree(name, M_DRM) after it, with MALLOC_DECLARE(M_DRM) added (the type is kernel-resident, kern_slaballoc.c:202). Extracted-from-patched-bytes kobject_release_fixed in the same kernel: loop delta 0, releases still 16384, identical container lifetimes. fix_status: fixed.

Reachability (who can actually trigger this)

In-tree creators: ttm_bo.c:1565 (ttm_bo_global_init), ttm_memory.c:257/300/321 (memory zones + global), ttm_page_alloc.c:838, ttm_page_alloc_dma.c:1177 β€” ~5-7 kobjects per drm subsystem lifecycle, each torn down via kobject_put. Trigger requires drm module load/unload cycles (root kldload/kldunload) or GPU attach/detach events: ~40 B per release, ~100-200 B per full lifecycle. Root-gated, slow-growth availability issue β€” matches the filed Low severity (AV:L/AC:H/PR:H).

Privesc assessment (standing directive)

None. Pure allocation leak; the lost buffer is never re-accessed (no UAF window), nothing is copied to userspace, no length or bounds is ever wrong. No primitive β†’ no escalation chain.

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

fixed
↓ fix.diffper-fix-DF-2220

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log lines DF2220 CONTROL/BASELINE/FIXED/VERDICT: control delta=0, baseline delta=655360 (40/cycle, releases=16384 confirming the release path ran), BASELINE init:+32 after_release:+32 with content=[leaked_name_probe_0123456789] (allocation SURVIVED release), FIXED init:+32 after_release_fixed:+0, fixed loop delta=0; run.first_meterbug.log documents the corrected meter artifact

PoC changes

Rebuilt as verbatim-code kernel-compiled rig; carried the verbatim kvasprintf body (module-resident symbol) into the kernel TU; fabricated kobj_type release callbacks in both shapes (production container-freeing and non-freeing probe); deterministic M_DRM accounting meter

Verified recommended fix

Save kobj->name before the type release callback and kfree(name, M_DRM) after it in kobject_release, mirroring upstream kobject_cleanup (see fix.diff)

Verdict

Reproduced with the verbatim production kobject_init_and_add/kobject_release compiled kernel-resident. 16384 production-shape cycles (kmalloc container embedding the kobject, verbatim kobject_init_and_add naming it via the verbatim kvasprintf from drm_dragonfly.c, then the real kobject_put -> kref_put -> kobject_release chain whose release callback kfrees the container, exactly like ttm_bo_global_kobj_release) grow the M_DRM accounting by 655360 bytes = 40 bytes per cycle while the paired kmalloc/kfree control returns to 0 and the fix-2220 patched release (name pointer saved before the release call and kfree()d after, mirroring upstream kobject_cleanup) also returns to 0. Single-shot semantics: after the real kobject_release returns, the name allocation is still live and its content intact (leaked_name_probe_0123456789 read back after release); with the fixed release the meter returns to base. Slow unbounded kernel memory growth, root-gated (kldload/kldunload of drm modules or GPU attach cycles, ~5-7 kobjects per cycle); no UAF (the leaked buffer is simply lost), no corruption primitive, no privesc chain. Fix validated A/B in the same kernel.