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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5676 Β· 9 files
FileTypeDescriptionSize
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 has cmpl $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 bcmp for both MATCHes (objdump of _prop_object_internalize_context_alloc at 0xffffffff809e1510: two callq 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

VERDICT.md
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5676

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.