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)
PoC verification
Evidence pack
findings/poc/DF-2220 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- guest tree
linux_kobject.csha25650b635ee...== audited tree. kobject_init_and_add/kobject_releasecompiled into the kernel verbatim via#include <dev/drm/linux_kobject.c>; thekvasprintfthey call is the verbatimdrm_dragonfly.c:67-84body (kvasprintf_verbatim.csha25612efc1be...) β the symbol lives in the drm module, which is not (and cannot be) loaded on this guest.kobject_release_fixed= same bytes +fix.diff, def renamed (fixed_kobject.csha256c53ad2be...).- the
kobject_put β kref_put β kobject_releasechain 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, likettm_bo_global_kobj_release), name via the verbatimkvasprintf(linux_kobject.c:39), teardown through the realkobject_putchain.releases=16384proves 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).
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
fixedConfirmed 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.
No comments yet.