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

Unprivileged kernel heap info leak via volume_list when device_name not NUL-terminated

Summary

hammer_volume.c:107 ioc->device_name[MAXPATHLEN] passed to hammer_install_volume->kstrdup (hammer_ondisk.c:131) WITHOUT forced NUL termination. kstrdup strlen walks past ioctl buffer into adjacent heap. volume->vol_name captures OOB bytes. hammer_ioc_volume_list:311 len=strlen(volume->vol_name)+1 KKASSERT(len<=MAXPATHLEN) compiled out non-INVARIANTS. copyout(volume->vol_name,...len). LIST_VOLUMES case (hammer_ioctl.c:213) has NO caps_priv_check unlike ADD(197)/DEL(207). Root setups leaky vol_name ANY user recovers via LIST_VOLUMES ioctl. Fix: force NUL on input strnlen on output.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0870 Β· 14 files
FileTypeDescriptionSize
poc.c trigger-source unprivileged probe: HAMMERIOC_LIST_VOLUMES + HAMMERIOC_ADD_VOLUME 4.5 KB view raw
poc_root.c trigger-source root probe: proves ADD_VOLUME w/ non-NUL device_name fails (ENAMETOOLONG) so kstrdup OOB never persists 2.6 KB view raw
build.sh build-script cc -I/usr/src/sys -o poc poc.c; same for poc_root 298 B view raw
run.sh run-script invokes ./poc <file-on-hammer-mount> as the unprivileged user 705 B view raw
run.log run-log maxx run on unpatched #0 kernel - LIST_VOLUMES succeeds, leaks /dev/vn0 332 B view raw
run.root.log run-log root run on #0 - ADD_VOLUME w/ non-NUL name returns ENAMETOOLONG (install fails, no persistent leak) 327 B view raw
fix_run.log run-log maxx run on patched #1 kernel - LIST_VOLUMES now returns EPERM 241 B view raw
fix_build.log build-log tail of nativekernel + installkernel build of the single-fix kernel (rc=0) 5.6 MB ↓ download
env.txt environment uname, cc, vfs.usermount, mount table for the unpatched run 461 B view raw
fix.diff suggested-fix gate LIST_VOLUMES on error==0 (one-line fix, validated) 502 B view raw
VERDICT.md verdict full mechanism walkthrough and impact analysis 7.7 KB ↓ raw
README.md readme build/run/expected + HAMMER1 setup instructions 3.7 KB ↓ 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 build/run/expected + HAMMER1 setup instructions
↓ download raw

DF-0870 PoC β€” Unprivileged HAMMER volume_list disclosure

Claim (from the finding)

An unprivileged user can leak kernel heap bytes via the HAMMER HAMMERIOC_LIST_VOLUMES ioctl because (a) the LIST_VOLUMES case in hammer_ioctl.c:213 has no per-case caps_priv_check (unlike ADD_VOLUME at line 197 / DEL_VOLUME at line 207), and (b) root can volume-add with a non-NUL-terminated device_name so kstrdup walks past the buffer and the leaked heap bytes persist as volume->vol_name, recoverable later by any user via LIST_VOLUMES.

What this PoC actually demonstrates

  • The bypass at hammer_ioctl.c:213-216 IS real and unprivileged. Run as maxx (uid 1001, no wheel) on a file on a HAMMER mount, HAMMERIOC_LIST_VOLUMES returns rc=0 and discloses each volume's vol_no and device_name (the backing block-device path). HAMMERIOC_ADD_VOLUME from the same caller correctly returns EPERM.

  • The "kernel heap info leak" half does NOT reproduce. The kstrdup OOB read is real as a code observation, but it cannot persist:

  • unprivileged users cannot reach hammer_ioc_volume_add (gated by the top-level caps check via if (error == 0) at line 196);
  • even when reached by root, hammer_install_volume() then calls nlookup() on the captured (1024+N)-byte garbage string, which fails with ENAMETOOLONG, so hammer_free_volume() kfree's the leaky vol_name before it is ever inserted into the mount's volume list;
  • the subsequent LIST_VOLUMES still returns the original clean device path.

