MATCH() preamble memcmp in _prop_object_internalize_context_alloc reads past the NUL sentinel (bounded OOB read; userland crash reproduced, kernel read page-contained on current bcmp)
Summary
Only *xml=='<' is established before memcmp of up to 15 bytes at &xml[1]: when '<' is the last byte before the NUL sentinel the range exceeds the object. Userland rig SIGSEGVs on a guard page at the sentinel (gcc emits a widened 4-byte load); the shipped kernel calls bcmp whose aligned-longword implementation reads past the object only within the same page (cannot fault, masked) - no kernel impact on current codegen, latent UB any vectorized bcmp turns into a kernel OOB read. The '<!--' find_tag test compiles to the same widened class. xml[1]-check + strncmp fix validated in the rig.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5676 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 1.9 KB | β raw | |
| df5676_guard.c | β | 2.6 KB | view raw | |
| build.sh | β | 92 B | view raw | |
| run.sh | β | 90 B | view raw | |
| run.log | β | 2.1 KB | view raw | |
| fix.diff | β | 1011 B | view raw | |
| manifest.json | β | 879 B | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-5676 β MATCH() preamble memcmp reads past the NUL sentinel (bounded OOB read)
Severity: Low Β· Class: out-of-bounds read (CWE-125)
File: sys/libprop/prop_object.c:739-746 (_prop_object_internalize_context_alloc), same-class widening at prop_object.c:391-393 (find_tag <!--)
The defect
#define MATCH(str) (memcmp(&xml[1], str, sizeof(str) - 1) == 0)
...
if (MATCH("?xml ") || MATCH("!DOCTYPE plist")) {
Only *xml == '<' and *xml != NUL are established (prop_object.c:736) before
comparing up to 15 bytes at &xml[1]. When the '<' is the last byte before
the NUL sentinel, the memcmp is permitted (and with block/vectorized memcmp,
expected) to read past the end of the buffer.
What was proven on the guest
- Userland, same source: guard-page harness (
df5676_guard.c) places the input so the sentinel is the last byte of a page followed by PROT_NONE.prop_array_internalize(" <")β SIGSEGV (exit 139) in the libc memcmp β the overread is realized. Codegen shows why: the rig object hascmpl $0x6c6d783f,0x1(%rbx)β gcc's widened 4-byte load of "?xml" at xml+1 (only 1 valid byte there). - Kernel: the shipped kernel binary calls
bcmpfor both MATCHes (objdump of_prop_object_internalize_context_allocat 0xffffffff809e1510: twocallq bcmp). DragonFly's kernel bcmp (sys/libkern/bcmp.c) uses aligned longword loads β the alignment loop compares byte-wise until 8-aligned (stops at the sentinel on mismatch), and aligned 8-byte loads never cross a page; the compare result is masked to the significant bytes. Consequence: on the current kernel codegen the overread stays within the same page, cannot fault, and no byte of it influences any observable result β no kernel impact today (latent UB that becomes live if bcmp/memcmp ever gets an unaligned/SSE implementation).
Fix validation
fix.diff (in prop_object.c): reject xml[1] == NUL up front and switch MATCH
to strncmp, which stops at a NUL in either operand by definition; the
<!-- check in find_tag validates each byte before the (widenable) test.
Validated on the guest with the same-source rig:
- baseline rig: probe β SIGSEGV
- fixed rig: all three probes (" <", " <?xml", " <!DOCTYPE") return cleanly, exit 0
Files
df5676_guard.c, build.sh, run.sh, fix.diff, run.log
DF-5676 β VERDICT
reproduced (userland same-source) / impact=dos (userland crash) / kernel: no impact on current codegen / confidence=certain (the OOB read is real)
Reproduced
Guard-page harness against the same-source libprop rig: input " <" with the
NUL sentinel as the last byte of a page followed by PROT_NONE β
prop_array_internalize SIGSEGVs inside libc memcmp during the
_prop_object_internalize_context_alloc preamble skip (run.log). The rig
object disassembly proves the widened read: cmpl $0x6c6d783f,0x1(%rbx) β a
4-byte load at xml+1 where only the sentinel byte is valid.
Kernel side, honestly scoped
The shipped kernel's _prop_object_internalize_context_alloc
(0xffffffff809e1510) calls bcmp twice for the MATCHes. sys/libkern/bcmp.c
walks byte-wise until p1 is longword-aligned (the sentinel mismatches '?' on
the first byte during that phase and returns immediately for the classic
short-input case), and its block loads are aligned 8-byte loads that cannot
cross a page boundary, with the result masked to the significant bytes. So the
kernel realizes the read past the object end (within the slab page) but
cannot fault and cannot disclose. Filed Low for that reason: a real CWE-125
defect with a proven userland crash and a latent kernel UB that any future
bcmp/memcmp "optimization" (SSE, unrolled) turns into a kernel OOB read.
Fix validation
fix.diff: if (_PROP_EOF(xml[1])) goto bad; + strncmp MATCH + explicit
per-byte _PROP_EOF validation in the find_tag <!-- test. On the guest:
baseline rig crashes (139), fixed rig runs all probes clean (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
fixedfix.diff (EOF pre-check + strncmp MATCH + per-byte <!-- validation) applied to the same-source rig on the guest: baseline SIGSEGVs on probe 1, fixed lib runs all three probes cleanly (exit 0). Kernel rebuild not performed β kernel-side realization is page-contained/masked (no observable kernel behavior to validate); the identical source change compiles in both builds (rig builds with the same headers).
run.log tail: '== baseline (expect SIGSEGV) == base exit=139' and '== fixed (expect no fault) == ... no fault observed, fix exit=0'; fix.diff
Confirmed kernel references
Detail
Exploit chain
userland program calls prop_array_internalize/prop_dictionary_internalize on a heap buffer whose '<' lands within 15 bytes of the buffer end (e.g. plist string built in place): vectorized memcmp reads past the allocation -> SIGSEGV or silent heap overread; kernel ioctl paths (DM/vquotactl/tbridge) reach the same code but the kernel bcmp realization is page-contained and masked -> no kernel fault/leak on current codegen.
Evidence (decisive lines)
run.log (guard-page SIGSEGV exit 139 on baseline rig; fixed rig all probes clean exit 0; rig objdump showing cmpl $0x6c6d783f,0x1(%rbx)); kernel objdump of _prop_object_internalize_context_alloc at 0xffffffff809e1510 showing the two callq bcmp; sys/libkern/bcmp.c aligned-longword analysis in VERDICT.md.
PoC changes
Harness written fresh: guard-page mmap placement so the NUL sentinel is the last byte of a mapped page; three probes (sentinel-immediate, 5-byte prefix match, 9-byte prefix match) to cover byte-wise vs widened realizations.
Verified recommended fix
Validate xml[1] against EOF before MATCH and use strncmp (stops at NUL in either operand); validate cp[1]/cp[2] bytes individually in the find_tag '<!--' test before the widenable comparison.
Verdict
OOB read past the NUL sentinel in _prop_object_internalize_context_alloc MATCH() is real and realized: same-source userland rig crashes with SIGSEGV on a guard page placed at the sentinel (gcc-widened 4-byte load of '?xml' at xml+1, cmpl $0x6c6d783f,0x1(%rbx) in the rig object; libc memcmp completes the vectorized read). In the shipped kernel the same MATCHes compile to bcmp calls whose aligned-longword implementation reads past the object only within the same page, cannot fault, and masks the result β no kernel impact on current codegen; latent UB that becomes a kernel OOB read if bcmp/memcmp is ever vectorized. Fixed and validated userland-side: baseline rig SIGSEGVs, fixed rig (sentinel pre-check + strncmp MATCH + per-byte EOF validation for the find_tag '<!--' test) runs all probes clean.
No comments yet.