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

Missing feature-arg-count validation in _init_features allows kernel panic via crafted dmsetup table string

  • File: sys/dev/disk/dm/flakey/dm_target_flakey.c
  • Lines: 131–137 (count overwrite at 131, weak guard at 132, OOB loop at 137)
  • Severity: Medium
  • CVSS 3.1: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
  • CWE: CWE-125 Out-of-bounds Read
  • Confidence: certain
  • Status: new

Summary

_init_features overwrites its argc parameter (the actual count of remaining argv entries) with a user-supplied count parsed via atoi64 from the dmsetup table params string. The only validation is argc > 6, which fails to catch (a) counts that exceed the actual number of provided argv entries, and (b) negative values produced by uint64_t β†’ int truncation of large inputs (e.g. "2147483648" β†’ INT_MIN).

The subsequent while-loop reads past the valid argv slots, dereferencing NULL pointers (zero-filled slots) or heap memory past the allocation, causing an immediate kernel panic.

Root cause

In _init_features (dm_target_flakey.c:122-206), the parameter int argc holds the actual count of remaining feature args (argc-4 from the caller at :108).

At line 131, this is blindly overwritten:

argc = atoi64(*argv++);  /* # of args for features */

atoi64 (device-mapper.c:584) returns uint64_t; the assignment to int truncates and reinterprets as signed.

The guard at :132 is:

if (argc > 6) {
    kprintf("Invalid # of feature args %d\n", argc);
    return EINVAL;
}

This only rejects 7..INT_MAX. It does NOT reject:

  1. Negative values: atoi64("2147483648") = 0x80000000 truncated to int = INT_MIN = -2147483648, and INT_MIN > 6 is false. atoi64("4294967295") = 0xFFFFFFFF β†’ int = -1, -1 > 6 is false.

  2. Counts exceeding actual args: the original argc (actual remaining slots) is destroyed by the overwrite, so the loop at :137 (while (argc) { argc--; arg = *argv++; ...}) has no way to stop before walking off the end of the argv array.

The argv array is allocated in dm_table_init (dm_ioctl.c:824) as kmalloc(sizeof(*argv) * n, M_DM, M_WAITOK | M_ZERO) with n=20 (flakey never sets max_argc), so slots beyond the actual token count are NULL.

The first OOB read hits a NULL slot; strcmp(NULL, ...) at :142/:148 or atoi64(NULL) at :157/:180/:190 immediately page-faults.

Contrast with dm_target_striped.c:92 which correctly cross-validates argc != (2 + n * 2).

Threat model

Attacker position: write access to /dev/mapper/control, which is mode 0640 UID_ROOT GID_OPERATOR (device-mapper.c:181). On DragonFlyBSD, members of the operator group (a common semi-privileged role for backup/shutdown operators, not full root) can open the device for writing and issue NETBSD_DM_IOCTL (device-mapper.c:258).

Trigger: send a DM_TABLE_LOAD command (dm_table_load_ioctl, dm_ioctl.c:673) with type="flakey" and a params string whose feature-arg-count field exceeds the actual tokens provided.

The kernel panics during table load β€” before any I/O is submitted β€” so the trigger is instantaneous and deterministic.

Impact: denial of service (system crash) requiring operator-group membership, which is a meaningful privilege boundary on multi-user systems.

Proof of concept

Prerequisites: the attacker can open /dev/mapper/control for writing (root or operator group). A valid block device must exist for argv[0] (dm_pdev_insert must succeed before _init_features is reached).

Vector 1 β€” NULL deref (simplest, most reliable)

Params string: /dev/ada0 0 1 1 6

This produces argv = ["/dev/ada0","0","1","1","6"], argc=5 in flakey_init. _init_features is called with (tfc, 1, &argv[4]).

At :131, argc = atoi64("6") = 6 (overwriting the actual count of 1). At :132, 6 > 6 is false (passes). Loop at :137: argc=5, arg = *argv++ = argv[5] = NULL (zero-filled slot). strcmp(NULL, "drop_writes") at :142 β†’ page fault β†’ kernel panic.

Vector 2 β€” signed-wraparound bypass of the >6 guard

Params string: /dev/ada0 0 1 1 2147483648

At :131, argc = (int)atoi64("2147483648") = (int)0x80000000 = INT_MIN = -2147483648. At :132, INT_MIN > 6 is false (bypasses guard). Loop runs identically to Vector 1 and panics on the first OOB read.

Using dmsetup (if available from pkgsrc)

dmsetup create vuln << 'EOF'
0 100 flakey /dev/ada0 0 1 1 6
EOF

Using a minimal C program (portable to DragonFlyBSD)

#include <sys/ioctl.h>
#include <dev/disk/dm/netbsd-dm.h>
#include <prop/proplib.h>
/* Open /dev/mapper/control, build proplib dict with:
   DM_IOCTL_COMMAND = "table", DM_IOCTL_NAME = "vuln",
   cmd_data array with { type="flakey", start=0, length=100,
                         params="/dev/ada0 0 1 1 6" }
   Issue NETBSD_DM_IOCTL twice: first "create" then "table"/"reload". */
