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)
PoC verification
Evidence pack
findings/poc/DF-2226 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
DF-2226 - Verification Verdict
Status: reproduced Impact: none (hardening) Confidence: certain
Mechanism
_prop_dictionary_internalize accepts
Verdict
REPRODUCED on the stock guest: prop_dictionary_internalize of
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
sys/libprop/prop_dictionary.c:1339sys/libprop/prop_dictionary.c:248sys/libprop/prop_dictionary.c:252
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_testablefix.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)
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
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
No comments yet.