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 whenvfs.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.
Recommended fix
--- 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)
PoC verification
Evidence pack
findings/poc/DF-2622 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- Cited:
sys/vfs/hammer2/hammer2_vfsops.c:1012-1020(hammer2_vfs_mount)
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 belowdevstr[]('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.
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
"@"βstrchrfinds 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
- Stock kernel #0 (
run_stock.log+console_excerpts.txt[A]): both inputs reach :1020; console showsdevice="" label="DATA"and the mount then fails ENOENT viahammer2_init_devvp(""). - Instrumented kernel #1 (
console_excerpts.txt[B]): *volume=""βlabel==devstr=1 devstr[-1]=0x00*volume="@"βlabel==devstr=1 devstr[-1]=0xffThe 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). - 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
fixedfix.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']
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.
No comments yet.