From: Michael Bommarito <michael.bommarito@gmail.com>
To: Joseph Qi <joseph.qi@linux.alibaba.com>,
Mark Fasheh <mark@fasheh.com>, Joel Becker <jlbec@evilplan.org>
Cc: ZhengYuan Huang <gality369@gmail.com>,
ocfs2-devel@lists.linux.dev, linux-fsdevel@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: [PATCH 3/3] ocfs2: reject regular files with non-zero i_size and zero i_clusters
Date: Sun, 17 May 2026 07:10:14 -0400 [thread overview]
Message-ID: <20260517111015.3187935-4-michael.bommarito@gmail.com> (raw)
In-Reply-To: <20260517111015.3187935-1-michael.bommarito@gmail.com>
On a volume mounted WITHOUT OCFS2_FEATURE_INCOMPAT_SPARSE_ALLOC, a
regular file with non-zero i_size, zero i_clusters, and no
OCFS2_INLINE_DATA_FL flag is structurally malformed: the extent
map declares no allocated clusters yet the size header claims
the file has content. ocfs2_populate_inode() copies i_size into
the in-core inode and dispatches to ocfs2_aops; subsequent reads
or truncates then operate on an inconsistent extent state.
This is the shape an attacker who keeps the rest of the extent
list intact (to satisfy the inline-data, refcount, chain-list,
and per-field validators already in this function) would produce
when forging only the inode header to publish a synthetic file
size on a victim node. It is also the shape on-disk corruption
of the i_clusters field produces. Reject early in the
validator.
The check is restricted to non-sparse volumes
(ocfs2_sparse_alloc() returns false). On non-sparse mounts the
allocator path always grows clusters before i_size:
ocfs2_extend_file() takes the !sparse branch into
ocfs2_extend_no_holes(), which calls ocfs2_extend_allocation()
to journal new clusters first, and only then
ocfs2_simple_size_update() journals the larger i_size. The
truncate path likewise lowers i_size in ocfs2_orphan_for_truncate()
and then frees clusters in ocfs2_commit_truncate(), which uses
ocfs2_clusters_for_bytes(new_i_size) as its new_highest_cpos:
when new_i_size > 0 the floor is at least one cluster, so the
on-disk dinode never legitimately exposes a non-inline regular
file with i_size > 0 and i_clusters == 0 on a non-sparse volume.
On sparse-alloc volumes the same shape is legitimate: an
ocfs2_extend_file() call goes through ocfs2_zero_extend() +
ocfs2_simple_size_update(), which grows i_size on its own
without changing i_clusters; a freshly truncate -s 1M of a
sparse regular file is therefore on-disk
(i_size = 1048576, i_clusters = 0). The check therefore opts
out via ocfs2_sparse_alloc(OCFS2_SB(sb)).
System inodes (OCFS2_SYSTEM_FL) carry their own size and
cluster invariants validated by the allocator, journal, quota,
and truncate-log subsystems; skip them here. The inline-data
fast path is filtered separately by its own dedicated branch
below: its well-formed case is exactly i_clusters == 0 with
i_size <= id_count. Symlinks legitimately keep i_clusters ==
0 with non-zero i_size (fast symlinks), so this check is
restricted to S_IFREG.
Fixes: b657c95c1108 ("ocfs2: Wrap inode block reads in a dedicated function.")
Cc: stable@vger.kernel.org
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
Assisted-by: Claude:claude-opus-4-7
---
fs/ocfs2/inode.c | 41 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
index 305e22cc9b1d9..c63d2ced6b338 100644
--- a/fs/ocfs2/inode.c
+++ b/fs/ocfs2/inode.c
@@ -1571,6 +1571,47 @@ int ocfs2_validate_inode_block(struct super_block *sb,
goto bail;
}
+ /*
+ * On a non-sparse volume, a regular file with non-zero i_size
+ * and zero i_clusters that is not marked as inline data is
+ * structurally malformed: the extent map declares no allocated
+ * clusters yet the size header claims the file has content.
+ * ocfs2_populate_inode() would still publish i_size to VFS and
+ * leave the extent state inconsistent for any later read or
+ * truncate. This is the shape an attacker who keeps the rest
+ * of the extent list intact (to satisfy the inline-data,
+ * refcount, chain-list, and per-field validators above) would
+ * produce when forging only the inode header to publish a
+ * synthetic file size on a victim node. It is also the shape
+ * on-disk corruption of the i_clusters field produces.
+ *
+ * The check opts out on sparse-alloc volumes, where the
+ * extend path (ocfs2_extend_file -> ocfs2_zero_extend ->
+ * ocfs2_simple_size_update) legitimately grows i_size without
+ * allocating clusters. On non-sparse volumes the equivalent
+ * path (ocfs2_extend_no_holes) journals clusters first and
+ * i_size second, and truncate-down floors i_clusters at
+ * ocfs2_clusters_for_bytes(new_i_size) which is >= 1 whenever
+ * new_i_size > 0, so the rejected shape never appears on disk.
+ *
+ * Skip system inodes (OCFS2_SYSTEM_FL) and the inline-data
+ * fast path (handled below). Symlinks legitimately keep
+ * i_clusters == 0 with non-zero i_size (fast symlinks), so
+ * restrict to S_IFREG.
+ */
+ if (!ocfs2_sparse_alloc(OCFS2_SB(sb)) &&
+ S_ISREG(le16_to_cpu(di->i_mode)) &&
+ !(le32_to_cpu(di->i_flags) & OCFS2_SYSTEM_FL) &&
+ !(le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL) &&
+ le64_to_cpu(di->i_size) != 0 &&
+ le32_to_cpu(di->i_clusters) == 0) {
+ rc = ocfs2_error(sb,
+ "Invalid dinode #%llu: regular file i_size %llu with i_clusters 0 and no inline-data flag on non-sparse volume\n",
+ (unsigned long long)bh->b_blocknr,
+ (unsigned long long)le64_to_cpu(di->i_size));
+ goto bail;
+ }
+
if (le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL) {
struct ocfs2_inline_data *data = &di->id2.i_data;
--
2.53.0
next prev parent reply other threads:[~2026-05-17 11:10 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-17 11:10 [PATCH 0/3] ocfs2: harden inode validators against forged metadata Michael Bommarito
2026-05-17 11:10 ` [PATCH 1/3] ocfs2: reject dinodes with non-canonical i_mode type or stray bits Michael Bommarito
2026-05-18 1:36 ` Joseph Qi
2026-05-17 11:10 ` [PATCH 2/3] ocfs2: reject dinodes whose i_rdev disagrees with the file type Michael Bommarito
2026-05-18 1:37 ` Joseph Qi
2026-05-17 11:10 ` Michael Bommarito [this message]
2026-05-18 1:38 ` [PATCH 3/3] ocfs2: reject regular files with non-zero i_size and zero i_clusters Joseph Qi
2026-05-18 21:40 ` [PATCH 0/3] ocfs2: harden inode validators against forged metadata Andrew Morton
2026-05-19 0:57 ` Michael Bommarito
2026-06-01 17:39 ` Joel Becker
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260517111015.3187935-4-michael.bommarito@gmail.com \
--to=michael.bommarito@gmail.com \
--cc=gality369@gmail.com \
--cc=jlbec@evilplan.org \
--cc=joseph.qi@linux.alibaba.com \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=mark@fasheh.com \
--cc=ocfs2-devel@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®