So the user-observable leak is the volume device path (e.g. /dev/vn0), which is normal filesystem metadata β€” not uninitialized kernel heap bytes.

See VERDICT.md for the full mechanism walkthrough with path:line citations.

Setup (one-time, requires root)

The audit guest boots HAMMER2 on /, so a HAMMER1 filesystem must be created for this test. Run on the guest as root:

# 12 GB sparse image is the newfs_hammer minimum (>10 GB) without -f tricks.
dd if=/dev/zero of=/var/tmp/hammer.img bs=1m count=12000
vnconfig vn0 /var/tmp/hammer.img
newfs_hammer -f -L TESTHAMMER /dev/vn0
mkdir -p /mnt/hammer
mount_hammer /dev/vn0 /mnt/hammer
touch /mnt/hammer/testfile
chown maxx:maxx /mnt/hammer/testfile
chmod 666 /mnt/hammer/testfile

Build

./build.sh        # cc -o poc poc.c -I/usr/src/sys ; cc -o poc_root poc_root.c -I/usr/src/sys

Run (as the unprivileged user)

ssh dfbsd-maxx   # or otherwise become uid 1001
cd /path/to/poc/DF-0870
./run.sh /mnt/hammer/testfile

Expected on the unpatched #0 kernel

HAMMERIOC_LIST_VOLUMES: rc=0 errno=0 (Success) nvols=1
  [0] vol_no=0 device_name='/dev/vn0' len=8
HAMMERIOC_ADD_VOLUME (non-NUL device_name): rc=-1 errno=1 (Operation not permitted)

Expected on the patched #1 kernel (after fix.diff)

HAMMERIOC_LIST_VOLUMES: rc=-1 errno=1 (Operation not permitted)
HAMMERIOC_ADD_VOLUME (non-NUL device_name): rc=-1 errno=1 (Operation not permitted)

