Heap overflow in $AttrDef name copy via unbounded wchar-to-char do/while walk
Summary
ntfs_vfsops.c:444 kmalloc(num*sizeof(ntvattrdef)=72B each). :458-460 do{ ntmp->ntm_ad[i].ad_name[j]=ad.ad_name[j] }while(ad.ad_name[j++]) NO bound on j. ad_name source is wchar[64] in 160-byte struct attrdef. All 64 wchars + 16 wchars of ad_type/reserved1/ad_flag/ad_minlen/ad_maxlen non-zero => j runs 0..79. j=72..79 writes past ntmp->ntm_ad[i] into ntmp->ntm_ad[i+1].ad_name[0..7] heap OOB write not repaired by post-loop assignments. Crafted NTFS image mount = heap corruption M_NTFSMNT. Slab grooming potential priv-esc.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0871 · 24 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_img.py | trigger-source | NTFS image crafter: evil $AttrDef (all-nonzero 160-byte records) | 11.0 KB | view raw |
| craft_benign.py | trigger-source | control crafter: short NUL-terminated $AttrDef names (no overflow) | 2.3 KB | view raw |
| harness.c | trigger-source | deterministic overflow harness (BUGGY do/while, poisoned allocator) | 7.5 KB | view raw |
| harness_fixed.c | exploit-chain | fixed harness mirroring fix.diff (bounded do/while + NUL-terminate) | 2.5 KB | view raw |
| build.sh | build-script | exact guest build: images + harnesses | 802 B | view raw |
| run.sh | run-script | exact run: harness + live mount churn | 2.6 KB | view raw |
| build.log | build-log | harness build output | 120 B | view raw |
| baseline_harness.log | run-log | baseline #0 harness: SIGSEGV at j=72 | 504 B | view raw |
| harness_run.log | run-log | buggy harness SIGSEGV + cross-entry corruption report | 2.8 KB | view raw |
| harness_compare.log | run-log | buggy-vs-fixed harness before/after | 811 B | view raw |
| mount_attempt.log | run-log | live mount attempts: silent corruption + churn | 688 B | view raw |
| panic.txt | panic-signature | Fatal trap 12 / kqueue_register+0x526 fault 0xffffffffffffffff | 859 B | view raw |
| fix.diff | suggested-fix | git-apply-able: bound do/while to sizeof(ad_name) + NUL-terminate | 593 B | view raw |
| fix_apply.log | fix-log | patch application + patched region verification | 831 B | view raw |
| fix_build.log | fix-build-log | single-fix ntfs.ko build (cc 8.3, rc=0) | 15.8 KB | view raw |
| fix_run.log | fix-run-log | patched module: 30 evil-image mount cycles, 0 panics | 557 B | view raw |
| env.txt | environment | uname, cc version, vfs.usermount, module sha256 | 536 B | view raw |
| ntfs_evil.img | test-image | crafted NTFS image, num=2 evil $AttrDef entries | 512.0 KB | ↓ download |
| ntfs_evil_n1.img | test-image | crafted NTFS image, num=1 evil $AttrDef entry | 512.0 KB | ↓ download |
| ntfs_benign.img | test-image | control NTFS image, well-formed $AttrDef | 512.0 KB | ↓ download |
| README.md | readme | human-readable reproduction guide | 9.2 KB | ↓ raw |
| VERDICT.md | verdict | full narrative: mechanism, evidence, impact, fix validation | 8.1 KB | ↓ raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | ↓ download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0871 — PoC evidence pack
Heap overflow in NTFS $AttrDef name copy via unbounded wchar→char
do/while walk. File: sys/vfs/ntfs/ntfs_vfsops.c:444,458-460.
CWE-787 (OOB write). Severity: High.
The bug (confirmed line-by-line)
ntfs_mountfs() parses the on-disk $AttrDef table (MFT record 4) at mount
time. For every 160-byte struct attrdef it reads, it allocates a 72-byte
struct ntvattrdef and copies the attribute name with an unbounded
do/while:
/* sys/vfs/ntfs/ntfs_vfsops.c */
ntmp->ntm_ad = kmalloc(num * sizeof(struct ntvattrdef), M_NTFSMNT, M_WAITOK); /* :444 */
...
for (i=0;i<num;i++){
...
j = 0; /* :457 */
do {
ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j]; /* :459 DEST is char[0x40]=64 */
} while(ad.ad_name[j++]); /* :460 NO bound on j */
ntmp->ntm_ad[i].ad_namelen = j - 1; /* :461 */
ntmp->ntm_ad[i].ad_type = ad.ad_type; /* :462 */
}
Type facts (sys/vfs/ntfs/ntfs.h):
- typedef u_int16_t wchar; (ntfs.h:37)
- struct attrdef { wchar ad_name[0x40]; u32 ad_type; u32 r1[2]; u32 ad_flag; u64 min; u64 max; } = 160 B (ntfs.h:206)
- struct ntvattrdef { char ad_name[0x40]; int ad_namelen; u32 ad_type; } = 72 B (ntfs.h:215)
The destination ad_name is char[64] but the loop index j is bounded
only by finding a NUL wchar in the source ad.ad_name[64]. If the
on-disk $AttrDef record has no NUL wchar anywhere in its 64-wide-char name
and its trailing 32 B (ad_type/reserved1/ad_flag/ad_minlen/ad_maxlen,
which alias ad.ad_name[64..79]) are also all non-zero half-words, the walk
runs j = 0..80+. The write ntmp->ntm_ad[i].ad_name[j] then:
| j | destination byte | effect |
|---|---|---|
| 0..63 | ntvattrdef[i].ad_name[0..63] |
in-bounds |
| 64..67 | ntvattrdef[i].ad_namelen |
repaired by :461 |
| 68..71 | ntvattrdef[i].ad_type |
repaired by :462 |
| 72.. | ntvattrdef[i+1].ad_name[0..] (cross-entry) or, for the last entry, past the whole kmalloc() |
HEAP OOB WRITE into M_NTFSMNT slab |
The post-loop assignments at :461-462 "repair" the within-object bytes
(ad_namelen/ad_type of entry i) but do not touch the cross-entry /
past-allocation bytes written at j>=72. Those stay corrupted.
Reproduction
A) Deterministic harness (harness.c)
A faithful userspace transcription of :444,457-462 backed by a poisoned
allocator (victim array at the end of a guard-paged page). No slab luck or
INVARIANTS needed — the overflow is detected byte-exactly.
- buggy, num=1:
do/whilewritesdest[j]forj>=72→ SIGSEGV at the first byte past the 72-bytentvattrdefobject. Deterministic proof the write leaves the object. - buggy, num=2: entry[0]'s walk overwrites 72 bytes of entry[1].ad_name (cross-entry corruption, the finding's claimed primitive), then the last entry's overflow crosses the allocation → SIGSEGV.
- fixed (
harness_fixed.c, mirrorsfix.diff): clean exit 0, 0 bytes past the allocation, name length capped at 63 + NUL-terminated.
B) Live kernel (ntfs.ko on #0 GENERIC, INVARIANTS ON)
A crafted NTFS image (craft_img.py, 1–2 evil 160-byte $AttrDef records
whose entire 160 bytes are non-zero half-words + a zero terminator) is mounted
with vnconfig + mount_ntfs (root). Each mount runs the unbounded copy.
During a churn loop the slab gets groomed so the corrupted ntm_ad chunk is
adjacent to a victim object; the 0xFF bytes the evil data writes
(truncated low-bytes of the 0xFFFFFFFF trailing fields) corrupt a victim
pointer to 0xffffffffffffffff, and its later dereference faults:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xffffffffffffffff instruction pointer = 0x8:0xffffffff8063b1c6 current process = 1015 Stopped at kqueue_register+0x526: movq (%r15),%rdx
(panic.txt.) Controlled experiment (causation):
- benign image (short NUL-terminated $AttrDef names → walk stops j<64,
no overflow): 10 mount/unmount cycles, 0 panics, guest UP.
- evil image: panic during churn (above).
The live panic is timing/layout-dependent (heap grooming); the harness is the deterministic proof and reproduces on every run.
Impact / threat model
- Primitive: attacker-content-controlled heap OOB write into the
M_NTFSMNTslab zone (kmalloc-128/kmalloc-256). The written bytes are the low bytes of attacker-chosen on-diskwchars, so content is shapable. - Trigger:
mount -t ntfsof a crafted image. Root-only in the default config (vfs.usermount=0;mount_ntfs/vnconfignot setuid). - Realistic threat: a malicious NTFS image (USB / download / multi-user system / automount) causes kernel heap corruption and/or panic when mounted by root — the classic untrusted-filesystem-image attack surface. High severity (kernel memory corruption from untrusted data).
- uid=0 escalation: the trigger requires root credential, so there is no
unprivileged→root privilege boundary to cross end-to-end (root→kernel is
game-over by definition). This is a valid hard blocker for an
uid0verdict. The primitive could be slab-groomed to escalate if an unprivileged user could mount the image (e.g. an admin setsvfs.usermount=1and provides an attacker-owned, device-accessible image), but that is a conditional scenario, not the default.
Fix (fix.diff)
Bound the do/while to the destination array length and force NUL-termination:
- } while(ad.ad_name[j++]);
+ } while(ad.ad_name[j++] &&
+ j < (int)sizeof(ntmp->ntm_ad[i].ad_name));
+ /* DF-0871: force NUL-termination within the fixed-size buffer
+ * so the wchar->char walk can never overrun ntvattrdef.ad_name. */
+ ntmp->ntm_ad[i].ad_name[sizeof(ntmp->ntm_ad[i].ad_name) - 1] = '\0';
This is a minimal, targeted fix at the root cause. git apply --check passes.
Supersedes/matches the finding markdown's proposal.
Phase 8 — fix validation (single-fix ntfs.ko)
- Applied
fix.diffto in-guest/usr/src/sys/vfs/ntfs/ntfs_vfsops.c(git apply), rebuiltntfs.ko(cd /usr/src/sys/vfs/ntfs && make, rc=0,fix_build.log), hot-swapped (kldunload/kldload, sha256dbc96b62…). - Before (unpatched
#0): buggy harness SIGSEGVs at j=72 (deterministic); live evil-image churn →kqueue_registerpanic (panic.txt). - After (patched
ntfs.ko): fixed harness exit 0, 0 bytes past the allocation (harness_compare.log); 30 evil-image mount cycles (15× num=1 - 15× num=2) → 0 panics, guest UP (
fix_run.log). Benign image still mounts and parses attributes (functional regression check OK).
→ fix_status: fixed.
Files
| file | purpose |
|---|---|
craft_img.py |
NTFS image crafter: evil $AttrDef (all-nonzero) records |
craft_benign.py |
control crafter: short NUL-terminated $AttrDef names |
harness.c |
deterministic overflow harness (BUGGY do/while) |
harness_fixed.c |
same harness mirroring fix.diff (bounded + NUL-term) |
build.sh/run.sh |
exact repro scripts |
build.log |
harness build output |
harness_run.log |
buggy harness SIGSEGV + cross-entry report |
harness_compare.log |
buggy-vs-fixed before/after |
baseline_harness.log |
baseline (#0) harness confirmation |
mount_attempt.log |
live mount attempts (silent corruption + churn) |
panic.txt |
live kernel panic signature (kqueue_register fault) |
fix.diff |
git-apply-able one-hunk fix |
fix_apply.log |
patch application + patched region |
fix_build.log |
single-fix ntfs.ko build output |
fix_run.log |
patched-module live re-test (30 cycles, 0 panics) |
env.txt |
guest uname / cc / sysctls / module sha256 |
manifest.json |
machine-readable catalog |
VERDICT.md |
full narrative |
Reproduce
On the repo root (guest up on with-src):
cd findings/poc/DF-0871
scp -F ../../../dfbsd-qemu/config craft_img.py craft_benign.py harness.c harness_fixed.c dfbsd:/root/df0871/
./../../../dfbsd-qemu/vm.sh run_root 'cd /root/df0871 && python3 craft_img.py ntfs_evil.img 2 && cc -O2 -o harness harness.c && (./harness 2; echo exit=$?)'
# deterministic: SIGSEGV at j=72 + cross-entry corruption reported
DF-0871 — VERDICT
Verdict: REPRODUCED (heap OOB write / corruption, with a live panic on GENERIC).
Impact: corruption → manifests as panic (kqueue_register victim deref).
Confidence: certain.
Fix: VALIDATED (fixed).
Root-cause confirmation
ntfs_mountfs() (sys/vfs/ntfs/ntfs_vfsops.c) parses the on-disk $AttrDef
table at mount time. For each 160-byte struct attrdef it allocates a 72-byte
struct ntvattrdef and copies the attribute name with an unbounded
wchar→char do/while:
sys/vfs/ntfs/ntfs_vfsops.c:444—ntmp->ntm_ad = kmalloc(num * sizeof(struct ntvattrdef), M_NTFSMNT, M_WAITOK);(72 B per entry)sys/vfs/ntfs/ntfs_vfsops.c:458-460—c do { ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j]; /* DEST is char[0x40]=64 */ } while(ad.ad_name[j++]); /* NO bound on j */
Type facts (confirmed in sys/vfs/ntfs/ntfs.h):
- ntfs.h:37 — typedef u_int16_t wchar;
- ntfs.h:203 — #define NTFS_ATTRNAME_MAXLEN 0x40 (= 64)
- ntfs.h:206 — struct attrdef { wchar ad_name[0x40]; u32 ad_type; u32 r1[2]; u32 ad_flag; u64 min; u64 max; } = 160 B
- ntfs.h:215 — struct ntvattrdef { char ad_name[0x40]; int ad_namelen; u32 ad_type; } = 72 B
The index j is bounded only by a NUL wchar in the source ad.ad_name
(which is wchar[64]). If a crafted $AttrDef record has no NUL wchar
anywhere in its 64-wide-char name and its trailing 32 B (ad_type /
reserved1 / ad_flag / ad_minlen / ad_maxlen, aliasing ad.ad_name[64..79])
are all non-zero half-words, the walk runs j = 0..80+. The destination write
ntmp->ntm_ad[i].ad_name[j] then:
j=0..63→ in-bounds name bytesj=64..67→ntvattrdef[i].ad_namelen(repaired by:461)j=68..71→ntvattrdef[i].ad_type(repaired by:462)j=72..→ntvattrdef[i+1].ad_name[0..](cross-entry) or, for the last entry, past the wholekmalloc()→ HEAP OOB WRITE intoM_NTFSMNT.
The post-loop assignments (:461-462) repair the within-object bytes of entry
i but do not touch the bytes written at j>=72. Those stay corrupted,
exactly as the finding claims.
Evidence
A. Deterministic harness (harness.c)
Faithful userspace transcription of :444,457-462 with a poisoned allocator
(victim array at the end of a guard-paged page). The source is presented as an
oversized wchar[256] buffer (all non-zero up to a final NUL) so the do/while
is genuinely unbounded — defeating the compiler's ad_name[64] array-bounds UB
assumption that would otherwise let a naïve transcription silently "pass".
- buggy, num=1: writes
dest[j]forj>=72→ SIGSEGV at the first byte past the 72-byte object. Deterministic proof the write leaves the object. - buggy, num=2: entry[0]'s walk overwrote 72 bytes of entry[1].ad_name (cross-entry corruption — the finding's claimed primitive), then the last entry's overflow crosses the 144-byte allocation → SIGSEGV.
- fixed (
harness_fixed.c, mirrorsfix.diff): exit 0, 0 bytes past the allocation, name length capped at 63 + NUL-terminated.
(harness_run.log, harness_compare.log.)
B. Live kernel (#0 GENERIC, INVARIANTS ON)
Crafted NTFS image (craft_img.py): 1–2 evil 160-byte $AttrDef records whose
entire 160 bytes are non-zero half-words + an all-zero terminator. vnconfig +
mount_ntfs (root) runs the unbounded copy on every mount. During a churn
loop the slab is groomed so the corrupted ntm_ad chunk is adjacent to a
victim object; the 0xFF bytes the evil data writes (truncated low-bytes
of the 0xFFFFFFFF trailing fields) corrupt a victim pointer to
0xffffffffffffffff, whose later dereference faults:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xffffffffffffffff Stopped at kqueue_register+0x526: movq (%r15),%rdx
(panic.txt.)
Controlled experiment (causation):
- benign image (craft_benign.py, short NUL-terminated $AttrDef names →
walk stops j<64, no overflow): 10 mount/unmount cycles, 0 panics,
guest UP.
- evil image: kqueue_register panic during churn (above).
The live panic is timing/layout-dependent (heap-grooming); the harness is the deterministic proof and fires on every run. A single clean mount of the evil image usually produces silent within-allocation/cross-entry corruption (no immediate INVARIANTS trip because the bytes land in slab padding within the same chunk); repeated mounts groom the slab into a configuration where the corrupted chunk borders a dereferenced victim and the kernel faults.
Impact / threat model & escalation ceiling
- Primitive: attacker-content-controlled heap OOB write into the
M_NTFSMNTslab zone (kmalloc-128fornum=1,kmalloc-256fornum=2). The written bytes are the low bytes of attacker-chosen on-diskwchars, so the content is shapable. - Trigger:
mount -t ntfsof a crafted image. Root-only in the default config (vfs.usermount=0;mount_ntfs/vnconfignot setuid — verified inenv.txt). - Realistic threat: a malicious NTFS image (USB / download / multi-user system / automount) causes kernel heap corruption and/or panic when mounted by root — the classic untrusted-filesystem-image attack surface. High severity (kernel memory corruption from untrusted on-disk data).
- uid=0 escalation — BLOCKED by a valid hard blocker: the write is
reachable only from an already-root credential (the
mountsyscall requires root). Per the audit's bright-line rule, root→kernel is game-over by definition; there is no unprivileged→root privilege boundary to cross end-to-end. The primitive could be slab-groomed to escalate if an unprivileged user could mount the image (e.g. an admin setsvfs.usermount=1and provides an attacker-owned, device-accessible image), but that is a conditional scenario, not the default config. Honest impact is therefore heap corruption / DoS (panic) from a malicious filesystem image.
PoC changes (vs. the seeded scaffolding)
This pack was built from scratch (no seeded sources). Reuses the proven
DF-0785 NTFS image builder. Key additions:
- craft_img.py — DF-0871 variant: evil $AttrDef (all-nonzero 160-byte
records) + well-formed root dir ir_size==datalen so only the
mount-time $AttrDef bug is exercised (DF-0785's lookup bug isolated).
- craft_benign.py — negative-control crafter (short NUL-terminated names).
- harness.c / harness_fixed.c — deterministic transcriptions. v2 of the
buggy harness uses an oversized source buffer to defeat the compiler's
array-bounds UB assumption (v1 silently passed at -O2).
Fix (fix.diff) — VALIDATED
Minimal, targeted fix at the root cause — bound the do/while to the destination array length and force NUL-termination:
- } while(ad.ad_name[j++]);
+ } while(ad.ad_name[j++] &&
+ j < (int)sizeof(ntmp->ntm_ad[i].ad_name));
+ /* DF-0871: force NUL-termination within the fixed-size buffer
+ * so the wchar->char walk can never overrun ntvattrdef.ad_name. */
+ ntmp->ntm_ad[i].ad_name[sizeof(ntmp->ntm_ad[i].ad_name) - 1] = '\0';
git apply --check: OK. Supersedes/matches the finding markdown's proposal.
Phase 8 — single-fix ntfs.ko (hot-swap)
git applyoffix.diffto in-guest/usr/src/sys/vfs/ntfs/ntfs_vfsops.c; rebuiltntfs.ko(cd /usr/src/sys/vfs/ntfs && make, cc 8.3, rc=0 —fix_build.log); hot-swapped (kldunload→ install →kldload, sha256dbc96b6262bd579da8a7b050f490047048e6b5ed4f5fd338324d3848788e021d).
| kernel / module | harness | live evil-image mount |
|---|---|---|
unpatched #0 GENERIC |
SIGSEGV at j=72 | panic kqueue_register+0x526 (fault 0xffffffffffffffff) |
fixed ntfs.ko |
clean exit 0, 0 B over | 30 cycles, 0 panics, guest UP |
Benign image still mounts and parses attributes on the fixed module (functional
regression check OK). → fix_status: fixed.
Fix verification
fixedVALIDATED the fix. git apply --check passes; fix.diff applied to in-guest /usr/src/sys/vfs/ntfs/ntfs_vfsops.c, ntfs.ko rebuilt (rc=0) and hot-swapped. BEFORE (unpatched #0 ntfs.ko): deterministic harness SIGSEGVs at j=72 + cross-entry corruption; live crafted-image mount churn -> kernel panic kqueue_register+0x526 (fault 0xffffffffffffffff). AFTER (patched ntfs.ko): harness_fixed exit 0 with 0 bytes past the allocation; 30 evil-image mount cycles (15x num=1 + 15x num=2) -> 0 panics, guest UP; benign image still mounts and parses attributes (functional regression OK). The fix closes the bug.
BEFORE baseline: harness '[harness] SIGSEGV caught -> do/while wrote PAST the 72-byte allocation.'; live 'Fatal trap 12 ... fault virtual address = 0xffffffffffffffff ... Stopped at kqueue_register+0x526'. AFTER patched module: harness_fixed 'bytes written past the 72-byte allocation = 0 (expect 0)'; live 'PATCHED_CHURN_DONE' after 30 cycles with 0 panics and guest UP (grep -c panic = 0).
Confirmed kernel references
Detail
Exploit chain
Primitive: attacker-content-controlled heap OOB write into M_NTFSMNT slab zone (kmalloc-128 for num=1, kmalloc-256 for num=2); written bytes are the low bytes of attacker-chosen on-disk wchars, so content is shapable. Grooming: repeated vnconfig/mount of the crafted image churns the slab so the corrupted ntm_ad chunk borders a dereferenced victim (observed: a kqueue-related object). Conversion: the corrupted 0xFF-bytes turned a victim pointer into 0xffffffffffffffff, dereferenced in kqueue_register -> fatal page fault (panic). OUTCOME: blocked by a VALID hard blocker -- the trigger is mount -t ntfs, which is ROOT-ONLY in the default config (vfs.usermount=0; mount_ntfs and vnconfig are not setuid -- verified in env.txt). Root->kernel is game-over by definition; there is no unprivileged->root privilege boundary to cross end-to-end, so an honest uid0 verdict is not supportable on the default GENERIC. The primitive COULD be slab-groomed to escalate IF an unprivileged user could mount the image (e.g. an admin sets vfs.usermount=1 + provides an attacker-owned, device-accessible image), but that is a conditional scenario, not the default. Demonstrated impact is heap corruption / DoS (kernel panic) from a malicious filesystem image. Harness/chain file written: harness.c + harness_fixed.c (no separate escalation module, since the root-only mount is the blocker).
Evidence (decisive lines)
DETERMINISTIC (harness, every run): '[harness] SIGSEGV caught -> do/while wrote PAST the 72-byte allocation.' and '[harness] cross-entry demo: entry[0] do/while overwrote 72 byte(s) of entry[1].ad_name'. FIXED harness: 'bytes written past the 72-byte allocation = 0'. LIVE (#0 GENERIC): 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xffffffffffffffff / Stopped at kqueue_register+0x526: movq (%r15),%rdx' (panic.txt). CONTROL: benign image = 10 mount cycles, 0 panics, guest UP.
PoC changes
Built the evidence pack from scratch (no seeded sources), reusing the proven DF-0785 NTFS image builder. craft_img.py: DF-0871 variant with evil $AttrDef (all-nonzero 160-byte records) and a WELL-FORMED root dir (ir_size==datalen) to isolate the mount-time bug from DF-0785's lookup bug. craft_benign.py: negative-control crafter (short NUL-terminated names). harness.c/harness_fixed.c: deterministic transcriptions of :444,457-462 with a poisoned allocator; v2 of the buggy harness uses an oversized source buffer to defeat the compiler's ad_name[64] array-bounds UB assumption (v1 silently passed at -O2).
Verified recommended fix
In sys/vfs/ntfs/ntfs_vfsops.c:460, bound the do/while to the destination array length and force NUL-termination: change } while(ad.ad_name[j++]); to } while(ad.ad_name[j++] && j < (int)sizeof(ntmp->ntm_ad[i].ad_name)); and add ntmp->ntm_ad[i].ad_name[sizeof(ntmp->ntm_ad[i].ad_name) - 1] = '\0'; so the wchar->char walk can never overrun the 64-byte ntvattrdef.ad_name. Minimal, targeted at the root cause. Supersedes/matches the finding markdown's proposal. Full git-apply-able diff in findings/poc/DF-0871/fix.diff.
Verdict
REPRODUCED. The bug is real and confirmed line-by-line: ntfs_mountfs() at sys/vfs/ntfs/ntfs_vfsops.c:458-460 copies the $AttrDef attribute name with an unbounded do/while (while(ad.ad_name[j++])) whose destination ntmp->ntm_ad[i].ad_name is only char[0x40]=64 (ntfs.h:215) while the source ad.ad_name is wchar[64]=128B inside a 160-byte struct attrdef (ntfs.h:206, wchar=u_int16_t per ntfs.h:37). A crafted $AttrDef record with no NUL wchar anywhere in its 160 bytes makes j run 0..80+, so writes at j>=72 leave the 72-byte ntvattrdef object (cross-entry into ntvattrdef[i+1].ad_name, or past the whole kmalloc for the last entry) -- a heap OOB write into the M_NTFSMNT slab. The post-loop assignments (:461-462) repair only the within-object bytes (j=64..71), not the j>=72 corruption. Deterministic harness SIGSEGVs at j=72 (poisoned allocator) and shows 72 bytes of cross-entry corruption; live on #0 GENERIC a crafted-image mount churn grooms the slab so the 0xFF bytes (truncated low-bytes of the 0xFFFFFFFF trailing fields) corrupt an adjacent victim pointer to 0xffffffffffffffff, whose dereference panics in kqueue_register+0x526 (panic.txt). Controlled: a benign image (short NUL-terminated names, walk stops j<64) survives 10 mount cycles with 0 panics; the evil image panics.
No comments yet.