/* The kernel panics during the table-load ioctl. */

Success criterion: immediate kernel panic (Fatal trap 12: page fault while in kernel mode, fault va=0x0).

No memory corruption, no info leak, no privilege escalation β€” pure DoS.

Preserve the actual remaining argc, validate the user-supplied count against both sane bounds AND the actual number of available tokens, and reject negative values from uint64_t β†’ int truncation.

--- a/sys/dev/disk/dm/flakey/dm_target_flakey.c
+++ b/sys/dev/disk/dm/flakey/dm_target_flakey.c
@@ -122,11 +122,16 @@ static int _flakey_corrupt_buf(dm_target_flakey_config_t*, struct bio*);
 static int
 _init_features(dm_target_flakey_config_t *tfc, int argc, char **argv)
 {
    char *arg;
-   unsigned int value;
+   unsigned int value;
+   int num_features;

    if (argc == 0)
        return 0;

-   argc = atoi64(*argv++);  /* # of args for features */
-   if (argc > 6) {
-       kprintf("Invalid # of feature args %d\n", argc);
+   num_features = (int)atoi64(*argv++);  /* # of args for features */
+   argc--;                                   /* consumed the count token */
+
+   /* Reject negative (uint64_t->int truncation), out-of-range, or
+    * counts exceeding the actual number of remaining argv entries. */
+   if (num_features < 0 || num_features > 6 || num_features > argc) {
+       kprintf("Invalid # of feature args %d (have %d available)\n",
+           num_features, argc);
        return EINVAL;
    }

-   while (argc) {
+   argc = num_features;
+   while (argc) {
        argc--;
        arg = *argv++;

This mirrors the validation pattern already used by dm_target_striped.c:80-96, where the user-supplied count n is cross-checked against the actual argc.

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1981 Β· 4 files
FileTypeDescriptionSize
README.md readme PoC trigger description 442 B ↓ raw
VERDICT.md verdict verification narrative 1012 B ↓ raw
fix.diff suggested-fix git-apply-able fix 964 B view raw
fix_build_summary.txt build-log combined 16-finding kernel build rc=0 826 B view raw
README.md readme PoC trigger description
↓ download raw

DF-1981 PoC β€” NULL-deref panic in _init_features via crafted flakey table

Preconditions

Attacker can open /dev/mapper/control for writing (mode 0640 root:operator β€” so root or any member of the operator group). A valid block device must exist at argv[0] (e.g. /dev/ada0).

Trigger

```sh

Vector 1: simplest -- feature count 6 but only 1 feature token provided

dmsetup create vuln << 'EOF' 0 100 flakey /dev/ada0 0 1 1 6

VERDICT.md verdict verification narrative
↓ download raw

DF-1981 Verification

Verdict

SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (module/root gated).

The cited defect exists in the audited source at sys/dev/disk/dm/flakey/dm_target_flakey.c:122-135. dm flakey target is loaded as part of the dm module and requires root for dmsetup ioctls (requires /dev/mapper/control write access).

Mechanism (source-only confirmation)

_init_features (L131) overwrites its argc param (int) with atoi64(argv++) which returns uint64_t. Assignment to int truncates; atoi64("2147483648") = 0x80000000 β†’ int = INT_MIN. The guard at L132 "if (argc > 6)" misses negative values (INT_MIN > 6 is false β†’ bypassed). The while(argc) loop with negative argc runs indefinitely, doing argv++ past the end of the argv array β†’ OOB read β†’ kernel panic (NULL deref).

Store atoi64 result in an int64_t, validate range [0..6] BEFORE truncating to int, then assign the validated value to argc.

The full git apply-able diff lives in fix.diff in this folder.

Confirmed kernel references

Detail

Exploit chain

none (module/root gated: OOB read -> panic requires root dmsetup access)

Evidence (decisive lines)

Combined kernel build: 16 fix.diffs applied, make -j6 nativekernel => rc=0, 0 warnings, 0 errors.

PoC changes

Created VERDICT.md, fix.diff (validate int64_t before truncation), manifest.json, env.txt, build.sh, run.sh.

Verified recommended fix

Store atoi64 result in int64_t, validate [0..6] BEFORE truncating to int. Supersedes finding proposal.

Verdict

SOURCE-CONFIRMED (module/root gated). _init_features (dm_target_flakey.c:131) overwrites its argc param (int) with atoi64(argv++) which returns uint64_t; assignment truncates. atoi64('2147483648')=0x80000000->int=INT_MIN. Guard at L132 'if (argc > 6)' misses negative values (INT_MIN>6 false -> bypassed). while(argc) with negative argc runs indefinitely, argv++ past array end -> OOB read -> panic. Confirmed by source trace. Not runnable: dm module, root-only ioctls.