[PATCH AUTOSEL 7.0-5.15] ntfs3: reject inodes with zero non-DOS link count

Sasha Levin
2026-04-28T10:42:14Z

From: Ziyi Guo <n7l8m4@u.northwestern.edu>

[ Upstream commit e10e72f69734a90c8719d160e8efb164ce5d9e26 ]

ntfs_read_mft() counts file name attributes into two variables:
names (all names including DOS 8.3) and links (non-DOS names
only). The validation at line 424 checks names but set_nlink()
at line 436 uses links. A corrupted NTFS image where all file
name attributes have type FILE_NAME_DOS passes the names check
but results in set_nlink(inode, 0).

When such an inode is loaded via a code path that passes name=NULL
to ntfs_iget5() and the nlink=0 inode enters the VFS. The subsequent
unlink, rmdir, or rename targeting this inode calls drop_nlink()
which triggers WARN_ON(inode->i_nlink == 0) in fs/inode.c.

An all-DOS-name MFT record cannot exist on a valid NTFS volume.
Reject such records by checking for links == 0 before
calling set_nlink().

Signed-off-by: Ziyi Guo <n7l8m4@u.northwestern.edu>
Signed-off-by: Konstantin Komarov <almaz.alexandrovich@paragon-software.com>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---

LLM Generated explanations, may be completely bogus:

Now I have enough information to write the analysis. Let me finalize my
review.

## Analysis

### PHASE 1: COMMIT MESSAGE FORENSICS

**Step 1.1 — Subject line parse**
Record: subsystem `ntfs3`, verb `reject`, intent: reject inodes with
zero non-DOS link count (validation check).

**Step 1.2 — Tags**
Record: Only two SoBs (Ziyi Guo, Konstantin Komarov). No Fixes:, no Cc:
stable, no Reported-by:, no Link:, no Reviewed-by:. Absence is expected
per the rules.

**Step 1.3 — Body text**
Record: Author explains a precise bug mechanism: `names` counts all file
name attrs (incl. FILE_NAME_DOS) while `links` counts only non-DOS. The
existing guard `if (!names)` can pass while `set_nlink(inode, links)`
still sets nlink=0 when every ATTR_NAME is DOS-type. Subsequent
unlink/rmdir/rename of such an inode hits `drop_nlink()` which fires
`WARN_ON(inode->i_nlink == 0)` in `fs/inode.c`. Trigger:
corrupted/malicious NTFS image; no version info given.

**Step 1.4 — Hidden fix detection**
Record: Explicitly a fix ("reject"). Not hidden.

### PHASE 2: DIFF ANALYSIS

**Step 2.1 — Inventory**
Record: 1 file `fs/ntfs3/inode.c`, +5/-0, one function
`ntfs_read_mft()`. Single-file surgical fix.

**Step 2.2 — Code flow**
Record: Before: after `names` validation, code falls through to
`set_nlink(inode, links)`. After: additional `if (!links) { err =
-EINVAL; goto out; }` short-circuits before `set_nlink` for records
whose only names are DOS 8.3.

**Step 2.3 — Bug mechanism**
Record: Category (g) Logic/correctness + defensive validation for on-
disk corruption. Closes an inconsistency between the validator (`names`)
and the consumer (`links`). Prevents VFS invariant violation
(inode->i_nlink==0 for a living inode), which is what the WARN_ON in
`drop_nlink()` guards.

**Step 2.4 — Quality**
Record: Obviously correct; the branch targets the existing `out:` which
does `iget_failed(inode)` and returns `ERR_PTR(err)`. Adds no lock
changes, no allocation changes. Zero regression risk on valid NTFS
(valid records have `links >= 1`, author states an all-DOS-name MFT
record cannot exist on a valid NTFS volume).

### PHASE 3: GIT HISTORY INVESTIGATION

