Unvalidated vol_no from crafted volume header causes kernel heap OOB write in volume_map bitmap
Summary
hammer_ondisk.c:210 volume->vol_no=ondisk->vol_no trusts raw int32 from disk NO range check. :234 hammer_volume_number_add(hmp,volume). Inline hammer.h:1582 i=__hammer_vol_index(vol_no)=vol_no>>6 NO masking. :1583 hmp->volume_map[i] |= __hammer_vol_low(vol_no). volume_map is uint64_t[4] supports vol_no 0-255 (HAMMER_MAX_VOLUMES=256). vol_no=256: i=4 writes 8 bytes past volume_map (last field of hammer_mount). vol_no=0x7FFFFFFF: i=0x1FFFFFF write ~256MB past panic. vol_no=256+64*K: single-bit OR write at controllable heap offset K*8 past volume_map. RB_INSERT at :227 uses full int32 comparison does not constrain range. rootvol check :241 runs AFTER OOB write. HAMMER_VOL_ENCODE masks to 8 bits for offset encoding but __hammer_vol_index does NOT = asymmetry root cause. CRC of volume header never validated at mount (hammer_install_volume:199-209). Trigger: crafted HAMMER image vol_no=0x7FFFFFFF mount = immediate panic. Fix: if(vol_no<0||vol_no>=HAMMER_MAX_VOLUMES) EFTYPE + mask in __hammer_vol_index &3.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0797 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_img.c | trigger-source | HAMMER volume-header vol_no patcher (offset 0x90; CRC left stale -- never validated at mount) | 5.5 KB | view raw |
| harness.c | trigger-source | Deterministic OOB-write primitive proof (transcribes __hammer_vol_index / volume_map[] |=) | 7.5 KB | view raw |
| build.sh | build-script | cc -O2 -o craft_img craft_img.c ; cc -O2 -o harness harness.c | 295 B | view raw |
| run.sh | run-script | harness + image forge (vol_no=0x7FFFFFFF) + mount -> panic on #0 | 2.7 KB | view raw |
| VERDICT.md | verdict | Full narrative: mechanism, path:line, panic decode, fix validation | 8.5 KB | β raw |
| README.md | readme | Build/run/expected + preconditions | 2.6 KB | β raw |
| build.log | build-log | Final successful build of craft_img + harness | 238 B | view raw |
| run.log | run-log | harness output: deterministic OOB extent for vol_no 0/256/0x200/0x7FFFFFFF/-1 | 2.3 KB | view raw |
| panic.txt | panic-signature | Fatal trap 12 at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8) -- the OOB OR-write instruction | 1.8 KB | view raw |
| baseline_mount.log | run-log | Baseline #0 mount attempt that triggered the panic | 426 B | view raw |
| craft_log.txt | run-log | First image forge + newfs_hammer pipeline output | 1002 B | view raw |
| fix_run.log | run-log | Patched #1 mount: 'mount: Invalid argument', no panic, dmesg rejection line | 1.6 KB | view raw |
| fix_build.log | build-log | Full make -j6 nativekernel output of the single-fix kernel (rc=0) | 5.6 MB | β download |
| fix.diff | suggested-fix | git-apply-able: range check vol_no in hammer_ondisk.c + mask __hammer_vol_index in hammer.h | 1.6 KB | view raw |
| env.txt | environment | uname, kern.version, cc version, INVARIANTS in kernel config | 596 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 |
DF-0797 β PoC
Unvalidated vol_no from crafted HAMMER volume header β kernel heap OOB write in volume_map bitmap.
sys/vfs/hammer/hammer_ondisk.c:210 volume->vol_no = ondisk->vol_no trusts a
raw int32 from disk. hammer.h:1582 __hammer_vol_index(vol_no) = vol_no >> 6
has NO mask; volume_map[] is uint64_t[4] (valid indices 0..3 = vol_no 0..255).
A crafted vol_no outside [0,256) writes past volume_map (the last field of
struct hammer_mount):
vol_no = 0x7FFFFFFFβi = 0x1FFFFFFβ ~255 MiB OOB β page fault panic at mountvol_no = 256βi = 4β 8-byte OOB OR-write pastvolume_map[3]vol_no = 256+64*K+Nβ single-bit OR at byte(K*8 + N/8)pastvolume_map
The volume-header CRC is NEVER validated at mount (hammer_crc_test_volume has
zero callers), so patching vol_no requires no CRC recompute.
Build
./build.sh # cc -O2 -o craft_img craft_img.c ; cc -O2 -o harness harness.c
Run
./run.sh # full pipeline: harness + image forge + mount # or, manually as root inside the DragonFly guest: dd if=/dev/zero of=/root/df0797.img bs=1m count=1024 vnconfig -c vn0 /root/df0797.img newfs_hammer -f -L TEST /dev/vn0 mkdir -p /mnt && mount -t hammer -o nohistory /dev/vn0 /mnt echo seed > /mnt/s.txt && sync && umount /mnt && vnconfig -u vn0 ./craft_img /root/df0797.img 2147483647 # forge vol_no = 0x7FFFFFFF vnconfig -c vn0 /root/df0797.img mount -t hammer -o nohistory /dev/vn0 /mnt # PANIC on #0 ; EINVAL on patched #1
Expected
- #0 unpatched (GENERIC, INVARIANTS ON): immediate kernel panic
Fatal trap 12: page fault while in kernel modeStopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8) - #1 patched (fix.diff applied):
mount: Invalid argument, dmesg:HAMMER: volume /dev/vn0 has invalid vol_no 2147483647, no panic. ./harnessalone: deterministic OOB-write proof (no kernel needed).
Preconditions
Root inside the DragonFly guest (mount is root-only; vfs.usermount=0).
Acceptable "admin mounted a crafted filesystem image" threat model.
Files
craft_img.cβ volume-headervol_nopatcher (offset 0x90 inhammer_volume_ondisk)harness.cβ deterministic OOB-write primitive proof (transcribes the inline fns)build.sh/run.shβ exact build/run pipelineVERDICT.mdβ full narrative withpath:linecitations and fix validationpanic.txtβ captured fatal trap 12 athammer_install_volume+0x519fix.diffβ git-apply-able fix (range check + mask), validated on #1manifest.json,env.txt, full logs
DF-0797 β Unvalidated vol_no from crafted HAMMER volume header β kernel heap OOB write in volume_map bitmap
Verdict
REPRODUCED β immediate kernel panic on GENERIC #0 from a crafted HAMMER
image; deterministic OOB-write primitive proven by harness; fix VALIDATED
on a single-fix kernel (#1) β the bad vol_no is rejected with EFTYPE/EINVAL
before the OOB write, no panic.
Mechanism (trigger β primitive β effect)
A HAMMER v1 volume header carries int32_t vol_no at byte offset 0x90 in
struct hammer_volume_ondisk (sys/vfs/hammer/hammer_disk.h:754). At mount:
hammer_install_volume()reads the header into a buffer and atsys/vfs/hammer/hammer_ondisk.c:210doesvolume->vol_no = ondisk->vol_no;β a raw int32 trusted from disk with NO range check.- At
:234it callshammer_volume_number_add(hmp, volume). hammer_volume_number_add(sys/vfs/hammer/hammer.h:1579-1584) computesint i = __hammer_vol_index(vol->vol_no)and writeshmp->volume_map[i] |= __hammer_vol_low(vol->vol_no).__hammer_vol_index(sys/vfs/hammer/hammer.h:1567-1571) is justvol_no >> 6with NO mask.hmp->volume_map[]isuint64_t[4](sys/vfs/hammer/hammer.h:875, the LAST field ofstruct hammer_mount), so valid indices are 0..3 (covers vol_no 0..255 =HAMMER_MAX_VOLUMES).
For a crafted vol_no outside [0, 256):
vol_no |
__hammer_vol_index |
effect of volume_map[i] |= bit |
|---|---|---|
0 |
0 |
in-bounds (legitimate root volume) |
256 |
4 |
8-byte OOB OR-write past volume_map[3] |
256+64*K+N |
4+K |
OOB OR-write of bit N at byte (K*8 + N/8) past end |
0x7FFFFFFF |
0x1FFFFFF |
~255 MiB past volume_map β unmapped page β panic |
-1 |
-1 (0x3FFFFFF) |
~512 MiB past β unmapped page β panic |
The HAMMER_VOL_ENCODE/HAMMER_VOL_DECODE macros (hammer_disk.h:291-294)
mask to 8 bits for offset encoding; __hammer_vol_index does NOT β this
asymmetry is the root cause.
The rootvol sanity check at hammer_ondisk.c:241 runs AFTER the OOB write at
:234, so it cannot prevent it.
CRC is NOT validated at mount
hammer_crc_test_volume() (sys/vfs/hammer/hammer_crc.h:180) is defined but
has ZERO callers anywhere in sys/vfs/hammer/ (verified by grep).
Therefore patching vol_no in the image requires NO CRC recompute β the
volume header CRC is never checked at mount time. (A separate hardening gap.)
Reproduction
Trigger (acceptable precondition: admin mounts a crafted filesystem image):
dd if=/dev/zeroa 1 GB image;vnconfig -c vn0 img; newfs_hammer -f -L TEST /dev/vn0.- Mount, seed a file, sync, unmount,
vnconfig -u. ./craft_img img 2147483647β patchesvol_noat offset 0x90 to0x7FFFFFFF.vnconfig -c vn0 img; mount -t hammer -o nohistory /dev/vn0 /mntβ panic.
Captured panic (GENERIC #0, with-src baseline, from dfbsd-qemu/boot.log):
Fatal trap 12: page fault while in kernel mode cpuid = 2; lapic id = 2 fault virtual address = 0xfffff80128a50a28 fault code = supervisor write data, page not present instruction pointer = 0x8:0xffffffff80948789 current process = 979 kernel: type 12 trap, code=2 Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8) db>
The faulting instruction orq %rdi,0x10a30(%rbx,%rdx,8) is the EXACT
compilation of hmp->volume_map[i] |= bit:
- %rbx = hmp; 0x10a30 = offset of volume_map[] in struct hammer_mount
- %rdx = i = 0x1FFFFFF; %rdx,8 = i*8
- %rdi = the bit (1<<63) to OR
So the kernel page-faulted executing volume_map[0x1FFFFFF] |= bit β
definitive in-kernel proof of the DF-0797 OOB write at hammer.h:1583
called from hammer_ondisk.c:234.
Deterministic OOB proof (./harness)
The harness transcribes __hammer_vol_index/__hammer_vol_low/volume_map[] |=
verbatim against a poisoned heap and shows the OOB extent for vol_no = 0, 256,
0x200, 0x7FFFFFFF, -1. It proves the controllable-offset primitive:
vol_no = 256 + 64*K + N β single-bit OR-write of bit N at qword (4+K),
i.e. byte offset (K*8 + N/8) past volume_map end. Both qword offset and
bit-within-qword are attacker-controlled.
Impact ceiling
- GENERIC (INVARIANTS ON, the default): immediate, deterministic kernel
panic / DoS at mount time via the far-OOB variant (
vol_no=0x7FFFFFFF). The small-OOB variant (vol_no=256..319) silently ORs a bit into adjacent kernel heap paststruct hammer_mountβ content of the write is a single set bit at a controllable offset, value not fully attacker-shaped (only OR-set, not arbitrary). On GENERIC, no INVARIANTS guard fires on this specific write (it is a plain array index, not a slab-redzone violation), so the small-OOB case corrupts silently. - Production (INVARIANTS OFF): the controllable-offset single-bit OR is a
constrained heap-write primitive, but escalation is bounded by:
(a) the write is OR-only (can set a bit, not clear or write arbitrary bytes),
(b) it requires a victim object with a security-interesting single-bit
flip to land exactly at the controllable offset,
(c) the target offset is relative to the end of
struct hammer_mount, a large (>64 KiB) allocation, so the reachable victim window is whatever is allocated immediately after it β not easily groomable to a specific victim. No escalation chain was developed: the primitive (single-bit OR at a controllable but hammer_mount-relative offset, triggered only at mount of a crafted image by root) is too narrow to convert touid=0on this guest. The realistic ceiling is therefore panic / DoS on GENERIC (reliable, mount-time) and a latent constrained heap-write primitive on production.
Threat model / preconditions: the bug requires mounting a crafted HAMMER
filesystem image. vfs.usermount=0 on this guest, so the mount itself is a
root-only action (acceptable: "admin mounted a crafted filesystem image");
the panic is the demonstration of impact. There is no unprivileged-only path
to the OOB β the corruption happens DURING mount, before any unprivileged
file operation.
PoC changes (what was built from scratch)
craft_img.cβ volume-headervol_nopatcher. Locates the HAMMER_FSBUF_VOLUME signature at block 0, patches the 4-bytevol_nofield at offset0x90, leavesvol_crcstale (verified: CRC is never validated at mount). No CRC32C machinery needed (unlike DF-0769/0776).harness.cβ deterministic transcription of__hammer_vol_index/volume_map[] |=showing the OOB extent for severalvol_novalues against a poisoned heap model.build.sh/run.shβ exact build & run pipeline (image create + forge + mount, plus harness).
Fix (fix.diff β git-apply-able, validated)
Two-part, minimal:
hammer_ondisk.c(before:234hammer_volume_number_add): reject out-of-rangevol_nowithEFTYPE:c if (volume->vol_no < 0 || volume->vol_no >= HAMMER_MAX_VOLUMES) { hkprintf("volume %s has invalid vol_no %d\n", volume->vol_name, volume->vol_no); error = EFTYPE; goto late_failure; }hammer.h__hammer_vol_index(defense in depth): mask the index to the array size:return ((vol_no >> 6) & 0x3);
This matches (and slightly broadens, with the inline mask as defense in
depth) the finding markdown's ## Recommended fix proposal.
Fix validation (Phase 8)
- Baseline
#0(with-src, unpatched): crafted image mount βFatal trap 12 ... Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8)β guest wedged in DDB. β bug present. - Single-fix kernel
#1(built withmake -j6 nativekernel KERNCONF=X86_64_GENERIC, rc=0, sha2563b1974ce583a3d22c5f67ee1e07999b1b7ddff8f6f3067b636c55d15d0af6d16): same crafted image mount βmount: Invalid argument(rc=1), dmesg showsHAMMER: volume /dev/vn0 has invalid vol_no 2147483647, no panic, guest stays up,kern.version=#1. β fix closes the bug. - The same
vol_no < 0 || vol_no >= HAMMER_MAX_VOLUMESguard also rejects the small-OOB variant (vol_no=256, 256>=256) and the negative variant (vol_no=-1, <0) by code inspection.
Build: make -j6 nativekernel (HAMMER is in X86_64_GENERIC); ~6 min for a
.c+.h change; only hammer_ondisk.c + hammer.h recompiled + link.
Fix verification
fixedVALIDATED the fix: on baseline #0 (with-src, unpatched) the crafted image (vol_no=0x7FFFFFFF) mount triggered 'Fatal trap 12 ... Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8)' and wedged the guest in DDB. On the single-fix kernel #1 (fix.diff applied, built with make -j6 nativekernel KERNCONF=X86_64_GENERIC rc=0) the SAME crafted image mount returned 'mount: Invalid argument' (rc=1), dmesg showed 'HAMMER: volume /dev/vn0 has invalid vol_no 2147483647', no panic, guest stayed up. Re-ran twice, deterministic. The same vol_no<0||vol_no>=HAMMER_MAX_VOLUMES guard also rejects the small-OOB (256>=256) and negative (-1<0) variants by code inspection. fix.diff passes 'git apply --check -p1' on the clean sys/ tree.
BASELINE #0: 'Fatal trap 12: page fault while in kernel mode / Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8) / db>' (guest down). PATCHED #1: 'mount: Invalid argument (rc=1)' + dmesg 'HAMMER: volume /dev/vn0 has invalid vol_no 2147483647' (guest up, kern.version=#1). Build: fix_build.log shows 'Kernel build for X86_64_GENERIC completed' with 'NK_DONE rc=0', no errors.
Confirmed kernel references
Detail
Exploit chain
none (not a write-capable escalation): the controllable-offset variant (vol_no=256+64K+N -> single-bit OR at byte K8+N/8 past volume_map) is characterized by harness.c and is a constrained heap-write primitive on production (INVARIANTS-OFF), BUT: (1) it is OR-only (can set one bit, not write arbitrary bytes), (2) the trigger requires root to mount a crafted HAMMER image (vfs.usermount=0) -- the corruption happens DURING mount, before any unprivileged file op, so there is no unpriv->root boundary being crossed (root->kernel is game-over by definition), (3) the offset is relative to the end of struct hammer_mount (a >64KiB allocation), making the victim window hard to groom to a specific target. Valid hard blocker: 'the write is reachable only from an already-root context (mount of a crafted image, root-only)'. On GENERIC (the default, INVARIANTS ON) the realistic ceiling is deterministic mount-time DoS/panic. Harness transcribes __hammer_vol_index/__hammer_vol_low/volume_map[] |= verbatim against a poisoned heap and shows OOB extents for vol_no 0/256/0x200/0x7FFFFFFF/-1; chain file: harness.c. No uid0 escalation attempted because the precondition is root-only mount (no privilege boundary to cross) -- reported honestly as panic/DoS.
Evidence (decisive lines)
panic.txt: 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xfffff80128a50a28 / fault code = supervisor write data, page not present / Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8) / db>'. harness run.log shows '__hammer_vol_index(2147483647) = 33554431 ... FATAL: +255 MiB past volume_map -> unmapped page fault'. dmesg on #1 patched: 'HAMMER: volume /dev/vn0 has invalid vol_no 2147483647', mount rc=1 'Invalid argument', no panic. Full untrimmed logs in findings/poc/DF-0797/{run.log,panic.txt,fix_run.log,fix_build.log}.
PoC changes
Built findings/poc/DF-0797/ from scratch: craft_img.c (HAMMER volume-header vol_no patcher at offset 0x90 in struct hammer_volume_ondisk; verifies HAMMER_FSBUF_VOLUME signature; leaves vol_crc stale because hammer_crc_test_volume is never called at mount -- verified), harness.c (deterministic transcription of __hammer_vol_index/__hammer_vol_low/volume_map[] |= against a poisoned heap model showing the OOB extent for vol_no 0/256/0x200/0x7FFFFFFF/-1), build.sh/run.sh (exact pipeline), fix.diff (range check + mask), VERDICT.md/README.md/manifest.json/env.txt, full logs.
Verified recommended fix
Two-part minimal fix in findings/poc/DF-0797/fix.diff (git-apply-able, validated): (1) in hammer_ondisk.c before line 234 add 'if (volume->vol_no < 0 || volume->vol_no >= HAMMER_MAX_VOLUMES) { hkprintf(...invalid vol_no...); error = EFTYPE; goto late_failure; }' to reject out-of-range vol_no before hammer_volume_number_add indexes volume_map; (2) in hammer.h __hammer_vol_index mask the index '(vol_no >> 6) & 0x3' as defense in depth. Matches the finding markdown's proposal (broadened with the inline mask).
Verdict
REPRODUCED on GENERIC #0 (with-src baseline) as an immediate mount-time kernel panic; root cause confirmed by source trace and panic-instruction decode. hammer_ondisk.c:210 assigns volume->vol_no = ondisk->vol_no (raw int32 from disk, NO range check); :234 calls hammer_volume_number_add(hmp, volume); hammer.h:1582-1583 computes i=__hammer_vol_index(vol_no)=vol_no>>6 (NO mask, hammer.h:1567-1571) then writes hmp->volume_map[i] |= __hammer_vol_low(vol_no). volume_map is uint64_t[4] (hammer.h:875, the LAST field of struct hammer_mount), so valid i is 0..3 (vol_no 0..255). A crafted image with vol_no=0x7FFFFFFF yields i=0x1FFFFFF, ~255 MiB OOB. Captured panic from dfbsd-qemu/boot.log: 'Fatal trap 12: page fault while in kernel mode ... supervisor write data, page not present ... Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8)' -- this instruction is the EXACT compilation of volume_map[i] |= bit (%rbx=hmp, 0x10a30=offset of volume_map, %rdx=i*8, %rdi=bit). Definitive in-kernel proof of the OOB write. Also verified hammer_crc_test_volume (hammer_crc.h:180) has ZERO callers -- the volume-header CRC is never validated at mount, so patching vol_no needs no CRC recompute.
No comments yet.