Run the privileged probe (proves the heap-leak half doesn't persist)

ssh dfbsd
/tmp/poc_root /mnt/hammer/testfile

Expected:

HAMMERIOC_ADD_VOLUME (non-NUL device_name): rc=-1 errno=63 (File name too long)
As expected: install failed -- kstrdup-captured leaked name is not a valid path; vol_name freed.
LIST_VOLUMES after failed ADD: rc=0 errno=0 nvols=1
  [0] vol_no=0 device_name='/dev/vn0' len=8

Fix

fix.diff adds the missing if (error == 0) gate before the hammer_ioc_volume_list() call (matching every privileged sibling case), which closes the demonstrable unprivileged disclosure. The defense-in-depth NUL-termination/strnlen hardening on the device_name field remains worthwhile but addresses a non-user-observable path.

VERDICT.md verdict full mechanism walkthrough and impact analysis
↓ download raw

DF-0870 β€” Unprivileged volume_list disclosure on HAMMER

Verdict (one line)

PARTIALLY REPRODUCED. Two real, distinct code defects exist in sys/vfs/hammer/hammer_ioctl.c, and ONE of them is demonstrated end-to-end as an unprivileged disclosure, but the finding's headline claim β€” "kernel heap info leak via volume_list when device_name not NUL-terminated" β€” is NOT exploitable as written. The kstrdup OOB-read never persists into a volume readable via HAMMERIOC_LIST_VOLUMES, because the volume install always fails when the captured name has trailing heap bytes. The bug that does fire is the missing caps_priv_check enforcement on the HAMMERIOC_LIST_VOLUMES case (the top-of-function privilege check is silently overwritten). That is reachable by an unprivileged user, who then reads back each mounted volume's vol_no and device_name (the backing block-device path) β€” minor info disclosure, not uninitialized kernel heap bytes.

What was actually demonstrated (unprivileged, default GENERIC #0)

maxx (uid 1001, not in wheel) opens a file on a live HAMMER mount and issues HAMMERIOC_LIST_VOLUMES. Result (deterministic across 3 runs):

HAMMERIOC_LIST_VOLUMES: rc=0 errno=0 (Success) nvols=1
  [0] vol_no=0 device_name='/dev/vn0' len=8
HAMMERIOC_ADD_VOLUME (non-NUL device_name): rc=-1 errno=1 (Operation not permitted)

The same caller's HAMMERIOC_ADD_VOLUME correctly returns EPERM, proving the discrepancy is the per-case overwrite, not a missing global check.

Root-cause walkthrough (every hop cited path:line)

hammer_ioctl() begins with a top-level capability check that should gate every case:

/* sys/vfs/hammer/hammer_ioctl.c:72 */
error = caps_priv_check(cred, SYSCAP_NOVFS_IOCTL);

SYSCAP_NOVFS_IOCTL is (__SYSCAP_GROUP_9 | 4) = 0x94 (sys/sys/caps.h:219) β€” it has neither __SYSCAP_NOROOTTEST (0x00040000) nor __SYSCAP_WHEELOK (0x00080000). Per caps_priv_check() (sys/kern/kern_caps.c:328-331), a cred with cr_uid != 0 (and not in group 0) gets EPERM immediately. So maxx should be denied.

Most cases preserve that result with the canonical pattern if (error == 0) { error = hammer_ioc_xxx(...); }. But HAMMERIOC_LIST_VOLUMES does not β€” it unconditionally overwrites error:

/* sys/vfs/hammer/hammer_ioctl.c:213-216 */
case HAMMERIOC_LIST_VOLUMES:
        error = hammer_ioc_volume_list(&trans, ip,
            (struct hammer_ioc_volume_list *)data);
        break;

The EPERM from line 72 is discarded; hammer_ioc_volume_list() runs and returns its own (success) error code. The duplicate caps_priv_check inside HAMMERIOC_ADD_VOLUME at line 197 β€” which the finding flags as the "privileged" sibling β€” is in fact redundant; it only fires when the top-level check already passed (it is nested under if (error == 0)).

hammer_ioc_volume_list() (sys/vfs/hammer/hammer_volume.c:291-330) then walks every mounted volume and copyouts volume->vol_no plus volume->vol_name (the device path captured at mount or volume-add time):

/* sys/vfs/hammer/hammer_volume.c:311-319 */
len = strlen(volume->vol_name) + 1;
KKASSERT(len <= MAXPATHLEN);
...
error = copyout(volume->vol_name,
                &ioc->vols[cnt].device_name[0], len);

vol_name is the string passed to kstrdup() at sys/vfs/hammer/hammer_ondisk.c:131. In normal operation it is the NUL-terminated device path supplied by mount_hammer / hammer volume-add.

Why the headline "kernel heap info leak" claim does NOT hold

The finding's mechanism is: an attacker triggers hammer_ioc_volume_add() (sys/vfs/hammer/hammer_volume.c:62-141) with ioc->device_name[MAXPATHLEN] filled with non-NUL bytes; that buffer is passed verbatim to hammer_install_volume() at line 107, which calls kstrdup(volname, ...) at hammer_ondisk.c:131; kstrdup (sys/kern/kern_slaballoc.c:1296-1308) does strlen(str)+1 and bcopy, so it walks past the 1024-byte buffer into adjacent heap and captures the OOB bytes into the freshly allocated volume->vol_name. So far the code description is accurate.

The chain then breaks at the next steps:

  1. Unprivileged users cannot even reach hammer_ioc_volume_add()'s body. Its case arm preserves the top-level caps check via if (error == 0) (line 196); maxx's EPERM from line 72 short-circuits the whole arm. Verified: HAMMERIOC_ADD_VOLUME returns EPERM for maxx in every run.

  2. Root can issue ADD_VOLUME, but the install always fails when the name has trailing heap bytes. After the leaky kstrdup, hammer_install_volume() calls nlookup_init()/nlookup() on volume->vol_name (the long garbage string). The kernel rejects it with ENAMETOOLONG (errno 63):

HAMMERIOC_ADD_VOLUME (non-NUL device_name): rc=-1 errno=63 (File name too long)

The failure path then calls hammer_free_volume() (hammer_ondisk.c:401), which kfrees volume->vol_name (line 406) and the volume struct. The captured OOB bytes are never persisted to any volume visible to LIST_VOLUMES. A subsequent LIST_VOLUMES call still returns exactly the original mount's clean device path (verified).

  1. Even if the install somehow succeeded, the resulting vol_name would be a string of length > MAXPATHLEN, and hammer_ioc_volume_list:312 has KKASSERT(len <= MAXPATHLEN) β€” on the default GENERIC kernel (INVARIANTS ON) this would panic before the copyout ever ran.

So: the kstrdup OOB read is a real code defect (defense-in-depth worth fixing by NUL-terminating device_name on input and/or using strnlen on output, as the finding suggests), but it does not produce a user-observable heap leak. The user-observable unprivileged disclosure is the volume device path + vol_no, which is normal filesystem metadata, not uninitialized kernel heap.

What this finding is, honestly

  • A real privilege-check bypass in hammer_ioctl.c:213-216 (and the same overwrite pattern appears in several sibling read-only cases β€” GETHISTORY, SYNCTID, GET_PSEUDOFS, WAI_PSEUDOFS, GET_VERSION, GET_INFO, GET_SNAPSHOT, GET_CONFIG, SCAN_PSEUDOFS β€” which the maintainers may or may not consider intentional). The bypass lets any user with a file descriptor on a HAMMER mount enumerate the volumes backing it. That is information disclosure, but of the device-path string, not of kernel heap.
  • A real but non-exploitable code defect: kstrdup of an attacker-supplied non-NUL-terminated device_name does walk past the buffer. It is not reachable by unprivileged users (the ADD path is privileged), and even when reached by root it cannot persist because the install always fails on the long garbage name.

Severity as filed: Low (info leak). Actual demonstrated severity: Info β€” disclosure of the volume device path to any holder of a file descriptor on a HAMMER mount. The "kernel heap info leak" framing is inaccurate.

Files in this evidence pack

file what it is
poc.c unprivileged probe (LIST_VOLUMES + ADD_VOLUME) β€” run as maxx
poc_root.c privileged probe β€” proves root's ADD_VOLUME w/ non-NUL name fails (ENAMETOOLONG), so the leak never persists
build.sh exact cc -I/usr/src/sys build line
run.sh exact run invocation
run.log decisive maxx run on the unpatched #0 kernel
run.root.log root run proving install fails
env.txt guest uname / cc / sysctls / mount table
fix.diff the validated one-line fix: gate LIST_VOLUMES on error == 0
fix_build.log full build of the single-fix kernel
fix_run.log re-run of the PoC on the patched #1 kernel β€” EPERM
manifest.json catalog

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: maxx LIST_VOLUMES baseline rc=0 (leaks /dev/vn0); patched rc=-1 EPERM. Root still works.

BEFORE: maxx rc=0 device_name=/dev/vn0. AFTER: maxx rc=-1 EPERM. Root rc=0 still works.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 10:23:47 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- info disclosure (device path string), not memory corruption. kstrdup OOB read exists but non-exploitable (never persists to list).

Evidence (decisive lines)

BEFORE (maxx): LIST_VOLUMES rc=0 device_name='/dev/vn0'. AFTER (maxx): LIST_VOLUMES rc=-1 EPERM. Root still works on patched.

PoC changes

Authored from scratch: poc.c (unprivileged LIST_VOLUMES + ADD_VOLUME probe), poc_root.c (proves kstrdup OOB doesn't persist), README.md (HAMMER1 setup), fix.diff (gate LIST_VOLUMES on error==0), VERDICT.md, manifest.json.

Verified recommended fix

Add if(error==0) gate around hammer_ioc_volume_list() at hammer_ioctl.c:213-216, matching every privileged sibling case. Supersedes finding proposal (finding addresses kstrdup OOB which is non-user-observable; actual exploitable bug is caps overwrite). Full diff in findings/poc/DF-0870/fix.diff.

Verdict

PARTIALLY REPRODUCED. Real bug is caps_priv_check overwrite: hammer_ioctl.c:213-216 HAMMERIOC_LIST_VOLUMES overwrites top-level EPERM -> maxx gets device_name (/dev/vn0). Finding's headline 'kernel heap info leak via kstrdup OOB' NOT user-observable: kstrdup-captured name too long -> ENAMETOOLONG -> hammer_free_volume frees before insertion. Actual impact: unprivileged disclosure of volume device path.