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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2452 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md verdict full mechanism walkthrough + Phase 8 fix validation
↓ download 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) allocates argv with kmalloc(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) where argc - 4 is the number of feature args actually present.
  • _init_features immediately overwrites its argc parameter 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 while loop reads *argv++ past the populated slots into the NULL tail. The next strcmp(arg, "drop_writes") (or strcmp(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

  1. Module load: reaching the dm ioctl requires kldload dm, root-only.
  2. Device node: /dev/mapper/control is 0640 root:operator (device-mapper.c:181). The maxx user (uid 1001) gets EACCES.
  3. 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)

  1. Baseline (with-src snapshot, kernel 6.5-DEVELOPMENT #0, unpatched dm_target_flakey.ko): PoC panics deterministically β€” Fatal trap 12, fault VA 0x0, Stopped at strcmp+0xc. Guest in DDB.
  2. Patched: applied fix.diff to /usr/src/sys/dev/disk/dm/flakey/dm_target_flakey.c, built the module alone (make in sys/dev/disk/dm/flakey), installed dm_target_flakey.ko β†’ /boot/kernel/dm_target_flakey.ko, reloaded.
  3. Re-run: same PoC returns EINVAL (errno 22) β€” reload ioctl returned rv=22 (Invalid argument). Guest stays up.
  4. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (same kernel, module-only rebuild of dm_target_flakey.ko)

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.