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

Internalizer accepts empty <key></key> violating keysym non-empty invariant guarded by _PROP_ASSERT

Summary

_prop_dictionary_internalize_body decodes <key>...</key> content into tmpkey without rejecting zero-length results. Empty key stored via prop_dictionary_set->_prop_dict_keysym_alloc creates keysym with pdk_key[0]==\0. Violates invariant documented at prop_dictionary.c:250 (they are never empty) asserted at line 252. On INVARIANTS kernels _PROP_ASSERT expands to KKASSERT which panics on production no-op so invariant violated silently. No check rejects keylen==0 in internalizer. strcpy(pdk->pdk_key "") writes lone NUL. _PROP_ASSERT(pdk->pdk_key[0]!=\0) at line 252 would fire if keysym externalized as standalone object but _prop_dictionary_externalize externalizes keys via _prop_object_externalize_append_encoded_cstring bypassing _prop_dict_keysym_externalize so common round-trip does not panic. No in-tree kernel caller invokes prop_dictionary_all_keys (grep zero callers) so panic latent. Unprivileged local user can send proplib dict with <key></key> via ioctl (DM_TABLE_LIST udev requests). Defense-in-depth/correctness issue internalizer fails to enforce invariant rest of code assumes. [VERIFIED pass2: runtime reproduced - empty key internalizes and round-trips, assert only on externalize side; reject-at-internalize fix]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2226 Β· 10 files
FileTypeDescriptionSize
VERDICT.md β€” 2.1 KB ↓ raw
README.md β€” 299 B ↓ raw
build.sh β€” 102 B view raw
run.sh β€” 78 B view raw
env.txt β€” 314 B view raw
manifest.json β€” 706 B view raw
verdict.json β€” 2.7 KB view raw
fix.diff β€” 271 B view raw
prop_empty_key.c β€” 1.6 KB view raw
run.log β€” 711 B view raw

DF-2226 evidence pack

  • finding: sys/libprop/prop_dictionary.c (1325-1345 (internalize), 246-249 (externalize assert))
  • verdict: reproduced (certain) - see VERDICT.md
  • runtime PoC: prop_empty_key.c
  • reproduce: build.sh / run.sh ; expected output in run.log
  • fix.diff: present, git-apply-able
VERDICT.md
↓ download raw

DF-2226 - Verification Verdict

Status: reproduced Impact: none (hardening) Confidence: certain

Mechanism

_prop_dictionary_internalize accepts : after decoding the key string there is no keylen>0 rejection (the code comment even says non-empty), so an empty keysym is interned. The invariant is only asserted on the OTHER side, in _prop_dict_keysym_externalize (_PROP_ASSERT(pdk_key[0] != 0), prop_dictionary.c:248) - a kernel INVARIANTS build panics there, the shipped userland lib (NDEBUG) silently emits .

Verdict

REPRODUCED on the stock guest: prop_dictionary_internalize of x returns a live dictionary and prop_dictionary_get(dict, empty) fetches the object (empty keysym interned). Externalize round-trips through the whole plist layer. Kernel-side internalize of untrusted XML has no in-tree caller reachable by an unprivileged user (devd consumes kernel-EXTERNALIZED props, not the reverse), so impact is hardening: any future kernel-side internalize of untrusted dictionaries panics INVARIANTS builds at prop_dictionary.c:248. sys/libprop is shared verbatim with the userland libprop, so the userland library accepts the same degenerate dictionaries.

Threat / reachability

Crafted plist XML fed to any libprop consumer (userland: today; kernel: if ever wired) - invariant violation / INVARIANTS panic; no unprivileged kernel path found.

Privilege escalation assessment

n/a (library invariant hardening; no kernel entry point)

Fix

Reject empty keys at internalize time (goto bad) so the invariant holds at ingestion instead of asserting at externalize.

Kernel references

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

not_testable

fix.diff compile-validated in the batch combined kernel build (all 37 diffs of this batch applied to in-guest /usr/src; make -j6 nativekernel KERNCONF=X86_64_GENERIC, see batch build log referenced in VERDICT.md); no behavior A/B performed for this Info-tier item; behavior A/B intentionally skipped for Info tier

batch combined kernel build log (see findings/poc/_batch41 note in VERDICT.md)
↓ fix.diffper-fix-DF-2226

Confirmed kernel references

Detail

Exploit chain

no escalation path: kernel-side internalize of untrusted XML has no in-tree caller reachable by unprivileged users (userland libprop accepts the degenerate dict today)

Evidence (decisive lines)

run.log: EMPTY key ACCEPTED + prop_dictionary_get(dict, empty) returns the live object; externalize emits <key></key>; VERDICT.md cites the assert at prop_dictionary.c:248

PoC changes

Initial draft used bare XML; DFly libprop internalize requires the wrapper - switched to plist form and the empty key reproduced immediately. Also captured the externalize round-trip ( emitted back) via a second probe.

Verified recommended fix

Reject empty keys at internalize time (goto bad) so the invariant holds at ingestion instead of asserting at externalize.

Verdict

REPRODUCED on the stock guest: prop_dictionary_internalize of x returns a live dictionary and prop_dictionary_get(dict, empty) fetches the object (empty keysym interned). Externalize round-trips through the whole plist layer. Kernel-side internalize of untrusted XML has no in-tree caller reachable by an unprivileged user (devd consumes kernel-EXTERNALIZED props, not the reverse), so impact is hardening: any future kernel-side internalize of untrusted dictionaries panics INVARIANTS builds at prop_dictionary.c:248. sys/libprop is shared verbatim with the userland libprop, so the userland library accepts the same degenerate dictionaries.