Unconditional memset on FREEBLKS bios writes to unmapped KVA causing kernel panic
Summary
dm_target_zero_strategy() calls memset(bp->b_data 0 bp->b_bcount) for ALL bio commands including BUF_CMD_FREEBLKS. For FREEBLKS buffer from getnewbuf() has b_data pointing to valid-but-unmapped KVA (pmap entries stripped by allocbuf(bp,0) during buffer recycling b_data reset to b_kvabase vfs_bio.c:2196 no pages mapped). memset writes b_bcount bytes to unmapped KVA causing fatal kernel page fault panic. Reachable when FFS mounted with MNT_TRIM on dm-zero device: any unprivileged user deleting file triggers ffs_blkfree() which issues FREEBLKS bio reaching zero target unchanged. Call chain: ffs_blkfree() getnewbuf(0 0 0 1) sets b_bcount=size no KVA remap vn_strategy devfs_spec_strategy (FREEBLKS shortcut b_cmd!=READ&&b_cmd!=WRITE) dev_dstrategy_chain dmstrategy non-bypass nestiobuf_add copies dangling b_data dm_target_zero_strategy memset panic. Linux dm-zero only calls zero_fill_bio for REQ_OP_READ confirming correct behavior. Attacker: any unprivileged local user with write+delete on FFS filesystem mounted trim on dm-zero. Impact: immediate kernel panic system-wide DoS. No privilege escalation or info leak.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2215 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | libprop PoC: creates dm-zero, shows mount -o trim fails (D_CANFREE absent -> FREEBLKS unreachable) | 8.4 KB | view raw |
| build.sh | build-script | cc -O2 -o poc poc.c -lprop | 89 B | view raw |
| run.sh | run-script | kldload dm; ./poc | 236 B | view raw |
| build.log | build-log | successful build output | 86 B | view raw |
| run.log | run-log | PoC run showing mount -o trim failure | 1.4 KB | view raw |
| fix_run.log | fix-run-log | fixed dm.ko: same behavior (path still unreachable, fix is defense-in-depth) | 603 B | view raw |
| fix.diff | suggested-fix | guard memset against non-READ/WRITE bios in dm_target_zero_strategy | 728 B | view raw |
| VERDICT.md | verdict | full analysis: latent bug, unreachability proof, defense-in-depth fix | 2.9 KB | β raw |
| README.md | readme | build/run instructions | 974 B | β raw |
| env.txt | environment | uname, cc version, dm_ops flags | 430 B | view raw |
DF-2215: dm_target_zero FREEBLKS memset (LATENT)
Build & Run
Setup (as root)
kldload dm cc -O2 -o poc poc.c -lprop
Run (as root β control dev is 0640 root:operator)
./poc
Expected Output
The PoC creates a dm-zero device, then attempts newfs + mount -o trim.
The mount fails:
Device:/dev/mapper/df2215dev does not support the TRIM command
This proves the FREEBLKS path is unreachable: dm_ops lacks D_CANFREE,
so the dm device never claims TRIM support. Without MNT_TRIM,
ffs_blkfree never generates FREEBLKS bios, and VOP_FREEBLKS returns
early for dm devices.
The code-level bug IS real: dm_target_zero_strategy (dm_target_zero.c:43)
calls memset(bp->b_data, ...) unconditionally β if a FREEBLKS bio ever
reached this function, bp->b_data would be NULL β panic. But on this kernel,
FREEBLKS bios never reach a dm device. This is a latent / defense-in-depth
hardening gap. See fix.diff for the guard.
DF-2215 VERDICT β dm_target_zero unconditional memset on FREEBLKS bios
Verdict: LATENT / UNREACHABLE on this kernel (code-level bug is REAL)
Summary
dm_target_zero_strategy (sys/dev/disk/dm/dm_target_zero.c:43) unconditionally
calls memset(bp->b_data, 0, bp->b_bcount) for every bio, including
BUF_CMD_FREEBLKS (discard/TRIM). For a FREEBLKS bio, bp->b_data is NULL
(there is no data buffer for a discard operation β only an offset and length).
This would cause a NULL-deref page fault.
However, on this kernel the FREEBLKS path through dm is unreachable:
dm_ops (device-mapper.c:73) does not set D_CANFREE, so the dm device never
claims TRIM support. Both paths that generate FREEBLKS bios to the dm device
are blocked:
- VOP_FREEBLKS β
devfs_spec_freeblks(devfs_vnops.c:2036): checksSI_CANFREE(propagated fromD_CANFREE) and returns early for dm devices. - ffs_blkfree TRIM path (ffs_alloc.c:1673): only taken when
MNT_TRIMis set, butmount -o trimFAILS for dm devices:"Device:/dev/mapper/xxx does not support the TRIM command".
The code-level bug is REAL but LATENT. It would become reachable if a future
commit adds D_CANFREE to dm_ops or if a dm target propagates FREEBLKS
bios from a TRIM-capable backing device.
Mechanism (why the code IS buggy)
dm_target_zero_strategy(dm_table_entry_t *table_en, struct buf *bp)
{
memset(bp->b_data, 0, bp->b_bcount); // <-- NULL deref for FREEBLKS
bp->b_resid = 0;
biodone(&bp->b_bio1);
return 0;
}
If a BUF_CMD_FREEBLKS bio reached this function:
- The master buf from getpbuf/getnewbuf has b_data == NULL
- nestiobuf_add (vfs_bio.c:4581) propagates: bp->b_data = mbp->b_data + offset = NULL + offset
- memset(NULL+offset, 0, size) β page fault β kernel panic
dmstrategy (device-mapper.c:385) explicitly routes FREEBLKS through the table strategy (it is NOT bypass), so if FREEBLKS were ever generated for a dm device, it WOULD reach the zero target.
Proof of unreachability (from PoC run)
Device:/dev/mapper/df2215dev does not support the TRIM command
This is because dm_ops (device-mapper.c:73) has flags D_DISK | D_MPSAFE
but NOT D_CANFREE.
Exploit chain
N/A β latent bug, unreachable on this kernel. No panic, no escalation.
Fix (defense-in-depth)
Guard the memset in dm_target_zero_strategy against non-READ/WRITE bios.
FREEBLKS bios should complete as no-ops (the zero target has no backing store
to discard). See fix.diff.
Fix validation
- fix_status: not_testable β the FREEBLKS path is unreachable on this kernel (dm_ops lacks D_CANFREE), so there is no runtime behavior to compare.
- Validated: fix.diff applies cleanly, compiles without errors (
-Werror), dm.ko installs and loads, dm device operations (create/reload/resume) all work normally. The guard does not affect normal READ/WRITE operations.
Fix verification
not_testablenot_testable: FREEBLKS path unreachable on this kernel (dm_ops lacks D_CANFREE), so no runtime behavior to compare. Validated: fix.diff applies cleanly, compiles without errors (-Werror), dm.ko installs and loads, dm device operations (create/reload/resume) all work normally. Guard does not affect normal READ/WRITE operations.
Fix compiles: cc -O2 -pipe ... -Werror ... -c dm_target_zero.c (no errors). dm.ko builds, installs, loads. PoC runs identically on fixed dm.ko: mount -o trim still fails (path unreachable). No runtime regression.
Confirmed kernel references
Detail
Exploit chain
none β non-corruption class (latent/unreachable code path). Bug real at code level but cannot be triggered on this kernel. No panic, no leak, no escalation. memset would write to NULL if a FREEBLKS bio reached the function, but FREEBLKS bios never reach dm devices.
Evidence (decisive lines)
mount -o trim output: 'Device:/dev/mapper/df2215dev does not support the TRIM command' | dm_ops flags at device-mapper.c:73: D_DISK | D_MPSAFE (NO D_CANFREE) | devfs_spec_freeblks (devfs_vnops.c:2036): if ((si_flags & SI_CANFREE) == 0) return 0; | ffs_blkfree (ffs_alloc.c:1673): if (!(mnt_flag & MNT_TRIM)) -> non-TRIM path, no FREEBLKS bio generated.
PoC changes
Replaced trigger.c (dmsetup + mount approach that won't work on zero target) with poc.c using libprop NETBSD_DM_IOCTL. PoC creates dm-zero device, demonstrates unreachability by showing mount -o trim fails (D_CANFREE absent). Authored fix.diff as defense-in-depth: guard memset against non-READ/WRITE bios.
Verified recommended fix
In dm_target_zero.c dm_target_zero_strategy, guard the memset: add 'if (bp->b_cmd != BUF_CMD_READ && bp->b_cmd != BUF_CMD_WRITE) { bp->b_resid = 0; biodone(&bp->b_bio1); return 0; }' before memset. FREEBLKS bios should complete as no-ops (zero target has no backing store to discard). Defense-in-depth β path unreachable on current kernels but would crash if D_CANFREE ever added to dm_ops.
Verdict
NOT REPRODUCED (LATENT / UNREACHABLE). The code-level bug is REAL: dm_target_zero_strategy (dm_target_zero.c:43) calls memset(bp->b_data, 0, bp->b_bcount) unconditionally, including for BUF_CMD_FREEBLKS where bp->b_data is NULL. However, the FREEBLKS path is UNREACHABLE on this kernel because dm_ops (device-mapper.c:73) lacks D_CANFREE. This blocks both FREEBLKS generation paths: (1) VOP_FREEBLKS -> devfs_spec_freeblks checks SI_CANFREE and returns early; (2) mount -o trim fails ('does not support the TRIM command') so MNT_TRIM never set and ffs_blkfree never takes TRIM path. Traced all paths line-by-line. Bug is latent β would become exploitable if a future commit adds D_CANFREE to dm_ops.
No comments yet.