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

Heap OOB read when parsing crafted directory entries near buffer tail β€” missing d_reclen >= DIRSIZ validation

Summary

ufs_dirhash.c:201-202 ufsdirhash_build: d_reclen==0||d_reclen>DIRBLKSIZ-(pos&(DIRBLKSIZ-1)) β€” only checks fits in 512-byte chunk NOT d_reclen>=DIRSIZ(0,ep) (min to hold own name). Crafted image: entry at chunk offset 504 d_reclen=8 d_namlen=255 d_ino=1. ep->d_name at chunk offset 512 = buffer end. ufsdirhash_hash :209 fnv_32_buf(ep->d_name,ep->d_namlen,...) reads 255 bytes past buffer into kernel heap OOB read. Same missing check at lookup :370-378 findfree :468-501 getprev :914-916 checkblock :746-749. ufs_lookup.c has ufs_dirbadentry but dirchk defaults 0 dirhash never calls. Trigger: crafted UFS image mount then ls crafted_dir. Fix: add d_reclen>=DIRSIZ check for d_ino!=0 entries.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0844 Β· 15 files
FileTypeDescriptionSize
dirhash_oob.c trigger-source Userspace harness: replicates dirhash build loop with guard page, deterministically proves OOB read 8.4 KB view raw
dirhash_patch.c trigger-source UFS image patcher: creates malformed dir entry (d_reclen=8, d_namlen=255) + trailing free entry 5.5 KB view raw
dirhash_corrupt.c trigger-source Earlier image corruptor (superseded by dirhash_patch.c) 5.2 KB view raw
image_trigger.sh trigger-script Shell script for full image workflow (reference) 3.8 KB view raw
build.sh build-script Build commands for harness and corruptor 273 B view raw
run.sh run-script Run commands for harness 276 B view raw
fix.diff suggested-fix git-apply-able fix: add d_reclen >= DIRSIZ(NEWDIRFMT, ep) check in ufsdirhash_build 776 B view raw
fix_build.log build-log Single-fix kernel build output (rc=0) 5.6 MB ↓ download
run_harness_patched.log run-log Harness output on patched kernel 1.3 KB view raw
env.txt environment Guest uname, cc version, sysctls 361 B view raw
VERDICT.md verdict Full analysis: mechanism, evidence, fix validation 7.0 KB ↓ raw
README.md readme human reproduce doc 2.0 KB ↓ raw
build.log build-log kernel build log excerpt proving -Werror clean compile of patched source 219 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human reproduce doc
↓ download raw

DF-0844 β€” Heap OOB read in UFS dirhash (missing d_reclen >= DIRSIZ check)

Summary

ufsdirhash_build() in sys/vfs/ufs/ufs_dirhash.c validates that each directory entry's d_reclen fits within its 512-byte DIRBLKSIZ chunk (line 201-202), but does not validate that d_reclen is large enough to hold the entry's own name (d_reclen >= DIRSIZ(NEWDIRFMT, ep)). A crafted UFS image with a truncated final entry (d_reclen=8, d_namlen=255) passes the existing check but causes ufsdirhash_hash() to read 255 bytes from ep->d_name β€” extending 247 bytes past the entry boundary into adjacent kernel heap (CWE-125).

The check already exists in ufs_dirbadentry() (ufs_lookup.c:636) but is gated behind dirchk=0 (default off) and never called by dirhash.

Build

./build.sh

Produces dirhash_oob (userspace harness) and dirhash_corrupt (image corruptor).

Run

Userspace harness (deterministic OOB proof)

./dirhash_oob          # Buggy mode: SIGSEGV at guard page = OOB confirmed
./dirhash_oob --fixed  # Fixed mode: entry rejected, no OOB

Image-based trigger (live kernel)

Requires root on the guest to mount. See image_trigger.sh for the full workflow, or use dirhash_patch.c to create a malformed UFS image:

# Create a UFS image with 25 files in testdir (dir size = 6656 bytes)
# (see image_trigger.sh for vnconfig/newfs/mount commands)

# Patch the last directory entry to be malformed
./dirhash_patch /tmp/df844.img $((1033 * 512))

# Mount and trigger dirhash
mount -o ro /dev/vn0 /mnt
ls -f /mnt/testdir/
sysctl -n vfs.ufs.dirhash_mem  # increases on unpatched, unchanged on patched

