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

prop_object_release_emergency dispatches pot_emergency_free on a NULL parent when the first walk iteration breaks (CWE-476; kernel-unreachable today, userland malloc-failure edge)

Summary

If the first loop iteration breaks via ocnt!=1 (a concurrent retain - exactly the contention this path exists for) or FREE_DONE, parent is still NULL at the unconditional emergency_free dispatch; the _PROP_ASSERT is a no-op without INVARIANTS. Reproduced deterministically at unit level (assert abort / NDEBUG NULL-deref); kernel-unreachable today (emergency requires M_WAITOK kmalloc to fail, which panics instead) - live for userland libprop under allocation pressure; a dead-code landmine if the allocation ever flips to NOWAIT. parent==NULL early-return fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5677 Β· 9 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 2.0 KB ↓ raw
df5677_emergency.c β€” 1.6 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 90 B view raw
run.log β€” 1.1 KB view raw
fix.diff β€” 484 B view raw
manifest.json β€” 939 B view raw
verdict.json β€” 3.4 KB view raw

DF-5677 β€” prop_object_release_emergency() NULL-parent dispatch (userland-edge defect, kernel-unreachable today)

Severity: Low Β· Class: NULL-pointer dereference on an error path (CWE-476) File: sys/libprop/prop_object.c:1005-1052

The defect

If the FIRST iteration of the emergency walk breaks via ocnt != 1 (another holder raced a retain β€” exactly the contention the emergency path exists for) or the object frees itself outright (_PROP_OBJECT_FREE_DONE), parent is still NULL when the function runs off the end of the loop:

    _PROP_ASSERT(parent);                          /* no-op w/o INVARIANTS */
    po = parent;
    (*po->po_type->pot_emergency_free)(parent);     /* NULL->pot_emergency_free */

Reachability (why Low)

prop_object_release only calls the emergency path when pot_free returns _PROP_OBJECT_FREE_FAILED, which for array/dict happens only when _prop_stack_push fails β€” i.e. a malloc failure. In-kernel, _PROP_MALLOC is kmalloc(M_WAITOK) which cannot return NULL (DragonFly panics on malloc-limit excess, kern_kmalloc.c:706) β€” so the whole emergency path is dead code in the kernel today. In userland it is reachable under allocation failure plus a concurrent retain racing the restored refcount; the window is narrow but the consequence is an immediate abort/NULL-deref.

What was proven on the guest (deterministic, unit-level)

Calling the static function (via TU-include) on a refcount-2 array β€” the deterministic equivalent of the raced state:

  • assert build: Assertion failed: (parent), function prop_object_release_emergency, file proporig/prop_object.c, line 1048 β†’ abort (134)
  • NDEBUG build: SIGSEGV (139) β€” the raw (*po->po_type->…)(NULL) load

Fix validation

fix.diff: if (parent == NULL) return; before the dispatch β€” if nothing was walked there is no parent to emergency-free. Fixed rig: both builds print "survived (bug not present)", exit 0.

Files

df5677_emergency.c, build.sh, run.sh, fix.diff, run.log

VERDICT.md
↓ download raw

DF-5677 β€” VERDICT

reproduced (deterministic unit-level) / impact=dos (userland-only; kernel path unreachable today) / confidence=certain

Reproduced

TU-include harness reaches the static prop_object_release_emergency with a refcount-2 array β€” the deterministic equivalent of the first-iteration ocnt != 1 break (a concurrent retain racing the restored refcount):

Assertion failed: (parent), function prop_object_release_emergency,
file proporig/prop_object.c, line 1048.   -> abort (134)
NDEBUG build:                              -> SIGSEGV (139)

Why kernel impact is nil today (and honestly scoped)

The emergency path is entered from prop_object_release only when pot_free returns _PROP_OBJECT_FREE_FAILED; for array/dict that requires _prop_stack_push to fail, i.e. a malloc failure. Kernel _PROP_MALLOC = kmalloc(M_WAITOK) β€” cannot return NULL (DragonFly panics on malloc-limit excess instead, sys/kern/kern_kmalloc.c:706). So in the shipped kernel the defect is dead code; it is live for userland libprop under memory pressure and would become kernel-live if the allocation ever flips to M_NOWAIT.

Related observations recorded in the same function (not separately filed): pot_emergency_free(parent) is invoked without the type lock held, and the refcount restore (atomic_inc_32, prop_object.c:1046/1102) runs outside the lock β€” both matter only on the same malloc-failure/race edge, and DF-5674 already tracks the pot_lock contract hole for this family.

Fix validation

if (parent == NULL) return; β€” nothing was walked, so there is nothing to emergency-free. Fixed rig: assert and NDEBUG builds both survive (exit 0).

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
baseline reproduced→ patch + rebuild →patched clean

fix.diff (parent==NULL early return) applied to the same-source rig on the guest: baseline aborts/SIGSEGVs, fixed harness survives in both assert and NDEBUG builds (exit 0). Kernel rebuild not performed β€” the emergency path cannot execute in-kernel (M_WAITOK push cannot fail), so there is no kernel behavior to validate.

run.log (baseline exit=134 assert / 139 NDEBUG; fixed 'survived (bug not present)' exit=0 both); fix.diff
↓ fix.diffper-fix-DF-5677

Confirmed kernel references

Detail

Exploit chain

userland libprop under malloc pressure: prop_object_release on a deep container hits _PROP_OBJECT_FREE_FAILED (stack-push malloc fails) -> emergency walk -> concurrent retain on the target restores refcount>1 -> first iteration breaks with parent==NULL -> NULL deref/abort. Kernel chain blocked by M_WAITOK semantics (no FAILED possible).

Evidence (decisive lines)

run.log: 'Assertion failed: (parent), function prop_object_release_emergency, file proporig/prop_object.c, line 1048' exit=134; NDEBUG control 'Segmentation fault' exit=139; fixed rig 'DF5677: survived (bug not present)' exit=0 both builds.

PoC changes

Harness written fresh: TU-include of prop_object.c to reach the static function; refcount-2 array as the deterministic stand-in for the raced state; NDEBUG control build to show the raw NULL deref behind the assert.

Verified recommended fix

Return early when parent==NULL (nothing was walked, nothing to emergency-free) instead of asserting and dereferencing.

Verdict

prop_object_release_emergency dispatches (*po->po_type->pot_emergency_free)(parent) with parent==NULL whenever the first loop iteration breaks (ocnt!=1 concurrent-retain race or DONE) β€” reproduced deterministically at unit level on the same-source rig: assert(parent) abort at prop_object.c:1048 (exit 134) and raw NULL-deref SIGSEGV under NDEBUG (exit 139). Kernel impact none today: the emergency path requires _prop_stack_push to fail, and kernel _PROP_MALLOC is kmalloc(M_WAITOK) which cannot return NULL (kern_kmalloc.c:706 panics instead) β€” dead code in-kernel, live for userland libprop under allocation failure plus the retain race. Fixed and validated on the rig: parent==NULL early-return makes both builds survive.