**Step 3.1 — Blame**
Record: `git blame` on the area around the fix shows `set_nlink(inode,
links)` was introduced by `110b24eb1a749b` ("fs/ntfs3: Taking DOS names
into account during link counting", Apr 17 2024, Konstantin Komarov).

**Step 3.2 — Fixes: target**
Record: No Fixes: tag in the commit, but the root cause (splitting
`names` vs `links` and using `links` for nlink while validating `names`)
was introduced by `110b24eb1a749b`. That commit had `Cc:
stable@vger.kernel.org` and `Fixes: 82cae269cfa95 ("fs/ntfs3: Add
initialization of super block")`.

**Step 3.3 — File history**
Record: Standalone commit; not part of any series. Close neighbors in
ntfs3 are independent fixes (e.g. `06909b2549d63`, `4b90f16e4bb56`
handling corrupted metadata).

**Step 3.4 — Author history**
Record: Ziyi Guo has 17 commits in the tree, all small defensive
validation / lock fixes across drivers (net, wifi, USB, ASoC, power) —
typical profile of a researcher/fuzzer-finder. Not the subsystem
maintainer, but the patch was applied by maintainer Konstantin Komarov.

**Step 3.5 — Dependencies**
Record: No dependency. Uses existing locals `links`/`names`, the
existing `out:` label, and `err = -EINVAL`. Self-contained.

### PHASE 4: MAILING LIST AND EXTERNAL RESEARCH

**Step 4.1 — b4 dig / thread**
Record: `b4 dig -c e10e72f69734a` → thread at `https://lore.kernel.org/a
ll/20260210155634.380168-1-n7l8m4@u.northwestern.edu/`. Single revision
(v1). Maintainer Konstantin Komarov replied Feb 24 2026: "Patch looks
good — applied. Thanks." No NAK, no stable nomination, no concerns.

**Step 4.2 — Reviewers**
Record: `b4 dig -w` shows the patch was sent to Konstantin Komarov
(maintainer), ntfs3@lists.linux.dev, linux-kernel@vger.kernel.org.
Appropriate recipients.

**Step 4.3 — Bug report**
Record: No Reported-by:/Link: tags. The commit wording ("A corrupted
NTFS image") and the author's fuzzer-style track record strongly suggest
this was found via fuzzing, though not attributed to syzbot on the list.

**Step 4.4 — Related patches**
Record: No series. Standalone.

**Step 4.5 — Stable discussion**
Record: None found.

### PHASE 5: CODE SEMANTIC ANALYSIS

**Step 5.1 — Key function**
Record: `ntfs_read_mft()`.

**Step 5.2 — Callers**
Record: `ntfs_read_mft` is only called from `ntfs_iget5()`. `ntfs_iget5`
is called with `name=NULL` from many user-reachable paths:
`fs/ntfs3/dir.c:265` (lookup via `indx_find`), `dir.c:337` (readdir /
`ntfs_dir_emit`), `fs/ntfs3/namei.c:371` (`ntfs3_get_parent` — NFS
export / open-by-handle), `fs/ntfs3/frecord.c:2945,3064`, plus super-
block metadata loads. In particular the dir.c:337 readdir path and the
namei.c:371 get_parent path exactly match the commit description's
"name=NULL to ntfs_iget5()".

**Step 5.3 — Callees affected by fix**
Record: Only `set_nlink(inode, links)` is gated behind the new check.
The `out:` path was already correct.

**Step 5.4 — Reachability**
Record: Reachable from userspace via mount + readdir/lookup on a crafted
NTFS image. `drop_nlink()` path reachable via `ntfs_unlink()` and
`ntfs3_rmdir()` in `fs/ntfs3/namei.c` / `fs/ntfs3/inode.c:1838`. End-to-
end chain confirmed.

**Step 5.5 — Similar patterns**
Record: Many recent ntfs3 commits follow the same pattern of hardening
against corrupted on-disk structures (`0dc7117da8f92`, `06909b2549d63`,
`4b90f16e4bb56`, `1732053c8a6b3`, `7443753436620`). Community actively
accepts and backports these.

### PHASE 6: CROSS-REFERENCING AND STABLE TREE ANALYSIS

**Step 6.1 — Bug present in stable?**
Record: The faulty `set_nlink(inode, links)` (without the `!links`
guard) was backported to:
- `stable/linux-5.15.y` as `7ab0c256964ef`
- `stable/linux-6.1.y` as `df40783dc3773`
- `stable/linux-6.6.y` as `e4fd2dce71fbd`
- `stable/linux-6.12.y` as `110b24eb1a749` (same SHA)
So the bug exists in 5.15+, 6.1+, 6.6+, 6.12+ stable trees.

**Step 6.2 — Backport complications**
Record: Verified the code context in `stable/linux-5.15.y` and
`stable/linux-6.6.y` — the surrounding lines (`names !=
le16_to_cpu(rec->hard_links)` block and `set_nlink(inode, links)`) are
identical to mainline. Patch applies cleanly; no rework expected for
5.15.y/6.1.y/6.6.y/6.12.y and newer active LTS branches. Branches
without 110b24eb's equivalent (older than 5.15 or before the April 2024
backport) do not have the bug and do not need this fix.

**Step 6.3 — Existing related stable fixes**
Record: None — no other "reject inodes with zero links" fix exists.

### PHASE 7: SUBSYSTEM AND MAINTAINER CONTEXT

**Step 7.1 — Subsystem criticality**
Record: `fs/ntfs3` — filesystem. IMPORTANT. Users of NTFS read-write
support (Windows dual-boot, removable media), plus security surface for
mountable filesystem images.

**Step 7.2 — Activity**
Record: Very active. Multiple corruption-hardening fixes in every recent
merge window.

### PHASE 8: IMPACT AND RISK ASSESSMENT

**Step 8.1 — Affected users**
Record: Users of ntfs3 who can mount attacker-controlled (or naturally
corrupted) NTFS images. Also anyone who auto-mounts removable media.
Filesystem-specific, not universal.

**Step 8.2 — Trigger**
Record: Crafted NTFS image where an MFT record has only FILE_NAME_DOS
attributes (and no non-DOS name). Valid NTFS never produces this, so the
trigger requires a malformed image. On systems allowing unprivileged
mounting (user namespaces + fuse-style setups, some distros' udisks
configs) the trigger can be reached without root.

**Step 8.3 — Failure mode severity**
Record: `WARN_ON(inode->i_nlink == 0)` → kernel warning with stack. On
`panic_on_warn` systems (common in hardened / KASAN / fuzzing
environments) it becomes a panic / DoS. Also a reliable way to trigger
VFS invariants violation for further exploration. Severity: MEDIUM-HIGH
(DoS via crafted FS image, security-relevant).

**Step 8.4 — Risk/benefit**
Record: Benefit: prevents a WARN/panic on malformed NTFS images, closes
a small attack surface. Risk: extremely low — 5 additional lines on the
error path, using the same `out:` cleanup already there; impossible to
affect a valid NTFS volume per the filesystem semantics described.

### PHASE 9: FINAL SYNTHESIS

**Step 9.1 — Evidence compile**

FOR:
- Small (5 lines), surgical, single-function, no lock/alloc churn.
- Fixes a concrete bug mechanism with clear reachability from user-
triggerable paths (readdir, lookup, NFS get_parent).
- Bug introduced by `110b24eb1a749b` (already in
5.15.y/6.1.y/6.6.y/6.12.y stable), so active LTS trees are vulnerable.
- Maintainer applied it with explicit approval ("Patch looks good —
applied").
- Same pattern (corrupted-FS validation) is standard stable material for
ntfs3.
- Failure mode is a WARN (or panic with panic_on_warn), security-
relevant for mountable FS images.
- Applies cleanly to stable branches (verified identical context in
5.15.y and 6.6.y).

AGAINST:
- No Fixes: / Cc: stable tags, no Reported-by:/Link:, no syzbot
attribution in message (absence of tags is expected per rules and not
a negative signal).

**Step 9.2 — Stable rules checklist**
1. Obviously correct & tested? Yes — maintainer-applied, trivial gate
check before existing code path.
2. Real bug? Yes — WARN triggers on documented code path.
3. Important? Yes — corrupted/malicious FS image → kernel WARN,
potential DoS; security-relevant.
4. Small/contained? Yes — 5 LOC, one function.
5. No new features/APIs? Correct.
6. Applies to stable? Yes — verified identical context in 5.15.y /
6.6.y.

**Step 9.3 — Exception**
Record: Fits the "corruption hardening" pattern commonly accepted for
filesystems.

**Step 9.4 — Decision**
The fix is an obvious, minimal validation that prevents a VFS invariant
violation reachable via a crafted NTFS image on all LTS trees that have
commit `110b24eb1a749b` backported (5.15, 6.1, 6.6, 6.12). Benefit
clearly outweighs negligible risk.

## Verification

- [Phase 1] Parsed commit message and tags directly from the commit
body; confirmed no Fixes:/Cc: stable/Reported-by: tags. No hidden
framing — patch explicitly says "reject".
- [Phase 2] Read the relevant portion of `fs/ntfs3/inode.c` (lines
140–230 and 400–500) to confirm `names` is incremented for every
ATTR_NAME, `links` only for non-DOS names, and the fix goto-targets
the existing `out:` handler.
- [Phase 3] `git blame` on `fs/ntfs3/inode.c` lines 420–440:
`set_nlink(inode, links)` originates in `110b24eb1a749b` ("fs/ntfs3:
Taking DOS names into account during link counting", 2024-04-17); that
commit has `Cc: stable@vger.kernel.org` and `Fixes: 82cae269cfa95`.
- [Phase 3] `git show 110b24eb1a749b`: confirmed the stable-tagged
commit that introduced the vulnerability pattern.
- [Phase 3] `git log --author="Ziyi Guo"`: 17 small validation / locking
fixes across subsystems; not subsystem maintainer.
- [Phase 4] `b4 dig -c e10e72f69734a`: found single-revision thread at `
https://lore.kernel.org/all/20260210155634.380168-1-
n7l8m4@u.northwestern.edu/`; saved mbox to `/tmp/thread.mbox` and read
the maintainer's "applied" reply.
- [Phase 4] `b4 dig -c e10e72f69734a -w`: confirmed recipients
(Konstantin Komarov, ntfs3@lists.linux.dev, linux-
kernel@vger.kernel.org).
- [Phase 4] Verified via spinics mirror that only one version was sent
and only one reply ("applied") was received.
- [Phase 5] `rg ntfs_iget5` in `fs/ntfs3`: enumerated all callers and
confirmed multiple user-reachable paths pass `name=NULL` (dir.c:337,
namei.c:371, frecord.c:2945/3064).
- [Phase 5] Read `drop_nlink()` at `fs/inode.c:416-422` to confirm the
exact `WARN_ON(inode->i_nlink == 0)` location.
- [Phase 5] Located `drop_nlink(inode)` call sites in ntfs3
(`namei.c:155`, `inode.c:1838`), confirming user unlink/rmdir/rename →
`drop_nlink` chain.
- [Phase 6] Queried each stable branch directly for the "Taking DOS
names" backport: found `7ab0c256964ef` in 5.15.y, `df40783dc3773` in
6.1.y, `e4fd2dce71fbd` in 6.6.y, `110b24eb1a749` in 6.12.y — the buggy
change is in those stables.
- [Phase 6] `git show stable/linux-6.6.y:fs/ntfs3/inode.c` and
`stable/linux-5.15.y:fs/ntfs3/inode.c` around the target lines:
context is identical to mainline, confirming clean application.
- [Phase 6] Searched each stable branch for the fix ("reject inodes" /
"non-DOS link" in inode.c); not present in any stable, so no
duplicate.
- [Phase 7] File path identifies subsystem as `fs/ntfs3`; `git log
--oneline fs/ntfs3` shows active subsystem with frequent corruption-
hardening fixes.
- [Phase 8] Failure mode verified by reading `drop_nlink()` source and
tracing the reachable ntfs3 call chain; severity assessed as MEDIUM-
HIGH (WARN → panic_on_warn).
- UNVERIFIED: Whether a syzbot report specifically mentions this
signature (no Reported-by in the commit; could not confirm a direct
syzkaller report link). This does not affect the decision because the
mechanism is verified by code reading.

The fix is small, obviously correct, fixes a real reachable bug, is
already applied by the maintainer, and applies cleanly to LTS trees that
have the buggy precursor commit. It meets all stable-kernel criteria.

**YES**

fs/ntfs3/inode.c | 5 +++++
1 file changed, 5 insertions(+)

diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c
index 6e65066ebcc1a..398913595a551 100644
--- a/fs/ntfs3/inode.c
+++ b/fs/ntfs3/inode.c
@@ -432,6 +432,11 @@ static struct inode *ntfs_read_mft(struct inode *inode,
ni->mi.dirty = true;
}

+ if (!links) {
+ err = -EINVAL;
+ goto out;
+ }
+
set_nlink(inode, links);

if (S_ISDIR(mode)) {
--
2.53.0