Expected results

Kernel dirhash_mem Harness
Unpatched (#0) 9685 β†’ 14177 (dirhash built with malformed entry) OOB READ CONFIRMED
Patched (#1) 9685 β†’ 9685 (entry rejected, no dirhash) Entry REJECTED

Fix

See fix.diff: adds d_reclen >= DIRSIZ(NEWDIRFMT, ep) check before hashing.

VERDICT.md verdict Full analysis: mechanism, evidence, fix validation
↓ download raw

DF-0844 β€” Verdict

Verdict: REPRODUCED (heap OOB read, CWE-125). Fix VALIDATED.

Finding: Heap OOB read when parsing crafted directory entries near buffer tail β€” missing d_reclen >= DIRSIZ validation in ufsdirhash_build().

Impact: Heap out-of-bounds read (up to 255 bytes) from kernel buffer cache memory. The OOB data is fed to the FNV hash function (internal to dirhash) and is not directly returned to userspace. Realistic impact ceiling: silent hash corruption or, if the OOB crosses a page boundary into unmapped memory, kernel panic (DoS). Not a direct info leak.

Severity assessment: Medium is appropriate. The bug is a genuine heap OOB read triggered by a crafted filesystem image (root-mount threat model), but the read data goes into an internal hash, not to userspace.


Mechanism (line-by-line)

The vulnerable code: sys/vfs/ufs/ufs_dirhash.c:199-216

ep = (struct direct *)((char *)bp->b_data + (pos & bmask));
if (ep->d_reclen == 0 || ep->d_reclen >
    DIRBLKSIZ - (pos & (DIRBLKSIZ - 1))) {     // ← only checks fits in 512-byte chunk
    brelse(bp);                                  // ← does NOT check d_reclen >= DIRSIZ
    goto fail;
}
if (ep->d_ino != 0) {
    slot = ufsdirhash_hash(dh, ep->d_name, ep->d_namlen);  // ← reads d_namlen bytes
    ...                                                      //   from d_name, OOB if
}                                                            //   d_reclen < DIRSIZ

The existing check at ufs_dirhash.c:201-202 validates that d_reclen fits within the current 512-byte DIRBLKSIZ chunk. It does not validate that d_reclen is large enough to hold the entry's own name (d_reclen >= DIRSIZ(NEWDIRFMT, ep)).

The missing check (already exists in ufs_dirbadentry)

sys/vfs/ufs/ufs_lookup.c:634-636 has exactly the check that dirhash lacks:

if ((ep->d_reclen & 0x3) != 0 ||
    ep->d_reclen > DIRBLKSIZ - (entryoffsetinblock & (DIRBLKSIZ - 1)) ||
    ep->d_reclen < DIRSIZ(OFSFMT(dp), ep) || namlen > MAXNAMLEN) {

But ufs_dirbadentry is only called when dirchk is non-zero (ufs_lookup.c:65-67: int dirchk = 0;), and the dirhash code never calls it.

OOB geometry

When a crafted entry has d_reclen=8 (passes chunk check when at chunk offset β‰₯ 504) but d_namlen=255: - DIRSIZ(0, ep) = DIRECTSIZ(255) = (8 + 256 + 3) & ~3 = 264 - d_reclen(8) < DIRSIZ(264) β€” malformed, but accepted by the existing check - ufsdirhash_hash(dh, ep->d_name, ep->d_namlen) calls fnv_32_buf(ep->d_name, 255, ...) - This reads 255 bytes from ep->d_name, extending 247 bytes past the entry's d_reclen boundary into adjacent data or past the kernel buffer tail

Trigger path (root-mount threat model)

  1. Attacker crafts a UFS filesystem image with a directory β‰₯ 2560 bytes (to trigger dirhash: ufs_mindirhashsize = DIRBLKSIZ * 5)
  2. The directory's last entry is malformed: d_reclen=8, d_namlen=255, d_ino!=0
  3. A trailing free entry (d_ino=0) covers the rest of the chunk so the loop ends cleanly
  4. Admin mounts the image (mount /dev/vn0 /mnt)
  5. Unprivileged user triggers dirhash build via readdir() or stat() on the directory
  6. ufsdirhash_build() iterates entries, accepts the malformed one, and ufsdirhash_hash() reads 255 bytes past the entry boundary

Reproduction evidence

1. Userspace harness (deterministic proof of OOB mechanism)

dirhash_oob.c replicates the dirhash build loop with a crafted entry placed at the exact tail of a page, followed by a PROT_NONE guard page. When fnv_32_buf reads ep->d_name (255 bytes), it faults into the guard page:

RESULT: *** OOB READ CONFIRMED ***
        fnv_32_buf(ep->d_name, 255) read past the buffer into the guard page.
        The missing d_reclen >= DIRSIZ(ep) check allowed a 255-byte
        out-of-bounds read from the entry's d_name field.

With --fixed flag (simulating the fix): entry correctly rejected, no OOB read.

2. Image-based PoC (live kernel reachability)

A crafted UFS image (dirhash_patch.c) with a malformed last entry was mounted on the unpatched #0 kernel:

dirhash_mem BEFORE trigger  = 9685
dirhash_mem AFTER readdir   = 14177  ← INCREASED (dirhash built with malformed entry)

The dirhash build SUCCEEDED with the malformed entry, proving the missing check allows acceptance. ufsdirhash_hash was called with the corrupted d_namlen=255, performing the OOB read. Guest remained alive (silent OOB, no panic).


Exploit chain

This is an OOB read, not a write primitive. No escalation to uid=0 is possible β€” the read data goes into the internal FNV hash, not to userspace. The realistic impact ceiling is: 1. Silent hash corruption β€” the dirhash produces wrong results, causing directory lookup failures or incorrect behavior (not a security impact per se) 2. DoS via panic β€” if the OOB read crosses a page boundary into unmapped memory, the kernel panics. This requires specific slab layout alignment (unlikely but possible) 3. No info leak β€” the OOB bytes are hashed and never returned to userspace


Fix

fix.diff

Adds d_reclen >= DIRSIZ(NEWDIRFMT, ep) check for entries with d_ino != 0 in ufsdirhash_build(), right before the ufsdirhash_hash call:

if (ep->d_ino != 0) {
    if (ep->d_reclen < DIRSIZ(NEWDIRFMT, ep)) {
        /* Corrupted directory. */
        brelse(bp);
        goto fail;
    }
    slot = ufsdirhash_hash(dh, ep->d_name, ep->d_namlen);

This matches the existing check in ufs_dirbadentry (ufs_lookup.c:636).

Fix validation

Built single-fix kernel (6.5-DEVELOPMENT #1, sha256 8ddcb6a0...), booted, re-ran the same crafted image:

PATCHED (#1):
  dirhash_mem BEFORE trigger  = 9685
  dirhash_mem AFTER readdir   = 9685  ← UNCHANGED (dirhash NOT built)
  dirhash_mem AFTER lookup    = 9685  ← UNCHANGED

The malformed entry is now rejected by the new check β†’ ufsdirhash_build fails β†’ dirhash is NOT created β†’ ufsdirhash_hash is NEVER called β†’ no OOB read. Directory lookups fall back to linear scan (correct behavior for corrupted directories).

Before/after contrast: - Unpatched (#0): dirhash_mem 9685 β†’ 14177 (malformed entry accepted, OOB read happened) - Patched (#1): dirhash_mem 9685 β†’ 9685 (malformed entry rejected, no OOB read)

Fix status: FIXED.


PoC files

File Type Description
dirhash_oob.c trigger-source Userspace harness: guard-page OOB proof
dirhash_patch.c trigger-source UFS image patcher: creates malformed dir entry
dirhash_corrupt.c trigger-source Earlier image corruptor (superseded by dirhash_patch.c)
image_trigger.sh trigger-script Shell script for full image workflow (reference)
build.sh build-script Build commands
run.sh run-script Run commands
fix.diff suggested-fix git-apply-able fix adding DIRSIZ check
fix_build.log build-log Single-fix kernel build output
run_harness_patched.log run-log Harness output on patched kernel
env.txt environment Guest environment

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: the crafted UFS image (malformed entry d_reclen=8/d_namlen=255) caused dirhash_mem to increase 9685->14177 on the unpatched #0 baseline (dirhash built, ufsdirhash_hash called with OOB namlen). On the single-fix #1 kernel, the same image caused dirhash_mem to stay at 9685 (dirhash NOT built -- the new d_reclen >= DIRSIZ check rejected the malformed entry before hashing, no OOB read). Fix closes the bug.

baseline #0: BEFORE=9685 AFTER_LOOKUP=14177 (dirhash built with malformed entry -> OOB read happened). patched #1: BEFORE=9685 AFTER_READDIR=9685 AFTER_LOOKUP=9685 (dirhash NOT built -> entry rejected -> no OOB read).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 11 11:29:01 UTC 2026 (sha256 8ddcb6a009154c34db6859ee3f4b17348c1470eae22d467fba8e030e7ada3d35)

Confirmed kernel references

Detail

Exploit chain

This is an OOB READ (CWE-125), not a write primitive. No escalation to uid=0 is possible: the read data is fed into the internal FNV hash function (ufsdirhash_hash) and never returned to userspace. The realistic impact ceiling is: (1) silent dirhash corruption causing incorrect directory lookups, or (2) DoS via panic if the 255-byte read crosses a page boundary into unmapped memory (unlikely but geometry-dependent). No info leak to userspace. For non-corruption classes, no chain is needed -- impact characterized and documented.

Evidence (decisive lines)

Harness (buggy mode): 'RESULT: *** OOB READ CONFIRMED *** fnv_32_buf(ep->d_name, 255) read past the buffer into the guard page.' Harness (--fixed): 'Entry REJECTED by bounds check.' Image PoC unpatched #0: dirhash_mem 9685->14177 (malformed entry accepted). Image PoC patched #1: dirhash_mem 9685->9685 (malformed entry rejected). Crafted entry: d_reclen=8, d_namlen=255, DIRSIZ(255)=264 > d_reclen(8) -> MALFORMED.

PoC changes

Created PoC evidence pack from scratch (no prior poc_results or poc folder existed). Wrote: dirhash_oob.c (userspace harness replicating dirhash build loop with guard-page OOB detection), dirhash_patch.c (UFS image patcher that creates malformed entry d_reclen=8/d_namlen=255 + trailing free entry for clean loop termination), dirhash_corrupt.c (earlier corruptor, superseded), build.sh, run.sh, baseline_run.sh, image_trigger.sh. All sources, logs, VERDICT.md, manifest.json, fix.diff saved to findings/poc/DF-0844/.

Verified recommended fix

Add d_reclen >= DIRSIZ(NEWDIRFMT, ep) check inside the 'if (ep->d_ino != 0)' block in ufsdirhash_build() at sys/vfs/ufs/ufs_dirhash.c:207, before the ufsdirhash_hash call at line 209. This matches the existing check in ufs_dirbadentry() (ufs_lookup.c:636). If the check fails, treat as corrupted directory (brelse + goto fail). Matches finding proposal (the finding also recommended 'add d_reclen >= DIRSIZ check for d_ino!=0 entries'). The full git-apply-able diff lives in findings/poc/DF-0844/fix.diff.

Verdict

REPRODUCED. The bug is real: ufsdirhash_build() at sys/vfs/ufs/ufs_dirhash.c:201-202 validates d_reclen fits within the 512-byte DIRBLKSIZ chunk but does NOT validate d_reclen >= DIRSIZ(NEWDIRFMT, ep) (the minimum to hold the entry's own name). A crafted UFS image with a truncated final entry (d_reclen=8, d_namlen=255) passes the existing check but causes ufsdirhash_hash() at line 209 to call fnv_32_buf(ep->d_name, 255) which reads 255 bytes from d_name, extending 247 bytes past the entry boundary into adjacent kernel heap -- a CWE-125 OOB read. The check already exists in ufs_dirbadentry() (ufs_lookup.c:636: d_reclen < DIRSIZ(OFSFMT(dp), ep)) but dirchk defaults to 0 and dirhash never calls it. Confirmed by: (1) userspace harness with guard page -> deterministic SIGSEGV proving the OOB mechanism, (2) image-based PoC on live unpatched kernel -> dirhash_mem increased 9685->14177 (malformed entry accepted, dirhash built, ufsdirhash_hash called with corrupted namlen). The OOB data goes into the internal FNV hash, not to userspace -- impact is silent hash corruption or potential DoS via panic if OOB crosses a page boundary.