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

One-byte stack out-of-bounds read in hammer2_vfs_mount label parsing for empty or '@'-only device strings

Field Value
ID DF-2622
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
CWE CWE-125 Out-of-bounds Read
File sys/vfs/hammer2/hammer2_vfsops.c
Lines 1012-1020
Area vfs
Confidence certain
Discovered 2026-08-28
Pass 2 (GLM 5.3 second pass)
Bucket hammer2
Reported pending
Known CVE none
CVE match novel

Summary

In hammer2_vfs_mount the default-label logic computes slice = label[-1] where label can equal devstr (the start of the stack buffer char devstr[MNAMELEN]) when the user-supplied volume string is empty ("") or exactly "@". This reads one byte before the buffer. The byte only selects the automatic label BOOT/ROOT/DATA, giving at most a ~1.6-bit console/log oracle on adjacent stack contents.

Root cause

vfsops.c:989 copyinstr(info.volume, devstr, MNAMELEN-1, &done) accepts any user string. vfsops.c:1007 label = strchr(devstr, '@'). For devstr="": label=NULL β†’ line 1016 label = devstr + strlen(devstr) == devstr. For devstr="@": label==devstr, label[1]==0 β†’ line 1018 sets devstr[0]=0. In both cases line 1020 slice = label[-1] dereferences devstr[-1], one byte before the 1024-byte stack array. The guard at vfsops.c:1008 ((label + 1) - devstr > done) does not reject either case.

Threat model & preconditions

  • Attacker position: any caller of mount(2) with a hammer2 volume argument of "" or "@" (unprivileged when vfs.usermount=1).
  • Privileges gained or impact: 1-byte kernel stack OOB read, indirectly observable through the chosen default label in the console message hammer2_mount: device=... label="BOOT|ROOT|DATA". No write, no crash.
  • Required config or capabilities: attempted hammer2 mount.
  • Reachability: mount syscall.

Proof of concept

Build & run

mount -t hammer2 "" /mnt    # or volume "@"

Expected output

console: hammer2_mount: device="" label="DATA" β€” label (BOOT/ROOT/DATA)
varies with the byte preceding devstr on the stack.

Impact

Minimal: ~1.6-bit stack-content oracle via console label selection.

