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)
PoC verification
Evidence pack
findings/poc/DF-0870 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_VOLUMESioctl because (a) the LIST_VOLUMES case inhammer_ioctl.c:213has no per-casecaps_priv_check(unlike ADD_VOLUME at line 197 / DEL_VOLUME at line 207), and (b) root canvolume-addwith a non-NUL-terminateddevice_namesokstrdupwalks past the buffer and the leaked heap bytes persist asvolume->vol_name, recoverable later by any user via LIST_VOLUMES.
What this PoC actually demonstrates
-
The bypass at
hammer_ioctl.c:213-216IS real and unprivileged. Run asmaxx(uid 1001, no wheel) on a file on a HAMMER mount,HAMMERIOC_LIST_VOLUMESreturnsrc=0and discloses each volume'svol_noanddevice_name(the backing block-device path).HAMMERIOC_ADD_VOLUMEfrom the same caller correctly returnsEPERM. -
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 viaif (error == 0)at line 196); - even when reached by root,
hammer_install_volume()then callsnlookup()on the captured (1024+N)-byte garbage string, which fails withENAMETOOLONG, sohammer_free_volume()kfree's the leakyvol_namebefore it is ever inserted into the mount's volume list; - the subsequent
LIST_VOLUMESstill 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.
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:
-
Unprivileged users cannot even reach
hammer_ioc_volume_add()'s body. Its case arm preserves the top-level caps check viaif (error == 0)(line 196); maxx'sEPERMfrom line 72 short-circuits the whole arm. Verified:HAMMERIOC_ADD_VOLUMEreturnsEPERMfor maxx in every run. -
Root can issue
ADD_VOLUME, but the install always fails when the name has trailing heap bytes. After the leakykstrdup,hammer_install_volume()callsnlookup_init()/nlookup()onvolume->vol_name(the long garbage string). The kernel rejects it withENAMETOOLONG(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).
- Even if the install somehow succeeded, the resulting
vol_namewould be a string of length > MAXPATHLEN, andhammer_ioc_volume_list:312hasKKASSERT(len <= MAXPATHLEN)β on the default GENERIC kernel (INVARIANTS ON) this would panic before thecopyoutever 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:
kstrdupof an attacker-supplied non-NUL-terminateddevice_namedoes 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
fixedVALIDATED: 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.
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.
No comments yet.