[PATCH AUTOSEL 6.18-5.10] romfs: detect hard link cycles
From: Sasha Levin
Date: Mon Oct 19 2026 - 10:39:43 EST
From: 이상호 <kudo3228@xxxxxxxxx>
[ Upstream commit d30b5a954e0a4dcc16bba9c310c13709a6169a52 ]
romfs_iget() follows on-disk hard link entries until it reaches a non-hard
link inode:
pos = be32_to_cpu(ri.spec) & ROMFH_MASK;
The target position is image-controlled, and the loop does not detect
cycles. A crafted romfs image can make the root inode a hard link. The hard
link can point back to itself and leave mount(2) spinning in the kernel.
Reject excessive hard link indirection with -ELOOP. Normal romfs images do
not need long hard link chains. This bounds corrupted-image traversal.
Propagate romfs_iget() errors from lookup because hard link traversal can
now fail with -ELOOP.
Signed-off-by: 이상호 <kudo3228@xxxxxxxxx>
Link: https://patch.msgid.link/20260701220729.822112-1-kudo3228@xxxxxxxxx
Signed-off-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>
Signed-off-by: Sasha Levin <sashal@xxxxxxxxxx>
---
LLM Generated explanations, may be completely bogus:
The local tree is **6.1.188** (`pending-6.1`). The unbounded hard-link
walk is present here; the fix is not.
---
## PHASE 1: COMMIT MESSAGE FORENSICS
**Step 1.1: PARSE THE SUBJECT LINE**
- Subsystem prefix: `romfs:`
- Action verb: `detect` (bug-fix language: find/stop a cycle that
currently is not handled)
- Claimed intent: stop unbounded on-disk hard-link traversal
Record: **[romfs] [detect] [bound hard-link walk so a crafted image
cannot spin forever]**
**Step 1.2: PARSE ALL COMMIT MESSAGE TAGS**
Verified from `d30b5a954e0a4` on `origin/master` (mainline), not from
this 6.1 HEAD:
- `Signed-off-by: 이상호 <kudo3228@xxxxxxxxx>` — author
- `Link:
https://patch.msgid.link/20260701220729.822112-1-kudo3228@xxxxxxxxx` —
original submission
- `Signed-off-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>` —
VFS maintainer applied it
- **No** `Fixes:` tag
- **No** `Reported-by:` / syzbot
- **No** `Tested-by:` / `Reviewed-by:` / `Acked-by:`
- **No** `Cc: stable@xxxxxxxxxxxxxxx` (expected for this review
pipeline)
Record: **Author SOB + VFS-maintainer SOB + lore Link. No syzbot, no
Fixes, no stable CC.**
**Step 1.3: ANALYZE THE COMMIT BODY TEXT**
- Bug: `romfs_iget()` follows `ri.spec` while the entry type is
`ROMFH_HRD`. The target is image-controlled. There is no cycle/depth
check.
- Symptom: a crafted image can make the root inode a self-referential
hard link; `mount(2)` never returns (kernel-side infinite loop).
- Fix: cap hard-link hops at 64 and return `-ELOOP`. Also return
`romfs_iget()` errors from lookup so `-ELOOP` is visible to userspace.
- Author: normal images do not need long hard-link chains.
Record: **[crafted-image infinite hard-link walk] [mount hangs in
kernel] [no version bound in message] [missing cycle/depth check on
image-controlled `spec`]**
**Step 1.4: DETECT HIDDEN BUG FIXES**
Not hidden. The subject and body describe a hang. The lookup `IS_ERR`
check is supporting error propagation, not cleanup-for-style.
Record: **Explicit hang fix, not a disguised cleanup.**
---
## PHASE 2: DIFF ANALYSIS
**Step 2.1: INVENTORY THE CHANGES**
Verified via `git show --stat d30b5a954e0a4`: **8 insertions, 0
deletions, one file `fs/romfs/super.c`.**
Functions:
- `romfs_lookup()` — after `romfs_iget()`, return `ERR_CAST(inode)` on
error
- `romfs_iget()` — `depth` counter; abort after
`ROMFS_MAX_HARDLINK_DEPTH` (64)
The provided candidate diff also shows `inode_state_read_once(i)` in
**unchanged context**. That helper does **not** exist in this 6.1 tree
(`grep` found zero hits). The **changed** lines do not use it.
Record: **[+8 / -0, one file] [romfs_lookup, romfs_iget] [single-file
surgical fix]**
**Step 2.2: UNDERSTAND THE CODE FLOW CHANGE**
Hunk 1 (`romfs_lookup`): on name match, optionally one hard-link hop,
then `romfs_iget()`. Before: any `ERR_PTR` was passed to
`d_splice_alias()`. After: lookup returns the error immediately.
Hunk 2 (`romfs_iget`): `for (;;)` reads the header, breaks only when
type is not `ROMFH_HRD`, else sets `pos` from `spec` with no hop limit.
After: 65th hop returns `-ELOOP`.
Affected paths: mount (`romfs_fill_super` → `romfs_iget`) and lookup of
hard-linked names.
Record: **[unbounded HRD walk → depth-capped walk; lookup now fails
closed on iget errors]**
**Step 2.3: IDENTIFY THE BUG MECHANISM**
Category: **logic / correctness plus unbounded iteration** (crafted-
image hang). Not UAF, not a leak.
Mechanism: on-disk `spec` is trusted as the next header. A cycle (or a
long in-image HRD chain) never hits a non-HRD type. `romfs_dev_read()`
only fails on I/O or out-of-image `pos` (`pos >= limit` in
`fs/romfs/storage.c`), so a **valid in-image cycle** never exits.
Record: **[unbounded image-controlled loop] [depth limit + -ELOOP]**
**Step 2.4: ASSESS THE FIX QUALITY**
Obviously correct: same pattern as symlink `MAX_NESTED_LINKS` /
`-ELOOP`. 64 hops is generous (VFS symlink cap in this tree is 8).
Legitimate genromfs images use hard links for `.` / `..` (one hop),
documented in `Documentation/filesystems/romfs.rst`.
Regression risk is low. The lookup `IS_ERR` check is slightly redundant
here because `d_splice_alias()` already does `if (IS_ERR(inode)) return
ERR_CAST(inode);` (`fs/dcache.c:3147`), but it is still correct.
Record: **[minimal, obviously correct] [low regression risk; 64 is well
above real romfs usage]**
---
## PHASE 3: GIT HISTORY INVESTIGATION
**Step 3.1: BLAME THE CHANGED LINES**
`git blame fs/romfs/super.c` around the loop:
- Hard-link walk in `romfs_iget()`: **da4458bda237aa** (David Howells,
2009-02-12), “NOMMU: Make it possible for RomFS to use MTD devices
directly”
- `git describe --contains da4458bda237aa` → **v2.6.30-rc1**
- `git merge-base --is-ancestor da4458bda237aa HEAD` → **yes, in this
tree**
The unbounded loop has been here for the entire 6.1 line.
Record: **[da4458bda237aa / v2.6.30] [buggy walk present in 6.1.188]**
**Step 3.2: FOLLOW THE FIXES: TAG**
No `Fixes:` tag. The introducing commit is identified by blame, not by
the message.
Record: **[No Fixes tag] [introducer da4458bda237aa is in this tree]**
**Step 3.3: CHECK FILE HISTORY FOR RELATED CHANGES**
Path-limited `git log --oneline -- fs/romfs/super.c` on this tree:
recent work is `romfs: check sb_set_blocksize() return value` (already
here), `read_folio`, inode alloc. No prior cycle check.
On `origin/master` after this fix: merge of vfs-7.3 plus unrelated `fs:
add dedicated block device open helpers`. **No follow-up correction** of
this patch.
`40e2c71d57565` (“romfs: fix returm err while getting inode in
fill_super”) already makes `romfs_fill_super()` check `IS_ERR(root)` —
mount will fail with `-ELOOP` once `romfs_iget()` can return it.
Record: **[standalone; no prerequisite series; fill_super already
propagates iget errors]**
**Step 3.4: CHECK THE AUTHOR'S OTHER COMMITS**
`git log --author="kudo3228@xxxxxxxxx"` on this tree: Bluetooth HIDP
payload/header validation. Same author pattern (image/packet bounds),
not the romfs maintainer.
Record: **[contributor, not romfs maintainer] [applied by VFS maintainer
Brauner]**
**Step 3.5: CHECK FOR DEPENDENT/PREREQUISITE COMMITS**
The patch is `[PATCH]` (single), not `n/m`. It only uses `IS_ERR` /
`ERR_PTR` / `ERR_CAST` / `ELOOP`, all present in 6.1. It does **not**
require `inode_state_read_once`.
Record: **[no dependencies] [can apply standalone to this tree]**
---
## PHASE 4: MAILING LIST AND EXTERNAL RESEARCH
**Step 4.1: FIND THE ORIGINAL PATCH DISCUSSION**
`b4 dig -c <sha>` cannot match a commit that is not HEAD. `b4 mbox` on
Message-ID `20260701220729.822112-1-kudo3228@xxxxxxxxx` saved
`/tmp/romfs-hardlink.mbox`: **2 messages**.
1. Submission 2026-07-02 to linux-kernel and linux-fsdevel, subject
`[PATCH] romfs: detect hard link cycles`
2. Christian Brauner 2026-07-03: applied to `vfs-7.3.misc` as
`c4d617e5d28d`
No v2/v3. No NAK. No reviewer-requested respin. Direct-to-maintainer
apply.
Record: **[lore thread via msgid
20260701220729.822112-1-kudo3228@xxxxxxxxx] [single revision] [applied
by Brauner] [no NAK]**
**Step 4.2: CHECK WHO REVIEWED THE PATCH**
Original To/Cc: `linux-kernel@xxxxxxxxxxxxxxx`, `linux-
fsdevel@xxxxxxxxxxxxxxx`. No extra reviewer list. Brauner applied it
(VFS maintainer per `MAINTAINERS`: FILESYSTEMS (VFS and
infrastructure)).
Record: **[linux-fsdevel + VFS maintainer apply; no Reviewed-by
trailers]**
**Step 4.3: SEARCH FOR THE BUG REPORT**
No `Reported-by` or bugzilla/syzbot link. The hang is reasoned from the
code, not from a public crash report.
lore.kernel.org HTTP fetches returned **403**; discussion was recovered
via `b4 mbox`.
Record: **[no public bug report] [author describes mount hang from
crafted root HRD cycle]**
**Step 4.4: CHECK FOR RELATED PATCHES AND SERIES**
Standalone `[PATCH] 1/1`. No related romfs cycle patches in the mbox or
in `git log origin/master -- fs/romfs/super.c` after it.
Record: **[standalone, not part of a multi-patch series]**
**Step 4.5: CHECK STABLE MAILING LIST HISTORY**
lore.kernel.org/stable search returned 403. `git log origin/master
--grep="detect hard link cycles" -- fs/romfs/super.c` finds only the
mainline commit. No evidence of a stable NAK.
Record: **[no stable-list discussion recovered; UNVERIFIED beyond 403 on
lore/stable]**
---
## PHASE 5: CODE SEMANTIC ANALYSIS
**Step 5.1: KEY FUNCTIONS**
`romfs_lookup()`, `romfs_iget()`. New macro `ROMFS_MAX_HARDLINK_DEPTH`.
**Step 5.2: TRACE CALLERS**
`git grep romfs_iget` in `fs/romfs/super.c`:
- `romfs_lookup()` — directory lookup (`->lookup`)
- `romfs_fill_super()` line 536 — get root inode during mount
`romfs_lookup` is `romfs_dir_inode_operations.lookup`.
**Step 5.3: TRACE CALLEES**
`romfs_iget` calls `romfs_dev_read`, `romfs_dev_strnlen`, `iget_locked`.
`romfs_dev_read` rejects `pos >= romfs_maxsize(sb)` with `-EIO`, so
**out-of-image** pointers stop; **in-image cycles** do not.
**Step 5.4: FOLLOW THE CALL CHAIN**
`mount("romfs")` → `romfs_get_tree` → `romfs_fill_super` →
`romfs_iget(root)`. If that root header is `ROMFH_HRD` pointing at
itself, the syscall never returns.
Lookup of a cyclic hard link after a successful mount has the same hang
in `romfs_iget`.
`romfs_fs_type.fs_flags = FS_REQUIRES_DEV` only — **no
`FS_USERNS_MOUNT`**. Mount needs `CAP_SYS_ADMIN` (or equivalent). Still
reachable from `mount(2)` on a loop/MTD/block image.
Record: **[reachable from mount and lookup] [needs mount privilege, not
unprivileged userns]**
**Step 5.5: SEARCH FOR SIMILAR PATTERNS**
No `ROMFS_MAX_HARDLINK` elsewhere. VFS uses `MAX_NESTED_LINKS = 8` in
`include/linux/namei.h`. SMB client uses the same `-ELOOP` hop bound.
This is the standard “bound image-controlled indirection” pattern.
Romfs docs (`Documentation/filesystems/romfs.rst`) say it is the **image
creator’s** job not to make hard-link loops. The kernel still must not
spin if they do.
Record: **[same class as VFS ELOOP bounds; romfs previously trusted the
image]**
---
## PHASE 6: CROSS-REFERENCING AGAINST THE LOCAL TREE
**Step 6.1: DOES THE BUGGY CODE EXIST IN THIS TREE?**
Yes. Local `fs/romfs/super.c`:
```291:303:fs/romfs/super.c
for (;;) {
ret = romfs_dev_read(sb, pos, &ri, sizeof(ri));
if (ret < 0)
goto error;
nextfh = be32_to_cpu(ri.next);
if ((nextfh & ROMFH_TYPE) != ROMFH_HRD)
break;
pos = be32_to_cpu(ri.spec) & ROMFH_MASK;
}
```
No `ROMFS_MAX_HARDLINK_DEPTH`. `git merge-base --is-ancestor
d30b5a954e0a4 HEAD` is false. Introduced in v2.6.30, still here.
Record: **[yes, romfs_iget HRD loop] [since v2.6.30]**
**Step 6.2: CHECK FOR BACKPORT COMPLICATIONS**
Local `romfs_lookup` / `romfs_iget` match the patch hunks. The only
mainline-only context is `inode_state_read_once`, which this patch does
not change. 6.1 still uses `i->i_state & I_NEW`.
Record: **[clean apply expected; no `inode_state_read_once`
dependency]**
**Step 6.3: CHECK IF RELATED FIXES ARE ALREADY HERE**
No cycle detection. Related: fill_super already returns `PTR_ERR(root)`
(`40e2c71d57565`). Another crafted-image romfs fix (`sb_set_blocksize`)
is already in this 6.1 tree — same class of image-validation bug.
Record: **[no existing cycle fix] [fill_super error path ready]**
---
## PHASE 7: SUBSYSTEM AND MAINTAINER CONTEXT
**Step 7.1: SUBSYSTEM AND CRITICALITY**
`fs/romfs/` — small read-only FS for initrd/embedded/MTD. **PERIPHERAL**
relative to ext4/VFS, but it is a real in-tree filesystem used on 6.1
devices that enable `CONFIG_ROMFS_FS`.
**Step 7.2: SUBSYSTEM ACTIVITY**
Low churn. The HRD walk has been stable since 2009. That makes the hang
long-lived, not a new-subsystem mismatch.
Record: **[romfs] [PERIPHERAL but long-present] [mature, little recent
change]**
---
## PHASE 8: IMPACT AND RISK ASSESSMENT
**Step 8.1: WHO IS AFFECTED**
Users who mount romfs (`CONFIG_ROMFS_FS`) from block or MTD. Not
universal. Includes initrd, embedded, and anyone mounting an untrusted
image (USB, loop).
**Step 8.2: TRIGGER CONDITIONS**
- Crafted image: root header type `ROMFH_HRD`, `spec` pointing at itself
(or a cycle).
- Or lookup of a cyclic hard-link directory entry.
- Not every boot; only bad/malicious images.
- Unprivileged users cannot mount romfs in a user namespace
(`FS_REQUIRES_DEV` only). Admins, installers, and fuzzers can.
**Step 8.3: FAILURE MODE SEVERITY**
Infinite loop in `romfs_iget` during `mount(2)` or lookup. After the
first header is cached, `romfs_dev_read` is a tight CPU loop → hung task
/ soft lockup, one CPU stuck in kernel, mount never completes.
Record: **[kernel hang / soft lockup] [HIGH to CRITICAL for
CONFIG_ROMFS_FS; local DoS with mount cap]**
**Step 8.4: RISK-BENEFIT RATIO**
- Benefit: stops a real hang on corrupted/malicious romfs images; 8
lines; same validation class already accepted in this tree
(`sb_set_blocksize`).
- Risk: very low; depth 64 will not break genromfs images.
Record: **[high benefit for romfs users] [very low risk] [favor
backport]**
---
## PHASE 9: FINAL SYNTHESIS
**Step 9.1: EVIDENCE**
FOR:
- Real unbounded loop; image-controlled `spec`; self-loop hangs
`mount(2)`
- Code is in this 6.1 tree since v2.6.30
- 8-line, one-file, obviously correct `-ELOOP` bound
- Applied by VFS maintainer; no NAK; no later revert/fix on mainline
- fill_super already handles `IS_ERR` from `romfs_iget`
- Applies without `inode_state_read_once`
- Important: hang/soft lockup, not a style tweak
AGAINST:
- romfs is niche
- Needs mount privilege (not unprivileged userns)
- No syzbot/Reported-by/Tested-by
- Lookup `IS_ERR` is redundant with `d_splice_alias` in 6.1
UNRESOLVED:
- lore.kernel.org HTTP 403; stable-list search not readable via WebFetch
(mbox via b4 succeeded for the fsdevel thread)
**Step 9.2: STABLE RULES CHECKLIST**
1. Obviously correct and tested? **PASS** — trivial depth bound; applied
by Brauner. Testing is implied by mainline merge, not by a Tested-by.
2. Fixes a real bug that affects users? **PASS** — crafted/corrupt romfs
hangs mount/lookup.
3. Important (crash, security, corruption, deadlock)? **PASS** — kernel
hang / soft lockup (local DoS).
4. Small and contained? **PASS** — 8 lines, one file.
5. No new features or APIs? **PASS**.
6. Can apply to this tree? **PASS** — buggy code present; hunks match
6.1 aside from unrelated `inode_state_read_once` context.
**Step 9.3: EXCEPTION CATEGORIES**
None of device-ID / quirk / DT / build / docs. Ordinary small hang fix.
**Step 9.4: DECISION**
This is a small, correct bound on image-controlled iteration that
prevents a kernel hang on 6.1. Privilege to mount does not make an
infinite kernel loop acceptable for stable. Backport it.
---
### What the commit solves for this tree
`romfs_iget()` walks on-disk hard links with no hop limit. A crafted
image can make that walk a cycle, including at the root inode, so
`mount(2)` spins in the kernel. 6.1.188 still has that loop. Returning
`-ELOOP` after 64 hops stops the hang; lookup error propagation is a
small companion change.
### Risk vs benefit
Benefit is avoiding a hung task on bad romfs images. Risk is near zero:
legitimate images do not need deep HRD chains, and the patch does not
change data structures or APIs.
### Concerns
None material. Do not port `inode_state_read_once`; it is not part of
the fix. No prerequisite commits.
---
### Verification
- **[Phase 1]** `git describe HEAD` → `p-6.1-11-g10bc6eb33d95f`; `make
kernelversion` → **6.1.188**
- **[Phase 1]** `git log origin/master -1 d30b5a954e0a4` — tags: author
SOB, Brauner SOB, msgid Link; no Fixes/Reported-by/Cc:stable
- **[Phase 2]** `git show --stat d30b5a954e0a4` — `fs/romfs/super.c` +8
- **[Phase 2]** Read local `romfs_iget` / `romfs_lookup` /
`romfs_fill_super` / `romfs_dev_read` — unbounded HRD loop; out-of-
image reads fail; in-image cycles do not
- **[Phase 2]** `d_splice_alias` in this tree already handles
`IS_ERR(inode)` (`fs/dcache.c:3147`)
- **[Phase 3]** `git blame` — HRD walk from **da4458bda237aa**
(v2.6.30); `git merge-base --is-ancestor da4458bda237aa HEAD` true
- **[Phase 3]** `git merge-base --is-ancestor d30b5a954e0a4 HEAD` false
— fix **not** in this tree
- **[Phase 3]** `grep ROMFS_MAX_HARDLINK fs/romfs/super.c` — not present
- **[Phase 3]** `git log origin/master d30b5a954e0a4^..origin/master --
fs/romfs/super.c` — no follow-up fix
- **[Phase 3]** `grep inode_state_read_once` in this tree — **zero
hits**
- **[Phase 4]** `b4 mbox` msgid
`20260701220729.822112-1-kudo3228@xxxxxxxxx` — 2-message thread;
Brauner applied to vfs-7.3.misc
- **[Phase 4]** WebFetch lore.kernel.org / patch.msgid.link /
git.kernel.org → **403** / bot page
- **[Phase 5]** Callers of `romfs_iget`: fill_super (mount) and lookup;
`fs_flags = FS_REQUIRES_DEV` only
- **[Phase 5]** Docs: hard-link loops called out as image-creator
responsibility, not kernel-enforced
- **[Phase 6]** Local loop at `fs/romfs/super.c:291-303` matches the
pre-patch code
- **[Phase 8]** Failure mode: infinite loop in kernel on mount/lookup of
cyclic HRD
- **UNVERIFIED:** stable@ mailing-list archive (HTTP 403); no public
crash ID beyond the commit text
**YES**
fs/romfs/super.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/fs/romfs/super.c b/fs/romfs/super.c
index e83f9b78d7a16..9b997831a46bc 100644
--- a/fs/romfs/super.c
+++ b/fs/romfs/super.c
@@ -240,6 +240,8 @@ static struct dentry *romfs_lookup(struct inode *dir, struct dentry *dentry,
if ((be32_to_cpu(ri.next) & ROMFH_TYPE) == ROMFH_HRD)
offset = be32_to_cpu(ri.spec) & ROMFH_MASK;
inode = romfs_iget(dir->i_sb, offset);
+ if (IS_ERR(inode))
+ return ERR_CAST(inode);
break;
}
@@ -262,6 +264,8 @@ static const struct inode_operations romfs_dir_inode_operations = {
.lookup = romfs_lookup,
};
+#define ROMFS_MAX_HARDLINK_DEPTH 64
+
/*
* get a romfs inode based on its position in the image (which doubles as the
* inode number)
@@ -273,6 +277,7 @@ static struct inode *romfs_iget(struct super_block *sb, unsigned long pos)
struct inode *i;
unsigned long nlen;
unsigned nextfh;
+ unsigned int depth = 0;
int ret;
umode_t mode;
@@ -289,6 +294,9 @@ static struct inode *romfs_iget(struct super_block *sb, unsigned long pos)
if ((nextfh & ROMFH_TYPE) != ROMFH_HRD)
break;
+ if (++depth > ROMFS_MAX_HARDLINK_DEPTH)
+ return ERR_PTR(-ELOOP);
+
pos = be32_to_cpu(ri.spec) & ROMFH_MASK;
}
--
2.53.0