--- a/sys/vfs/hammer2/hammer2_vfsops.c
+++ b/sys/vfs/hammer2/hammer2_vfsops.c
@@ -1014,10 +1014,16 @@ hammer2_vfs_mount(struct mount *mp, char *path, caddr_t data,
        if (label == NULL)
            label = devstr + strlen(devstr);
        else
            *label = '\0';      /* clean up trailing @ */

-       slice = label[-1];
+       /*
+        * label may equal devstr (empty device string or a
+        * bare "@"), do not read devstr[-1].
+        */
+       if (label > devstr)
+           slice = label[-1];
+       else
+           slice = 0;  /* selects default 'DATA' below */
        switch(slice) {

Timeline

  • 2026-08-28 Discovered during automated audit (pass 2, GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2622 Β· 17 files
FileTypeDescriptionSize
mnt2622.c β€” 932 B view raw
run_2622.sh β€” 613 B view raw
run_stock.log β€” 588 B view raw
console_excerpts.txt β€” 2.5 KB view raw
fix.diff β€” 416 B view raw
instr_build.log β€” 1.2 MB ↓ download
instr_build2.log β€” 5.6 MB ↓ download
instr_install.log β€” 80.4 KB view raw
fix_build.log β€” 5.6 MB ↓ download
fix_install.log β€” 80.4 KB view raw
env.txt β€” 505 B view raw
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
build.sh β€” 361 B view raw
run.sh β€” 101 B view raw
manifest.json β€” 1007 B view raw
verdict.json β€” 4.5 KB view raw

DF-2622 β€” slice = label[-1] reads one byte below devstr[] on empty/'@'-only mount specs

What this pack contains

file what
mnt2622.c direct mount(2) trigger with full control of the hammer2 volume string
run_2622.sh guest trigger battery (β€˜@’, β€˜β€™, controls)
run_stock.log stock kernel #0 run output
console_excerpts.txt console oracle output + instrumented devstr[-1] byte values (0x00 vs 0xff) + fix-kernel output
fix.diff bounds check label > devstr
build/install/env logs shared with the other three findings (same kernels)

Build

Guest (root): cc -O -o mnt2622 mnt2622.c

Run (as root in the guest)

./mnt2622 '@' /mnt/h2x     # devstr="@"  -> label==devstr -> devstr[-1] OOB read
./mnt2622 ''  /mnt/h2x     # devstr=""   -> label==devstr -> devstr[-1] OOB read

Expected

  • Console prints hammer2_mount: device="" label="<BOOT|ROOT|DATA>" β€” the chosen label encodes the class of the stack byte read below devstr[] ('a'β†’BOOT, 'd'β†’ROOT, anything elseβ†’DATA; ~1.6-bit oracle). Mount then fails ENOENT (empty device) β€” the OOB read has already happened.
  • Instrumented kernel prints the actual byte: observed 0x00 on one call and 0xff on the very next β€” live stack garbage below the array, varying with stack history (proof the read is out-of-bounds of any valid object).
  • Fixed kernel: identical user-visible behavior (label=DATA default), devstr[-1] never dereferenced.

Threat: one-byte stack OOB read on the mount path; the only externally visible consequence is the console label choice. Requires mount privilege to trigger; console is typically privileged. Low severity as filed β€” confirmed, no meaningful disclosure boundary crossed.

VERDICT.md
↓ download raw

DF-2622 VERDICT

Status: reproduced (one-byte stack OOB read confirmed live; observable impact: ~1.6-bit console oracle only). Confidence: certain.

Root cause (line-precise)

sys/vfs/hammer2/hammer2_vfsops.c:1007-1020
    label = strchr(devstr, '@');                       /* :1007 */
    if (label && ((label + 1) - devstr) > done) { ... EINVAL }
    if (label == NULL || label[1] == 0) {
        char slice;
        if (label == NULL)
            label = devstr + strlen(devstr);   /* ""    -> label==devstr */
        else
            *label = '\0';                     /* "@"   -> label==devstr */
        slice = label[-1];                      /* :1020  OOB READ        */

devstr is the 80-byte stack array char devstr[MNAMELEN] (vfsops.c:929). Two reachable inputs make label == devstr at :1020:

  • volume string exactly "@" β€” strchr finds index 0, label[1]==0 (the NUL), *label='\0' β‡’ devstr becomes "" and label==devstr;
  • volume string "" β€” label==NULL β‡’ label = devstr+0.

In both cases label[-1] = devstr[-1] reads one byte below the array β€” stale kernel-stack data. The byte's class is then disclosed on the console via the kprintf at :1037 (device="" label=…: 'a'β†’BOOT, 'd'β†’ROOT, elseβ†’DATA).

Reachability: confirmed with a direct mount("hammer2", mp, 0, &info) carrying info.volume = "@" / "" (the copyin at vfsops.c:989 accepts both; the guard at :1008 does not reject them; nothing earlier in sys_mount rejects an empty from-spec). Root privileges (or vfs.usermount=1) required to trigger β€” same precondition class as every mount-spec bug.

Observed

  1. Stock kernel #0 (run_stock.log + console_excerpts.txt [A]): both inputs reach :1020; console shows device="" label="DATA" and the mount then fails ENOENT via hammer2_init_devvp("").
  2. Instrumented kernel #1 (console_excerpts.txt [B]): * volume="" β†’ label==devstr=1 devstr[-1]=0x00 * volume="@" β†’ label==devstr=1 devstr[-1]=0xff The byte below devstr differs between two consecutive mount(2) calls β€” it is uninitialized stack memory, not any valid object. Neither value mapped to 'a'/'d' so the label printed DATA both times; the oracle is real (had the stale byte been 0x61/0x64, BOOT/ROOT would print and the mount would search for that PFS).
  3. In-bounds reference behavior captured for the same code path: boot root mount devstr="vbd0s1d" β†’ label[-1]='d' β†’ ROOT (the feature working as intended), and /dev/vn0@ β†’ label[-1]='0' β†’ DATA.

Impact (honest ceiling)

One byte of kernel stack is read out of bounds; only its 3-way class (~1.6 bits) reaches the console, which the same (privileged) user who triggered the mount can see. No memory content is returned to userland, no corruption. Low severity as filed β€” correct.

Fix

-       slice = label[-1];
+       if (label > devstr)
+           slice = label[-1];
+       else
+           slice = 0;  /* empty device: no slice letter */

Empty device now selects the default (DATA) without the read; the BOOT/ROOT slice-letter convenience keeps working for real devices.

Fix validation (kernel #2)

'@' and '' mounts: same ENOENT failure, same label="DATA" console line, and by construction devstr[-1] is never dereferenced (label > devstr guard). No behavior change for valid specs (root mount still selects ROOT via in-bounds 'd').

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (label > devstr guard, slice=0 default) included in the combined kernel #2 build (fix_build.log). Re-running the exact triggers: '@' and '' mounts fail ENOENT exactly as before and the console still shows device="" label="DATA" (the sane default), with devstr[-1] now never dereferenced; the in-bounds slice-letter feature is preserved (boot root mount still selects ROOT via 'd'). No OOB read is possible on the patched path by construction.

['fix.diff', 'console_excerpts.txt [C]', 'fix_build.log / fix_install.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Sat Aug 29 00:17:00 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['run_stock.log - \'./mnt2622 @ /mnt/h2x\' and \'./mnt2622 "" /mnt/h2x\' both FAILED errno=2 while the console printed hammer2_mount: device="" label="DATA" (the OOB-read-dependent label decision)', 'console_excerpts.txt [B] - instrumented kernel: DF2622: label==devstr=1 devstr[-1]=00 on one call, devstr[-1]=ff on the next (varying stale stack byte, both labeled OUT-OF-BOUNDS by the instrumentation)', 'console_excerpts.txt [B] - in-bounds references: boot root mount devstr="vbd0s1d" label[-1]=\'d\' -> ROOT; "/dev/vn0@" -> label[-1]=\'0\' -> DATA', 'console_excerpts.txt [C] - fix kernel #2: same triggers, label=DATA, no dereference below devstr', 'mnt2622.c - direct mount(2) with attacker-controlled hammer2_mount_info.volume']

PoC changes

No seed code. Wrote a direct mount(2) trigger (struct hammer2_mount_info per hammer2_mount.h) because the userland mount(8) path was unnecessary; confirmed copyinstr at :989 accepts both "@" and "" and the :1008 guard does not reject them. Captured console via the QEMU serial log.

Verified recommended fix

In hammer2_vfs_mount only read label[-1] when label > devstr; otherwise default the slice to 0 (empty device string has no partition letter).

Verdict

CONFIRMED live on the stock kernel: both mount(2) inputs that make label==devstr (volume string exactly "@" and volume string "") reach vfsops.c:1020 and execute slice = label[-1] = devstr[-1], reading one byte BELOW the 80-byte devstr[MNAMELEN] stack array. The byte's class leaks to the console through the label choice printed at :1037 ('a'->BOOT, 'd'->ROOT, else->DATA, ~1.6 bits) before the mount fails ENOENT in hammer2_init_devvp(""). The instrumented kernel printed the actual byte: 0x00 on one call and 0xff on the immediately following call - live stack garbage below the array varying with stack history, proving the read is out of bounds of any valid object. Honest ceiling: no memory content reaches userland, the console is the same privileged context that issued the mount, and no corruption occurs - Low severity as filed is correct; the defect itself (unconditional label[-1]) is real and trivially fixed. fix.diff guards with (label > devstr) and defaults slice=0; on kernel #2 the '@'/'' triggers show identical user-visible behavior (label=DATA, ENOENT) with devstr[-1] never dereferenced, and in-bounds cases (root mount 'vbd0s1d'->ROOT, '/dev/vn0@'->DATA) are unchanged.