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

Unbounded while(*cp!="\"") scan in XML attribute-value parser causes heap OOB read / kernel panic

Summary

_prop_object_internalize_find_tag scans XML attribute value with while(*cp!="\"") cp++ and only checks for NUL terminator AFTER loop has already overrun buffer. Kernel-side copyin path NUL-terminates attacker XML at buf[pref_len] so plist tag whose attribute value opens quote but never closes (e.g. <plist version="X) drives scan past kmalloc buffer into adjacent kernel heap. Scan continues until it hits 0x22 byte in freed/adjacent memory or walks into unmapped page and panics kernel. Loop at 484-485 has no terminator check in body. Every other scanner uses _PROP_ISSPACE (includes _PROP_EOF) stops at NUL but this one uses bare *cp!="\"" test NUL (0x00) is not 0x22 so loop sails past buffer NUL sentinel. _PROP_EOF check at 486 is dead defense by the time control reaches it *cp is whatever byte runaway scan stopped on. Earlier EOF check at 480-481 only proves *cp!=NUL immediately after opening quote does not bound subsequent scan. Reachable: prop_kern.c:398 kmalloc pref_len+1 copyin at 399 NUL-terminate at 404 prop_dictionary_internalize -> _prop_generic_internalize -> _prop_object_internalize_find_tag. Attacker: any local principal who can issue proplib ioctl (device-mapper.c:267 NETBSD_DM_IOCTL on /dev/mapper/control tbridge.c:258 kern_udev.c:892). Impact: (a) kernel panic very high probability scan reads linearly past kmalloc almost always reaches unmapped page reliable local kernel DoS; (b) on rare run 0x22 found in adjacent live heap poic_cp left pointing into kernel heap downstream parsing constructs prop objects string/data values copied back to userspace via prop_dictionary_copyout_ioctl leaking kernel heap contents. 64KB copyin limit doesnt help bug is about missing closing quote not input size.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2227 Β· 12 files
FileTypeDescriptionSize
oob_read_poc.c trigger-source privileged reproducer via /dev/udev UDEVPROP; mmap'd unclosed-quote plist; argv[1]=pref_len (60000=panic, 17=silent) 4.8 KB view raw
build.sh build-script cc -O2 -o oob_read_poc oob_read_poc.c 187 B view raw
run.sh run-script ./oob_read_poc [pref_len]; MUST run as root (/dev/udev is 0600) 457 B view raw
build.log build-log final successful userspace build 70 B view raw
run.log run-log baseline (#0) panic run summary 871 B view raw
panic.txt panic-signature Fatal trap 12 page fault in _prop_object_internalize_find_tag+0x32f cmpb $0x22,(%rax) at page-aligned addr 0xfffff8011864f000 1.4 KB view raw
fix_build.log build-log single-fix nativekernel build (NK_DONE rc=0) 6.4 KB view raw
fix_run.log run-log patched (#1) 4x clean EIO runs, guest up, 0 fatal traps 1.1 KB view raw
env.txt environment uname, cc, /dev/udev perms (root:wheel 0600), vfs.quota.enabled 342 B view raw
fix.diff suggested-fix per-iteration _PROP_EOF(*cp) check in while loop + stricter post-loop check 411 B view raw
VERDICT.md verdict full narrative: reproduced panic, mechanism, root-only reachability hard blocker, fix validated 8.7 KB ↓ raw
README.md readme human-facing build/run/expected 2.3 KB ↓ raw
README.md readme human-facing build/run/expected
↓ download raw

DF-2227: unbounded while (*cp != '"') scan in XML attribute-value parser

Bug

_prop_object_internalize_find_tag (sys/libprop/prop_object.c:484-485) scans an XML attribute value with while (*cp != '"') cp++; β€” no NUL check in the loop body. Every other scanner in the same function uses _PROP_ISSPACE (which includes _PROP_EOF, i.e. stops at NUL); only this loop uses the bare *cp != '"' test. The kernel copyin path NUL-terminates at exactly buf[pref_len] (prop_kern.c:404), so an attribute value that opens a " but never closes it (<plist version="X, pref_len == strlen) drives the scan past the NUL sentinel (0x00 β‰  0x22) into adjacent kernel heap.

Trigger (privileged)

Only privileged callers reach prop_dictionary_copyin_ioctl() on the default GENERIC guest. This PoC uses /dev/udev UDEVPROP (root:wheel 0600 β€” the only caller compiled into the base kernel). See VERDICT.md / DF-2231 for the full reachability analysis (all four callers are privileged).

Build / Run

cc -O2 -o oob_read_poc oob_read_poc.c   # or: ./build.sh
./run.sh            # as root; default pref_len=60000 -> kernel panic
./run.sh 17         # small pref_len -> silent OOB read -> clean EIO

Expected

  • Unpatched kernel (#0) with pref_len=60000 (large β†’ kmem/page alloc): kernel panic β€” page fault in the buggy loop itself: Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff8011864f000 (page-aligned, 1st byte past buffer) Stopped at _prop_object_internalize_find_tag+0x32f: cmpb $0x22,(%rax)
  • Patched kernel (#1): clean errno=5 EIO (unterminated quote now detected as a parse error at the NUL sentinel), no panic, guest stays up. Well-formed XML still parses normally.

Impact

Privileged kernel DoS (panic via page fault) + latent OOB read. NOT an unprivileged escalation: root-only reachability (valid hard blocker) AND a read-only primitive (no corruption to convert β€” valid hard blocker). The finding's secondary "heap leak via copyout" is not realised β€” the parse always fails after the OOB scan, so no kernel bytes reach userspace.

Fix

Add the per-iteration NUL check (same idiom the other scanners use): while (!_PROP_EOF(*cp) && *cp != '"') cp++; β€” see fix.diff (validated).

VERDICT.md verdict full narrative: reproduced panic, mechanism, root-only reachability hard blocker, fix validated
↓ download raw

DF-2227 β€” unbounded while (*cp != '"') scan in XML attribute-value parser

Verdict

REPRODUCED β€” the code claim is CONFIRMED: the attribute-value scan loop at sys/libprop/prop_object.c:484-485 (while (*cp != '"') cp++;) has no NUL check in its body, and the kernel copyin path NUL-terminates attacker XML at exactly buf[pref_len] (prop_kern.c:404). The pre-loop _PROP_EOF check at line 480 only verifies the first byte after the opening quote β€” it does not bound the subsequent scan. A plist whose attribute value opens a " but never closes it (e.g. <plist version="X with pref_len == strlen) drives the scan past the NUL sentinel into adjacent kernel memory: '\0' (0x00) β‰  '"' (0x22), so the loop advances and reads uninitialised/unmapped kernel heap. Confirmed as a kernel panic (page fault in the buggy instruction itself). The proposed fix was VALIDATED on a single-fix kernel (panic β†’ clean EIO, guest stays up; legitimate-XML parse path unchanged).

Impact (honest, on the default GENERIC guest)

Privileged kernel DoS (panic via page fault) + latent OOB read. NOT an unprivileged escalation — two independent valid hard blockers apply (see Phase 6): 1. Root-only reachability. All four kernel callers of prop_dictionary_copyin_ioctl() are privileged on the default guest (re-verified here, consistent with DF-2231): - sys/kern/kern_udev.c:892 UDEVPROP on /dev/udev → node is root:wheel 0600 (maxx gets EPERM). This is the only caller compiled into the base kernel and is what the PoC exercises (as root). - sys/kern/vfs_quota.c:346 vquotactl(530) → gated by vfs_quota_enabled (default 0, CTLFLAG_RD); sysctl vfs.quota.enabled is unset on this guest. - sys/dev/disk/dm/device-mapper.c:267 NETBSD_DM_IOCTL → dm module not loaded (would be 0640 root:operator). - sys/dev/misc/tbridge/tbridge.c:258 TBRIDGE_LOADTEST → module not loaded. => No unprivileged trigger exists. root→kernel is game-over by definition; this is a privileged DoS / hardening gap, not unpriv→root. 2. Read-only primitive. This is an OOB read (cmpb $0x22,(%rax)), not a write — there is no corruption primitive to convert into control flow, so no escalation chain is derivable regardless of reachability.

The finding's secondary "kernel heap leak via copyout" claim is not realised on this path: after the runaway scan (if it stops on a 0x22 byte in adjacent heap rather than faulting), the very next checks at prop_object.c:486-492 almost always return false (the byte after the stray 0x22 is not >), so _prop_object_internalize_find_tag returns false β†’ prop_dictionary_internalize returns NULL β†’ EIO. No prop objects are constructed from the OOB bytes, so nothing is copied back to userspace. The realistic ceiling is privileged kernel DoS.

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger. A privileged caller (/dev/udev UDEVPROP, root) issues an ioctl whose ap->a_data is a user struct plistref { void *pref_plist; size_t pref_len; } (sys/libprop/plistref.h:43-45).
  2. Sink. udev_dev_ioctl() (kern_udev.c:892) β†’ prop_dictionary_copyin_ioctl() β†’ _prop_object_copyin() (prop_kern.c:398): c buf = kmalloc(pref->pref_len + 1, M_TEMP, M_WAITOK); /* 398 */ error = copyin(pref->pref_plist, buf, pref->pref_len); /* 399 */ ... buf[pref->pref_len] = '\0'; /* 404 β€” sole terminator */ The NUL sentinel lands at exactly buf[pref_len] β€” the last allocated byte.
  3. Buggy scan (prop_object.c _prop_object_internalize_find_tag, parsing the <plist version="..."> tag): c cp++; /* 479: cp -> byte right after opening '"' */ if (_PROP_EOF(*cp)) /* 480: ONLY checks the FIRST post-quote byte */ return (false); ctx->poic_tagattrval = cp; /* 483 */ while (*cp != '\"') /* 484: *** NO NUL CHECK IN BODY *** */ cp++; /* 485: 'X'->cp++, '\0'(0x00!=0x22)->cp++ = OOB! */ Every other scanner in this function bounds itself with _PROP_ISSPACE (which includes _PROP_EOF, i.e. stops at NUL): lines 379, 423, 462, 469. Only the attribute-value loop at 484 uses the bare *cp != '"' test, so it sails past the NUL sentinel.
  4. Effect. cp now points past the allocation. The loop keeps reading adjacent kernel VA one byte at a time until it either finds a 0x22 byte (parse then fails cleanly β†’ EIO, silent OOB read) or walks into an unmapped page β†’ page fault β†’ kernel panic. - Reproduced reliably as a panic with a large pref_len (60000): the kmalloc(60001) allocation goes through the kmem/page allocator, so the byte immediately after buf[pref_len] sits at a page boundary that is typically unmapped β†’ fault on the first OOB read. Panic signature (2/2 runs): Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff8011864f000 (page-aligned, 1st byte past buffer) fault code = supervisor read data, page not present Stopped at _prop_object_internalize_find_tag+0x32f: cmpb $0x22,(%rax) The faulting instruction cmpb $0x22,(%rax) is the body of the loop at line 484 β€” definitive attribution. - With a small pref_len (17 β†’ kmalloc-32 slab bucket) the scan usually finds a 0x22 byte in adjacent slab memory first β†’ silent OOB read β†’ EIO.

Exploit chain

Not applicable (read-only primitive + root-only reachability = two valid hard blockers per Phase 6; no escalation chain is derivable). Impact ceiling is privileged kernel DoS (panic). No exploit.c/chain.c authored β€” the trigger PoC (oob_read_poc.c) is the deliverable and demonstrates the panic.

PoC changes

Rewrote the seeded oob_read_poc.c (which targeted the not-loaded dm /dev/mapper/control) into a parameterised privileged demonstrator via the base-kernel /dev/udev UDEVPROP ioctl: - Uses the real <sys/udev.h> UDEVPROP macro + <libprop/plistref.h>. - mmaps a user buffer of pref_len bytes: <plist version=" + 'A' padding, no closing quote, no > β€” so the prop_kern.c:404 NUL is the sole terminator and lands right after the unclosed value. - argv[1] selects pref_len: 60000 (default β†’ kmalloc page-zone β†’ reliable panic) or 17 (slab bucket β†’ silent OOB read β†’ EIO). - Added build.sh/run.sh. Original README claim ("kernel panic") is correct for the large-buffer mode; added the honest silent-OOB variant for the slab mode.

Fix (validated)

Add the per-iteration _PROP_EOF(*cp) check to the loop body β€” the same idiom every other scanner in this function already uses β€” and treat the unterminated case as a parse error. Full diff in fix.diff:

-   while (*cp != '\"')
+   while (!_PROP_EOF(*cp) && *cp != '\"')
        cp++;
-   if (_PROP_EOF(*cp))
+   if (_PROP_EOF(*cp) || *cp != '\"')
        return (false);

git apply --check against sys/: CLEAN (1 file, 2+2 lines).

Fix validation (single-fix kernel, make -j6 nativekernel)

  • Baseline #0 (unpatched, Jul 2 2026): /tmp/oob2227 60000 β†’ kernel PANIC (_prop_object_internalize_find_tag+0x32f: cmpb $0x22,(%rax), page fault @ 0xfffff8011864f000, guest DOWN). Reproduced 2/2.
  • Single-fix #1 (Aug 8 16:51:23 2026, sha256 350f0274…): /tmp/oob2227 60000 β†’ clean errno=5 EIO, NO panic, guest UP, 0 new Fatal trap in boot.log. 3/3 large + 1/1 small variant all clean. The fix's _PROP_EOF(*cp) check stops the scan at the buf[pref_len] NUL sentinel; the unterminated quote is then detected as a parse error.
  • Legit-XML path intact on #1: a well-formed <plist version="1.0"> <dict></dict></plist> still parses (returns EINVAL from the udev handler β€” parse succeeded, handler ran, no "command" key β€” i.e. the parser is not broken by the fix; only the malformed unclosed-quote input now fails cleanly).

Files

  • oob_read_poc.c β€” parameterised privileged demonstrator via /dev/udev UDEVPROP.
  • build.sh / run.sh β€” exact build/run (run.sh [pref_len], default 60000).
  • build.log β€” final clean userspace build.
  • run.log β€” baseline (#0) panic run summary.
  • panic.txt β€” full panic signature from dfbsd-qemu/boot.log (the crash proof).
  • fix_build.log β€” single-fix nativekernel build output (NK_DONE rc=0).
  • fix_run.log β€” patched (#1) clean-run summary (3Γ— large + small, all EIO, guest up).
  • env.txt β€” guest uname, cc, /dev/udev perms, vfs.quota.enabled.
  • fix.diff β€” git apply-able fix (per-iteration NUL check).
  • manifest.json β€” machine-readable catalog.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: ./oob_read_poc 60000 PANICS on unpatched #0 baseline (page fault in _prop_object_internalize_find_tag+0x32f cmpb $0x22,(%rax) @ 0xfffff8011864f000, guest DOWN, 2/2) and does NOT panic on single-fix #1 kernel (clean errno=5 EIO on 3/3 large + 1/1 small runs, guest UP, 0 new Fatal trap in boot.log). Legit-XML path intact on #1 (well-formed plist still parses -> EINVAL). git apply --check CLEAN (1 file, 2+2 lines).

baseline #0: Stopped at _prop_object_internalize_find_tag+0x32f: cmpb $0x22,(%rax) (page fault, guest DOWN). patched #1 run1/run2/run3: ioctl rc=-1 errno=5 (EIO) no panic, guest UP. patched #1 (17): errno=5 EIO no panic. after: 0 new Fatal trap in boot.log; legit plist still parses (EINVAL not EIO).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Aug 8 16:51:23 UTC 2026 (sha256 /boot/kernel/kernel = 350f027415058ca3bf4331a8a16e31ecfa3cc4b218bc9ec5b29cd4c813766d94)

Confirmed kernel references

Detail

Exploit chain

Not applicable (two independent valid hard blockers per Phase 6): (1) READ-ONLY primitive β€” OOB is cmpb $0x22,(%rax) read compare, no write capability, no corruption to groom/convert into control flow; (2) ROOT-ONLY reachability β€” all four kernel callers of prop_dictionary_copyin_ioctl() privileged on default GENERIC (/dev/udev UDEVPROP root:wheel 0600 only base-kernel caller; vquotactl gated vfs_quota_enabled=0; dm/tbridge not loaded), root->kernel game-over by definition. No exploit.c/chain.c. Impact ceiling = privileged kernel DoS (panic via page fault).

Evidence (decisive lines)

BASELINE #0 (unpatched), /tmp/oob2227 60000 as root: kmalloc(60001) -> page alloc -> next byte unmapped -> PANIC (ssh timed out, guest DOWN). boot.log: Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xfffff8011864f000 (page-aligned, 1st byte past buffer) / fault code = supervisor read data, page not present / Stopped at _prop_object_internalize_find_tag+0x32f: cmpb $0x22,(%rax). Reproduced 2/2 on #0.

PoC changes

Rewrote seeded oob_read_poc.c (targeted not-loaded dm) into parameterised privileged demonstrator via base-kernel /dev/udev UDEVPROP ioctl: real UDEVPROP + ; mmaps pref_len-byte buffer (

Verified recommended fix

Add per-iteration NUL check to the while loop at sys/libprop/prop_object.c:484 β€” same idiom every other scanner in the function uses: while (!_PROP_EOF(cp) && cp != '"') cp++; and tighten post-loop check to if (_PROP_EOF(cp) || cp != '"') return (false); so an unterminated attribute value is a clean parse error instead of an OOB scan. Defense-in-depth good hygiene even though reachability is privileged. Full git-apply-able diff in findings/poc/DF-2227/fix.diff; supersedes finding proposal.

Verdict

REPRODUCED. Claim CONFIRMED: the attribute-value scan loop at sys/libprop/prop_object.c:484 (while (*cp != '"') cp++;) has NO NUL check in its body, unlike every other scanner in the same function which use _PROP_ISSPACE (includes _PROP_EOF). The kernel copyin path NUL-terminates at exactly buf[pref_len] (prop_kern.c:404), and the pre-loop _PROP_EOF check at line 480 only verifies the FIRST post-quote byte. Input DragonFlyBSD Kernel Audit Β· static viewer generated 2026-09-06 09:00 UTC read-only audit Β· no live kernel data