prop_dictionary _nocopy twins export borrowed-pointer lifetime hazards into _KERNEL with 26 live dm/tbridge callers β dm_dev_status_ioctl:409 and dm_table_deps_ioctl:619-620 actively dangle the borrowed name/uuid pointers mid-function (latent kernel UAF-read; set twin: frozen-size UAF/leak/overflow, zero callers)
Summary
The dictionary _nocopy twins carry the DF-5690 class into the kernel - but unlike the array twin, the get variant has 25 LIVE call sites in dm_ioctl.c plus tbridge: two dm ioctls borrow name/uuid then REPLACE the same keys while the borrows are in scope (dm_dev_status:397->409 uuid; dm_table_deps:596-597->619-620 both) - the borrows dangle until function exit (line-traced: no post-replace deref today, so latent; any debug kprintf or reorder turns it into a kernel UAF-read). Set twin: frozen ps_size + current-NUL re-read = plist-outlives-buffer UAF read into copyout / plist-outgrows-buffer heap overflow; zero kernel callers. Page-guard rig 3/3: the exact dm patterns trap on replace-then-read, buffer-reuse leaks PWNED-BY-BUFFER-REUSE into externalize, frozen-size strcpy overruns; DML control (the exact in-tree sequence, no post-replace deref) stays clean. kernel-build-copies fix validated; dm caller fixes listed. A 2517-check matrix proves the rest of the file exact (guards byte-identical to the array twins; DF-2240 class ABSENT here - set_uint uses the unsigned creator).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5691 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 5.2 KB | β raw | |
| df5691.c | β | 30.5 KB | view raw | |
| guardalloc.h | β | 2.1 KB | view raw | |
| build.sh | β | 78 B | view raw | |
| run.sh | β | 102 B | view raw | |
| build.log | β | 1.4 KB | view raw | |
| run.log | β | 3.9 KB | view raw | |
| run2.log | β | 3.9 KB | view raw | |
| run3.log | β | 3.9 KB | view raw | |
| env.txt | β | 1.1 KB | view raw | |
| fix.diff | β | 1.8 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 7.2 KB | view raw |
DF-5691 β prop_dictionary_util.c _nocopy twins + typed-helper matrix
File under audit: sys/libprop/prop_dictionary_util.c (pass 2)
Sibling findings: DF-5690 (array _nocopy class, zero live kernel
callers), DF-2240 (array add_uint signed-creator), DF-5674
(prop_dictionary.c keysym).
What this pack proves
- matrix (
./df5691_plain matrix) β every typed helper in the file (get/set int8/16/32/64, uint8/16/32/64, bool, cstring, get_dict, set_and_rel, immutable-dict rows, NULL-dict rows, wrong-type sweep) differentially checked against the derived guard contract. Expected: 0 failures; the DF-2240 class is absent here (set_uintN uses the unsigned creator at prop_dictionary_util.c:167) and the matrix documents the known-good differential rows. - nolife BASELINE (
./df5691_guard nolife) β the_nocopytwins' lifetime hazards on the page-guard allocator (fork per case, SIGSEGV = deterministic trap): - T1:get_cstring_nocopyborrow β same-key replace (the LIVEdm_dev_status_ioctlpattern, dm_ioctl.c:397β:409) β deref traps. - T1b:dm_table_deps_ioctlpattern (borrow name+uuid β replace BOTH keys, dm_ioctl.c:619-620) β deref traps. - T2: dict release while borrowed β trap. - T2b:set_cstring_nocopyβ caller frees buffer β externalize reads freed page β trap. - T3: frozenps_sizevs current NUL βprop_string_cstringstrcpy overruns its ps_size+1 allocation β trap. - T4/DML/NCD controls: copy variant fine; the EXACT in-tree dm sequence without post-replace deref does NOT trap (the in-tree dangle is real but latent β documented). - nolife FIXED (
./df5691_fixed nolife-fixed) β same battery with fix.diff active (rig-only-DPROP_RIG_FORCE_NOCOPY_COPY): T2b/T3 no longer trap; T1/T1b/T2 still trap (documented caller-side borrow contract, same treatment as DF-5690 T1).
Build / run (guest, root, /tmp/df5691)
sh build.sh # 3 same-source libprop rigs: plain / guard / fixed sh run.sh # matrix + nolife baseline + nolife fixed
Expected output
MATRIX: <n> checks, 0 failures, 1 known(DF-2240-diff) rows- baseline: T1/T1b/T2/T2b/T3 β
TRAP(SIGSEGV) OK; T4/DML/NCD βexit=0 OK;NOLIFE(base): all expectations met - fixed: T2b/T3 β
exit=0 OK(frozen copy);NOLIFE(fixed): all expectations met
DF-5691 verdict β prop_dictionary_util.c _nocopy twins (pass 2)
File: sys/libprop/prop_dictionary_util.c Β· Mode: audit+verify (pass 2 GLM 5.3)
Rig: same-source userland libprop builds from /usr/src/sys/libprop
(md5 f17254fc6c6b827538f7c629861ee059 == audit tree) Γ {plain,
page-guard, fixed}. Guest: DragonFly 6.5-DEVELOPMENT, cc 8.3.
Bottom line
REPRODUCED (rig primitives; kernel impact today: none beyond a live,
latent dangle). The dictionary _nocopy twins exported by this file
(prop_dictionary_get_cstring_nocopy / prop_dictionary_set_cstring_nocopy,
template at prop_dictionary_util.c:183-218, expanded :217-218) carry the
DF-5690 borrowed-pointer class into the kernel with β unlike the array
twin β 26 live kernel call sites (dm_ioctl.c Γ25, tbridge.c Γ1). Two
of the dm sites actively invalidate the borrowed pointer mid-function:
dm_dev_status_ioctlborrowsuuidat dm_ioctl.c:397 and replaces the UUID key at :409 (prop_dictionary_setreleases the old object, prop_dictionary.c:988-993) βuuiddangles from :409 to function exit.dm_table_deps_ioctlborrowsname+uuidat :596-597 and replaces BOTH keys at :619-620 β both borrows dangle.
Line-by-line tracing (this run) confirms neither site dereferences the
borrow after the replace β the in-tree dangle is real but latent, one
added kprintf("%s", name) / refactor away from a kernel UAF-read. The
set-side twin has zero kernel callers today (rg over sys/).
What the rig proved (deterministic, 3/3 identical runs)
Baseline (guard-page allocator; fork per case; SIGSEGV = trap):
| case | result |
|---|---|
| T1 dict-replace dangle (dm_dev_status :397β:409 pattern) | TRAP(SIGSEGV) |
| T1b both-keys replace (dm_table_deps :619-620 pattern) | TRAP(SIGSEGV) |
| T2 dict release while borrowed | TRAP(SIGSEGV) |
T2b set_cstring_nocopy β caller frees β proplib-handed ptr read |
TRAP(SIGSEGV) |
| T2c caller reuses buffer β externalize (copyout analog) | leaks <string>PWNED-BY-BUFFER-REUSE</string> (ps_size ignored, re-read to current NUL) |
T3 frozen ps_size=15 vs current NUL@8190 β get_cstring strcpy |
TRAP(SIGSEGV) (heap overflow of the ps_size+1 allocation) |
| T4 copy-variant control | exit 0 |
| DML exact in-tree dm sequence without post-replace deref | exit 0, dict consistent β the latent-today proof |
| NCD alive-read sanity | exit 0 |
Fixed (fix.diff = kernel-build prop_string_create_cstring_nocopy
copies; rig-activated via -DPROP_RIG_FORCE_NOCOPY_COPY): T2b reads
intact "transient-buffer" (no trap), T2c externalizes the frozen copy
(exit 3 semantics), T3 copies exactly 15 bytes β while T1/T1b/T2 still
trap, which is the documented caller-side borrow contract (the get
side is fixed by caller discipline: retain the parent or use the copy
variant β see the dm hardening note in fix.diff's comment).
Simultaneously machine-confirmed clean (negative results)
- (b) get-side numeric guards: 2517-check differential matrix, 0
failures. The intN/uintN guard algebra is byte-identical to the array
twins (prop_dictionary_util.c:101-110/:128-137 vs prop_array_util.c:
89-98/:116-122) and exactly matches the derived range contract for
every width, signedness, and cross-width case;
prop_number_sizeminimal-width semantics (prop_number.c:392-418) included. - (c) DF-2240 signed-creator class is ABSENT here:
prop_dictionary_set_uint##sizeusesprop_number_create_unsigned_integer(prop_dictionary_util.c:167); set_uint64(UINT64_MAX)βget_uint64 roundtrips exactly (known-good differential row). The array bug wasadd_uint's signed creator (prop_array_util.c:187); no such mismatch exists in this file. - (d) misc:
prop_dictionary_set_and_rel(:222-232) release dance is balanced βprop_dictionary_setnever retains on a false return (prop_dictionary.c:975-1046: every failuregoto outprecedes the retain);get_dict(:44-54),get_bool(:56-70), the copy-variantget_cstringand all type guards are NULL-safe (prop_object_type(NULL)=UNKNOWN, prop_object.c:1114-1121) and wrong-type-safe (matrix). No other hazards found in the file.
Why not kernel-impact today
dm is optional dm (sys/conf/files:48) β module/optional dm only,
/dev/mapper/control is root-only in default devfs; tbridge is a
LINT64 regression device. The two dangling sites hold the borrow
un-dereferenced. Filed Low / hardening (severity per DF-5682/DF-5690
precedent, upgraded one notch from Info because the dangle is created by
live in-tree code, not merely one-caller-away).
Artifacts
run.log (decisive), run2.log/run3.log (stability, 18/18 OK each,
byte-identical results), build.log (3 rigs + md5 provenance),
df5691.c (harness), fix.diff (validated), guardalloc.h,
env.txt, manifest.json, verdict.json.
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
fixedFix validated on the same source in-rig (DF-5690/DF-5682 precedent; pure C-level control-flow change on a path the rig compiles identically, zero live kernel callers of the set twin, so no kernel rebuild performed): baseline traps T2b/T3 and leaks T2c; patched copy (exact fix.diff, activated by the rig-only -DPROP_RIG_FORCE_NOCOPY_COPY which is undefined in real builds) reads intact 'transient-buffer' with no trap, externalizes the frozen
run.log section 2 vs section 3: 'T2b ... TRAP(SIGSEGV)' -> 'T2b: NO TRAP, intact content "transient-buffer"'; T2c '<string>PWNED-BY-BUFFER-REUSE</string>' -> '<string>transient-buffer</string>' (exit=0 -> exit=3); 'T3 ... TRAP(SIGSEGV)' -> 'T3: NO TRAP copy=BBBBBBBBBBBBBBB... (len=15)'; 'NOLIFE(base): all expectations met' and 'NOLIFE(fixed): all expectations met'; fix.diff adds the _KERNEL copying branch at prop_string.c:188 (git-apply-able, hunk headers regenerated from a real diff).
Confirmed kernel references
- sys/libprop/prop_dictionary_util.c:183-218
- sys/libprop/prop_dictionary_util.c:167
- sys/libprop/prop_dictionary.c:988-993
- sys/libprop/prop_string.c:85-87
- sys/libprop/prop_string.c:188-200
- sys/libprop/prop_string.c:101-105
- sys/libprop/prop_string.c:302-304
- sys/dev/disk/dm/dm_ioctl.c:396-409
- sys/dev/disk/dm/dm_ioctl.c:596-620
- sys/dev/misc/tbridge/tbridge.c:125
- sys/conf/files:48
Detail
Evidence (decisive lines)
run.log section 2 (baseline): 'T1 ... TRAP(SIGSEGV) OK', 'T1b ... TRAP(SIGSEGV) OK', 'T2 ... TRAP(SIGSEGV) OK', 'T2b ... TRAP(SIGSEGV) OK', T2c leaked '<string>PWNED-BY-BUFFER-REUSE</string>' with 'exit=0 OK', 'T3 ... TRAP(SIGSEGV) OK', 'DML ... NOT dereferenced (latent)'; section 1: 'MATRIX: 2517 checks, 0 failures, 1 known(DF-2240-diff) rows'; section 3 (fixed): 'NOLIFE(fixed): all expectations met' with T2b intact "transient-buffer", T2c frozen copy exit=3, T3 len=15. run2.log/run3.log: 3/3 byte-identical stable reruns (18 OK rows each). build.log: md5-identical guest/host sources, redirected allocator macros, fix.diff applied.
PoC changes
Authored fresh (no seed). Harness revisions across 3 attempts: (1) fixture needed the full plist XML preamble (bare
Verified recommended fix
Make prop_string_create_cstring_nocopy copy in _KERNEL builds (see fix.diff); dm callers of get_cstring_nocopy must retain the parent (or use the copy variant) whenever the key can be replaced β dm_dev_status_ioctl:409 and dm_table_deps_ioctl:619-620 replace borrowed-from keys while the borrows are in scope.
Verdict
prop_dictionary_util.c's _nocopy twins (template at :183-218) export the DF-5690 borrowed-pointer class into _KERNEL with, unlike the array twin, 26 live kernel call sites: prop_dictionary_get_cstring_nocopy returns an unpinned pointer into the prop_string's sole-owned buffer that dangles the moment the same dict key is replaced (prop_dictionary.c:988-993 releases the old object), the dict is released, or the key removed; two in-tree dm sites actively create that dangle mid-function β dm_dev_status_ioctl borrows uuid at dm_ioctl.c:397 then replaces the UUID key at :409, and dm_table_deps_ioctl borrows name+uuid at :596-597 then replaces both keys at :619-620 β while prop_dictionary_set_cstring_nocopy stores caller-owned kernel memory forever (free skipped at prop_string.c:85-87) with ps_size frozen at creation although externalize/get_cstring re-read the buffer to its CURRENT NUL (prop_string.c:101-105, :302-304). Deterministically proven on the same-source page-guard rig: dict-replace dangle TRAPS (T1/T1b, the two exact dm patterns), dict-release dangle TRAPS (T2), freed-buffer read via the proplist pointer TRAPS (T2b), buffer-reuse externalize leaks
No comments yet.