NULL-deref kernel panic via mismatched feature_count in _init_features
Summary
_init_features trusts user-supplied feature_count token (argv[4]) without validating against actual number of remaining argv tokens. When feature_count exceeds supplied feature tokens while-loop at :137 walks past last valid token into M_ZERO-initialized (NULL) argv slots. Subsequent strcmp(arg ...) at :142 dereferences NULL pointer causing immediate kernel panic. argc=atoi64(*argv++) reads user-supplied count and discards callers argc. Only validation at :132 is argc>6 does not verify against available tokens. atoi64 returns uint64_t truncated to int so 4294967295 becomes -1 bypassing >6 check. Concrete trigger: params /dev/ada0 0 1 1 2 drop_writes count=2 but only 1 feature token. Iteration 2 reads NULL strcmp(NULL ...) panic. Any user with write access to /dev/mapper/control 0640 root:operator.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2452 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | libprop NETBSD_DM_IOCTL: create + reload flakey with mismatched feature_cnt | 5.2 KB | view raw |
| build.sh | build-script | cc -O2 -o poc poc.c -lprop | 62 B | view raw |
| run.sh | run-script | kldload dm + dm_target_flakey + ./poc (as root) | 184 B | view raw |
| build.log | build-log | PoC compile output (rc=0) | 0 B | β download |
| run.log | run-log | patched-module run: EINVAL (22), no panic | 397 B | view raw |
| fix_run.log | run-log | patched-module run: EINVAL (22), guest stays up | 614 B | view raw |
| fix_build.log | build-log | single-fix dm_target_flakey.ko module build output (rc=0) | 304 B | view raw |
| fix.diff | suggested-fix | validate declared feature_cnt against actual remaining args in _init_features | 635 B | view raw |
| panic.txt | panic-signature | Fatal trap 12 / strcmp+0xc / fault VA 0x0 | 727 B | view raw |
| env.txt | environment | uname, cc, kern.version, dm modules, /dev/mapper/control perms | 424 B | view raw |
| VERDICT.md | verdict | full mechanism walkthrough + Phase 8 fix validation | 5.1 KB | β raw |
DF-2452 β dm_target_flakey NULL-deref via mismatched feature_cnt
Verdict
REPRODUCED (panic / local DoS) + FIX VALIDATED. The bug is real and
deterministically crashes the kernel when a flakey table is reloaded with a
feature_cnt parameter that claims more feature args than actually follow in
the params string. _init_features trusts this count and walks past the
populated argv slots into the M_ZERO'd NULL tail, dereferencing NULL in
strcmp β page-fault panic. The escalation to uid=0 is blocked by a valid
hard blocker: this is a pure NULL-deref (read fault at VA 0x0) with no
write primitive, and the dm control device is root/operator-only. Realistic
impact ceiling: local DoS (root/operator can panic the kernel). The
authored fix.diff is built as a single-fix dm_target_flakey.ko module,
installed, and confirmed to close the bug (panic β clean EINVAL).
Mechanism (trigger β primitive β effect)
_init_features() in sys/dev/disk/dm/flakey/dm_target_flakey.c:
122: static int
123: _init_features(dm_target_flakey_config_t *tfc, int argc, char **argv)
124: {
125: char *arg;
126: unsigned int value;
127:
128: if (argc == 0)
129: return 0;
130:
131: argc = atoi64(*argv++); /* # of args for features */
132: if (argc > 6) { /* upper-bound check ONLY */
133: kprintf("Invalid # of feature args %d\n", argc);
134: return EINVAL;
135: }
136:
137: while (argc) {
138: argc--;
139: arg = *argv++; /* <-- walks past populated slots */
dm_table_init(sys/dev/disk/dm/dm_ioctl.c:824) allocatesargvwithkmalloc(sizeof(*argv) * n, M_DM, M_WAITOK | M_ZERO)and fills only the parsed tokens; the rest stay NULL.dm_target_flakey_init(dm_target_flakey.c:108) calls_init_features(tfc, argc - 4, argv + 4)whereargc - 4is the number of feature args actually present._init_featuresimmediately overwrites itsargcparameter with the count embedded in the FIRST feature arg (atoi64(*argv++)). The original argc parameter is lost β there is no check that the declared count <= the actual remaining args.- When the declared count exceeds the actual remaining args, the
whileloop reads*argv++past the populated slots into the NULL tail. The nextstrcmp(arg, "drop_writes")(orstrcmp(arg, "corrupt_bio_byte")) dereferences NULL β kernel page fault.
Trigger
params: <dev> <offset> <up_int> <down_int> 5 drop_writes
The 5 is the declared feature_cnt, but only ONE feature arg (drop_writes)
follows. After consuming "drop_writes" (iteration 1), iteration 2 reads
argv[6] which is NULL β strcmp(NULL, ...) β panic.
Observed crash signature (boot.log)
Fatal user address access from kernel mode from poc at ffffffff809d75cc Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 fault code = supervisor read data, page not present Stopped at strcmp+0xc: movzbl (%rdi,%rax,1),%edx db>
Fault VA 0x0, stopped at strcmp+0xc with %rdi==0 (NULL) β exactly the
NULL-deref from reading a zeroed argv slot.
Privilege analysis β why this is DoS, not privesc
- Module load: reaching the dm ioctl requires
kldload dm, root-only. - Device node:
/dev/mapper/controlis0640 root:operator(device-mapper.c:181). Themaxxuser (uid 1001) gets EACCES. - The primitive is a read-fault-equivalent NULL deref β no attacker-controlled bytes are written before the trap. There is no corruption to groom, so no escalation chain.
This is the valid hard blocker for Phase 6.
Exploit chain
none β pure NULL-deref panic, no write primitive (valid hard blocker).
Fix (fix.diff)
Save the original argc parameter before overwriting it. After reading the declared feature count, validate it against the actual remaining args:
int avail;
if (argc == 0)
return 0;
avail = argc - 1; /* remaining args after consuming the count */
argc = atoi64(*argv++); /* # of args for features */
if (argc > 6 || argc > avail) {
kprintf("Invalid # of feature args %d (available %d)\n", argc, avail);
return EINVAL;
}
Fix validation (Phase 8)
- Baseline (
with-srcsnapshot, kernel6.5-DEVELOPMENT #0, unpatcheddm_target_flakey.ko): PoC panics deterministically βFatal trap 12, fault VA0x0,Stopped at strcmp+0xc. Guest in DDB. - Patched: applied
fix.diffto/usr/src/sys/dev/disk/dm/flakey/dm_target_flakey.c, built the module alone (makeinsys/dev/disk/dm/flakey), installeddm_target_flakey.koβ/boot/kernel/dm_target_flakey.ko, reloaded. - Re-run: same PoC returns
EINVAL(errno 22) βreload ioctl returned rv=22 (Invalid argument). Guest stays up. - Verdict: fix closes the bug (panic β clean EINVAL).
PoC
poc.c β libprop NETBSD_DM_IOCTL:
1. command="create", name="df2452dev".
2. command="reload" with target type "flakey" and params
/dev/md0 0 10 5 5 drop_writes (feature_cnt=5 but only 1 arg follows).
Build: cc -O2 -o poc poc.c -lprop
Run (as root, after kldload dm; kldload dm_target_flakey): ./poc
Fix verification
fixedVALIDATED: PoC panics deterministically on unpatched dm_target_flakey.ko baseline (Fatal trap 12, fault VA 0x0, Stopped at strcmp+0xc) and does NOT panic on single-fix module (reload returns EINVAL 22, guest stays up). Fix closes the bug.
baseline: Fatal trap 12 / fault VA 0x0 / Stopped at strcmp+0xc / db> (guest dead). patched: reload ioctl returned rv=22 (Invalid argument), guest up.
Confirmed kernel references
Detail
Exploit chain
none β pure NULL-deref panic, no write primitive (valid hard blocker). /dev/mapper/control is root/operator-only (device-mapper.c:181), no unprivileged path. No corruption to groom, no escalation chain possible.
Evidence (decisive lines)
Fatal user address access from kernel mode from poc at ffffffff809d75cc / Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x0 / Stopped at strcmp+0xc: movzbl (%rdi,%rax,1),%edx / db>. AFTER FIX: reload ioctl returned rv=22 (Invalid argument) -- kernel survived.
PoC changes
Created poc.c from scratch using proven libprop NETBSD_DM_IOCTL pattern (reference DF-2448/DF-2449). Trigger: reload a flakey table with params '/dev/md0 0 10 5 5 drop_writes' where feature_cnt=5 but only 1 actual feature arg follows. build.sh, run.sh, VERDICT.md, fix.diff, manifest.json.
Verified recommended fix
In _init_features (dm_target_flakey.c:124), save original argc parameter before overwriting it with declared feature count. After reading count, validate: 'if (argc > 6 || argc > avail)' where avail = original_argc - 1. Prevents loop from walking past populated argv slots into NULL. Supersedes finding proposal (none existed).
Verdict
REPRODUCED. The bug is real and deterministic: _init_features (dm_target_flakey.c:131) reads a feature_cnt from the params string and trusts it without validating against the actual remaining argv slots (allocated M_ZERO in dm_table_init at dm_ioctl.c:824). When feature_cnt exceeds the args present, the while loop at line 137 walks past populated slots into the NULL tail, and strcmp(NULL,...) at line 142/148 dereferences NULL. Confirmed by panic signature: Fatal trap 12, fault VA 0x0, Stopped at strcmp+0xc.
No comments yet.