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

u_char length truncation in struct devfs_rule: name[-1] heap over-read and cross-mount rule-clear confusion

Summary

mntpointlen/namlen/linknamlen are u_char but assigned unbounded strlen(): a 256-char name stores namlen 0 so create_link evaluates rule->name[namlen-1] == name[-1] (one-byte heap over-read deciding the wildcard branch; with groomed 0x2a before the chunk it takes a 255-byte-strip path); devfs_rule_clear() matches a 300-char mntpoint rule on a 44-byte prefix (300&0xff==44) and can never clear rules whose length is a multiple of 256. Runtime verdict inconclusive (any 256-char matching pattern needs >=250 stars which first hits DF-3202's stack exhaustion - the session's first double fault was exactly that interaction); source-certain. Fix validated (u_int16_t fields + reject len >= PATH_MAX at ADD).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3200 Β· 9 files
FileTypeDescriptionSize
README.md β€” 142 B ↓ raw
VERDICT.md β€” 2.5 KB ↓ raw
build.sh β€” 99 B view raw
env.txt β€” 415 B view raw
fix.diff β€” 8.6 KB view raw
run.fixed.log β€” 201 B view raw
run.sh β€” 95 B view raw
trunc_namlen.c β€” 3.6 KB view raw
verdict.json β€” 2.8 KB view raw

DF-3200 β€” u_char length truncation in struct devfs_rule. See VERDICT.md. Build: cc -static -O -o trunc_namlen trunc_namlen.c ; Run as root.

VERDICT.md
↓ download raw

DF-3200 β€” u_char length truncation in struct devfs_rule (namlen/mntpointlen/linknamlen)

Root cause

struct devfs_rule (sys/sys/devfs_rules.h:53-59) stores the three string lengths as u_char, but devfs_rule_alloc() (devfs_rules.c:91-124) assigns len = strlen(...) with no bound β€” lengths >= 256 wrap:

  1. rule->namlen == 0 for a 256-char name β†’ devfs_rule_create_link() evaluates rule->name[rule->namlen - 1] == rule->name[-1] (devfs_rules.c:243) β€” a one-byte heap OVER-READ before the kstrdup'd name, and the wildcard branch decision is taken on a byte that is not part of the rule. With groomed heap content (0x2a before the chunk) the wildcard path then strips 255 bytes from a short node name β€” a large over-read feeding the DF-3199 disclosure.
  2. devfs_rule_clear() (devfs_rules.c:200-204) compares mntpointlen == rule1->mntpointlen where the ioctl side is a full size_t strlen and the stored side is the truncated u_char β€” a rule with a 300-char mntpoint (stored 44) is matched by a 44-char clear request whose first 44 bytes agree: rules for one "mount" removed by a clear intended for another (cross-mount confusion).
  3. Rules whose mntpoint length is an exact multiple of 256 store mntpointlen == 0 and can never be matched by CLEAR (clear rejects len==0) β€” permanent rule leak.

Verification status

The truncation itself is arithmetic certainty from source. Runtime isolation of the name[-1] read is entangled with DF-3202: any 256-char NAME pattern that still matches a node needs >= 250 '*'s, which blows the devfs core thread stack in wildCaseCmp before create_link runs β€” the first crash of this audit session (252-star pattern, double fault) is that interaction. Verdict: inconclusive at runtime on the stock kernel; the fix (u_int16_t fields + PATH_MAX bound + star cap) removes the class and is validated as part of the fixed-kernel run.

Fix

fix.diff: u_char -> u_int16_t length fields (kernel-internal struct) plus len >= sizeof(field) rejection in devfs_rule_alloc().

Files

trunc_namlen.c, build.sh, run.sh, fix.diff, verdict.json, manifest.json

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline no→ patch + rebuild →patched clean

fixed kernel stores full lengths (u_int16_t) and rejects the star-heavy 256-char patterns at ADD (EINVAL), so neither name[-1] nor the truncated clear compare is reachable

['DF-3200/run.fixed.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 8 10:25:07 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['VERDICT.md: derivation of name[-1] read and clear confusion', 'DF-3202/panic.txt: the 252-star 256-char pattern double fault (the entanglement)', 'run.fixed.log: pattern rejected at ADD, no OOB path reachable']

PoC changes

initial 255-'a'+'*' pattern matched no node (branch never reached); switched to stars+'null'+stars which matches but collides with DF-3202

Verified recommended fix

u_int16_t length fields in struct devfs_rule + reject len >= PATH_MAX at rule-add

Verdict

The u_char truncation itself is arithmetic certainty from source: devfs_rule_alloc() assigns strlen() into u_char fields (devfs_rules.h:53-59), so a 256-char name stores namlen 0 and devfs_rule_create_link() evaluates rule->name[rule->namlen-1] == rule->name[-1] - a one-byte heap over-read before the kstrdup'd name deciding the wildcard branch; mntpointlen truncation likewise makes devfs_rule_clear() remove rules for 256+-char mntoints on a 44-byte prefix match and makes len%256==0 rules unclearable. Runtime isolation was not possible on the stock kernel: any 256-char NAME pattern that still matches a node requires >=250 '*'s, which first exhausts the stack in wildCaseCmp (DF-3202) - the first double fault of this session (252-star pattern) is exactly that interaction. Hence inconclusive at runtime, certain from source; the fix removes the class and the fixed kernel rejects the pattern family at rule-add.