* [PATCH v2 0/3] ocfs2: harden inode validators against forged metadata
@ 2026-05-19 11:04 Michael Bommarito
2026-05-19 11:04 ` [PATCH v2 1/3] ocfs2: reject dinodes with non-canonical i_mode type Michael Bommarito
` (2 more replies)
0 siblings, 3 replies; 9+ messages in thread
From: Michael Bommarito @ 2026-05-19 11:04 UTC (permalink / raw)
To: Joseph Qi, Mark Fasheh, Joel Becker
Cc: ZhengYuan Huang, ocfs2-devel, linux-fsdevel, linux-kernel
This series adds three structural checks to OCFS2 dinode validation so
malformed on-disk fields are rejected before ocfs2_populate_inode()
copies them into the in-core inode.
The checks cover:
- i_mode values whose type bits do not name a canonical POSIX file
type;
- non-device dinodes whose id1.dev1.i_rdev field is non-zero; and
- non-inline dinodes that claim non-zero i_size while i_clusters is
zero, covering directories unconditionally and regular files on
non-sparse volumes.
The normal read path reports these through ocfs2_error(), matching the
existing suballoc-slot, inline-data, chain-list, and refcount checks.
The online filecheck path uses the same structural predicates but keeps
its own reporting contract, returning OCFS2_FILECHECK_ERR_INVALIDINO
instead of calling ocfs2_error().
Validation from v1 still applies to the unchanged reachability model.
---
Changes in v2:
- Patch 1 drops the tautological S_IFMT|07777 mask check and reuses
fs_umode_to_ftype() for the canonical file-type predicate.
- Patch 1 mirrors the i_mode check in the online filecheck path.
- Patch 2 factors the i_rdev cross-check into a shared predicate.
- Patch 2 mirrors the i_rdev check in the online filecheck path.
- Patch 3 factors the size/cluster invariant into a shared predicate.
- Patch 3 extends the zero-cluster rejection to non-inline
directories, while preserving the sparse regular-file carveout.
- Patch 3 mirrors the size/cluster check in the online filecheck path.
- Patches 1 and 3 add Link trailers to the Sashiko review Andrew
pointed out.
Testing after v2:
- Replayed all three patches onto the original base, checked with
checkpatch --strict, and applied cleanly with git am.
- Rebuilt a fresh bzImage from the v2-applied tree.
- Booted that v2 kernel five times under QEMU: sparse regular-file
regression, non-sparse no-forge regression, forged non-canonical
i_mode, forged non-device i_rdev, and forged non-zero i_size with
zero i_clusters.
- The three forged dinodes were rejected by
ocfs2_validate_inode_block(); the two unforged regression cases
completed without validator errors.
Michael Bommarito (3):
ocfs2: reject dinodes with non-canonical i_mode type
ocfs2: reject dinodes whose i_rdev disagrees with the file type
ocfs2: reject non-inline dinodes with i_size and zero i_clusters
fs/ocfs2/inode.c | 151 ++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 149 insertions(+), 2 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 1/3] ocfs2: reject dinodes with non-canonical i_mode type
2026-05-19 11:04 [PATCH v2 0/3] ocfs2: harden inode validators against forged metadata Michael Bommarito
@ 2026-05-19 11:04 ` Michael Bommarito
2026-05-19 12:21 ` Joseph Qi
2026-06-01 18:19 ` Joel Becker
2026-05-19 11:04 ` [PATCH v2 2/3] ocfs2: reject dinodes whose i_rdev disagrees with the file type Michael Bommarito
2026-05-19 11:04 ` [PATCH v2 3/3] ocfs2: reject non-inline dinodes with i_size and zero i_clusters Michael Bommarito
2 siblings, 2 replies; 9+ messages in thread
From: Michael Bommarito @ 2026-05-19 11:04 UTC (permalink / raw)
To: Joseph Qi, Mark Fasheh, Joel Becker
Cc: ZhengYuan Huang, ocfs2-devel, linux-fsdevel, linux-kernel
ocfs2_validate_inode_block() currently accepts any non-zero i_mode value.
ocfs2_populate_inode() then copies that mode verbatim into inode->i_mode
and dispatches on i_mode & S_IFMT to the file/dir/symlink/special_file
iops; an unrecognised type falls through to ocfs2_special_file_iops and
init_special_inode().
Reject dinodes whose type bits do not name one of the seven canonical
POSIX file types. Use fs_umode_to_ftype(), the same generic file-type
conversion helper OCFS2 already uses for directory entries, so the
accepted inode type set matches the kernel file-type vocabulary instead
of open-coding a local switch.
Apply the same structural check to the online filecheck read path.
filecheck keeps its own error namespace, so it reports malformed i_mode
through the filecheck logger and OCFS2_FILECHECK_ERR_INVALIDINO instead
of calling ocfs2_error(), but it must not allow a malformed dinode to
proceed into ocfs2_populate_inode().
Fixes: b657c95c1108 ("ocfs2: Wrap inode block reads in a dedicated function.")
Cc: stable@vger.kernel.org
Link: https://sashiko.dev/#/patchset/20260517111015.3187935-1-michael.bommarito%40gmail.com
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
Assisted-by: Claude:claude-opus-4-7
---
fs/ocfs2/inode.c | 36 ++++++++++++++++++++++++++++++++++--
1 file changed, 34 insertions(+), 2 deletions(-)
diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
index a510a0eb1adcc..e149ccbdc03ce 100644
--- a/fs/ocfs2/inode.c
+++ b/fs/ocfs2/inode.c
@@ -13,6 +13,7 @@
#include <linux/pagemap.h>
#include <linux/quotaops.h>
#include <linux/iversion.h>
+#include <linux/fs_dirent.h>
#include <asm/byteorder.h>
@@ -64,7 +65,12 @@ static int ocfs2_filecheck_read_inode_block_full(struct inode *inode,
static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
struct buffer_head *bh);
static int ocfs2_filecheck_repair_inode_block(struct super_block *sb,
- struct buffer_head *bh);
+ struct buffer_head *bh);
+
+static bool ocfs2_valid_inode_mode(umode_t mode)
+{
+ return fs_umode_to_ftype(mode) != FT_UNKNOWN;
+}
void ocfs2_set_inode_flags(struct inode *inode)
{
@@ -1494,6 +1500,24 @@ int ocfs2_validate_inode_block(struct super_block *sb,
goto bail;
}
+ /*
+ * Reject dinodes whose i_mode does not name one of the seven
+ * canonical POSIX file types. ocfs2_populate_inode() copies
+ * i_mode verbatim into inode->i_mode and then dispatches via
+ * switch (mode & S_IFMT) to file/dir/symlink/special_file iops;
+ * an unrecognised type falls into ocfs2_special_file_iops with
+ * init_special_inode(), which interprets i_rdev. Constrain the
+ * type here so the dispatch only ever sees a value mkfs.ocfs2 /
+ * VFS can produce.
+ */
+ if (!ocfs2_valid_inode_mode(le16_to_cpu(di->i_mode))) {
+ rc = ocfs2_error(sb,
+ "Invalid dinode #%llu: mode 0%o has unknown file type\n",
+ (unsigned long long)bh->b_blocknr,
+ le16_to_cpu(di->i_mode));
+ goto bail;
+ }
+
if (le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL) {
struct ocfs2_inline_data *data = &di->id2.i_data;
@@ -1624,6 +1648,15 @@ static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
(unsigned long long)bh->b_blocknr,
le32_to_cpu(di->i_fs_generation));
rc = -OCFS2_FILECHECK_ERR_GENERATION;
+ goto bail;
+ }
+
+ if (!ocfs2_valid_inode_mode(le16_to_cpu(di->i_mode))) {
+ mlog(ML_ERROR,
+ "Filecheck: invalid dinode #%llu: mode 0%o has unknown file type\n",
+ (unsigned long long)bh->b_blocknr,
+ le16_to_cpu(di->i_mode));
+ rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
}
bail:
@@ -1812,4 +1845,3 @@ const struct ocfs2_caching_operations ocfs2_inode_caching_ops = {
.co_io_lock = ocfs2_inode_cache_io_lock,
.co_io_unlock = ocfs2_inode_cache_io_unlock,
};
-
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 2/3] ocfs2: reject dinodes whose i_rdev disagrees with the file type
2026-05-19 11:04 [PATCH v2 0/3] ocfs2: harden inode validators against forged metadata Michael Bommarito
2026-05-19 11:04 ` [PATCH v2 1/3] ocfs2: reject dinodes with non-canonical i_mode type Michael Bommarito
@ 2026-05-19 11:04 ` Michael Bommarito
2026-05-19 12:21 ` Joseph Qi
2026-06-01 18:21 ` Joel Becker
2026-05-19 11:04 ` [PATCH v2 3/3] ocfs2: reject non-inline dinodes with i_size and zero i_clusters Michael Bommarito
2 siblings, 2 replies; 9+ messages in thread
From: Michael Bommarito @ 2026-05-19 11:04 UTC (permalink / raw)
To: Joseph Qi, Mark Fasheh, Joel Becker
Cc: ZhengYuan Huang, ocfs2-devel, linux-fsdevel, linux-kernel
id1.dev1.i_rdev is the device-number arm of the ocfs2_dinode id1 union.
It is only meaningful for character and block device inodes. For any
other user-visible file type the on-disk value must be zero.
ocfs2_populate_inode() currently copies id1.dev1.i_rdev into
inode->i_rdev before the S_IFMT switch decides whether the inode is a
special file. A non-device inode with a non-zero i_rdev can therefore
publish stale or attacker-controlled device state into the in-core inode.
System inodes legitimately use other arms of the same union, so keep
the cross-check restricted to non-system inodes. Factor that predicate
into a helper and use it in both the normal validator and online
filecheck path; filecheck reports the malformed dinode through
OCFS2_FILECHECK_ERR_INVALIDINO instead of ocfs2_error().
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 | 55 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
index e149ccbdc03ce..992980ea98046 100644
--- a/fs/ocfs2/inode.c
+++ b/fs/ocfs2/inode.c
@@ -72,6 +72,16 @@ static bool ocfs2_valid_inode_mode(umode_t mode)
return fs_umode_to_ftype(mode) != FT_UNKNOWN;
}
+static bool ocfs2_dinode_has_unexpected_rdev(struct ocfs2_dinode *di)
+{
+ umode_t mode = le16_to_cpu(di->i_mode);
+
+ if (le32_to_cpu(di->i_flags) & OCFS2_SYSTEM_FL)
+ return false;
+
+ return !S_ISCHR(mode) && !S_ISBLK(mode) && di->id1.dev1.i_rdev != 0;
+}
+
void ocfs2_set_inode_flags(struct inode *inode)
{
unsigned int flags = OCFS2_I(inode)->ip_attr;
@@ -1518,6 +1528,41 @@ int ocfs2_validate_inode_block(struct super_block *sb,
goto bail;
}
+ /*
+ * id1.dev1.i_rdev is the device-number arm of the id1 union and
+ * is only meaningful for character and block device inodes. For
+ * any other regular user-visible file type the on-disk value
+ * must be zero. ocfs2_populate_inode() currently runs
+ *
+ * inode->i_rdev = huge_decode_dev(le64_to_cpu(fe->id1.dev1.i_rdev));
+ *
+ * unconditionally, before the S_IFMT switch decides whether the
+ * inode is a special file. As a result, an i_rdev value present
+ * on a non-device inode is silently published into the in-core
+ * inode; a subsequent forced re-read or in-core mode mutation
+ * (cluster peer with raw write access to the shared LUN,
+ * on-disk corruption, or a separately forged dinode) can then
+ * expose the attacker-controlled device number to
+ * init_special_inode() without ever showing an unusual i_mode
+ * at validation time.
+ *
+ * System inodes (OCFS2_SYSTEM_FL) legitimately use the bitmap1
+ * and journal1 arms of the same union (allocator i_used /
+ * i_total counters and the journal ij_flags /
+ * ij_recovery_generation pair); those bytes are not an i_rdev
+ * and must not be checked here. Restrict the cross-check to
+ * non-system inodes, which is the full attacker-controllable
+ * surface.
+ */
+ if (ocfs2_dinode_has_unexpected_rdev(di)) {
+ rc = ocfs2_error(sb,
+ "Invalid dinode #%llu: non-device mode 0%o with i_rdev %llu\n",
+ (unsigned long long)bh->b_blocknr,
+ le16_to_cpu(di->i_mode),
+ (unsigned long long)le64_to_cpu(di->id1.dev1.i_rdev));
+ goto bail;
+ }
+
if (le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL) {
struct ocfs2_inline_data *data = &di->id2.i_data;
@@ -1657,6 +1702,16 @@ static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
(unsigned long long)bh->b_blocknr,
le16_to_cpu(di->i_mode));
rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
+ goto bail;
+ }
+
+ if (ocfs2_dinode_has_unexpected_rdev(di)) {
+ mlog(ML_ERROR,
+ "Filecheck: invalid dinode #%llu: non-device mode 0%o with i_rdev %llu\n",
+ (unsigned long long)bh->b_blocknr,
+ le16_to_cpu(di->i_mode),
+ (unsigned long long)le64_to_cpu(di->id1.dev1.i_rdev));
+ rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
}
bail:
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 3/3] ocfs2: reject non-inline dinodes with i_size and zero i_clusters
2026-05-19 11:04 [PATCH v2 0/3] ocfs2: harden inode validators against forged metadata Michael Bommarito
2026-05-19 11:04 ` [PATCH v2 1/3] ocfs2: reject dinodes with non-canonical i_mode type Michael Bommarito
2026-05-19 11:04 ` [PATCH v2 2/3] ocfs2: reject dinodes whose i_rdev disagrees with the file type Michael Bommarito
@ 2026-05-19 11:04 ` Michael Bommarito
2026-05-19 12:22 ` Joseph Qi
2 siblings, 1 reply; 9+ messages in thread
From: Michael Bommarito @ 2026-05-19 11:04 UTC (permalink / raw)
To: Joseph Qi, Mark Fasheh, Joel Becker
Cc: ZhengYuan Huang, ocfs2-devel, linux-fsdevel, linux-kernel
On a volume mounted without OCFS2_FEATURE_INCOMPAT_SPARSE_ALLOC, a
non-inline regular file with non-zero i_size and zero i_clusters is
structurally malformed: the extent map declares no allocated clusters
yet the size header claims content exists. Keep rejecting that shape,
but express it through a shared predicate so the same invariant is
available to normal inode reads and online filecheck.
The same zero-cluster shape is also malformed for non-inline directories.
ocfs2 directory growth allocates backing storage before advancing
i_size, and ocfs2_dir_foreach_blk_el() later walks until ctx->pos reaches
i_size_read(inode). A forged directory dinode with a huge i_size and no
clusters would repeatedly fail on holes while advancing through the
claimed size.
Sparse regular files remain exempt: on sparse-alloc volumes, truncate can
legitimately grow i_size without allocating clusters. System inodes and
inline-data dinodes also retain their separate storage rules.
Mirror the check in ocfs2_filecheck_validate_inode_block() as well.
filecheck reports through its own error namespace, so malformed
size/cluster state is logged as a filecheck invalid-inode result rather
than via ocfs2_error(), but it must not proceed into
ocfs2_populate_inode().
Fixes: b657c95c1108 ("ocfs2: Wrap inode block reads in a dedicated function.")
Cc: stable@vger.kernel.org
Link: https://sashiko.dev/#/patchset/20260517111015.3187935-1-michael.bommarito%40gmail.com
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
Assisted-by: Claude:claude-opus-4-7
---
fs/ocfs2/inode.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
index 992980ea98046..432eac01c1763 100644
--- a/fs/ocfs2/inode.c
+++ b/fs/ocfs2/inode.c
@@ -82,6 +82,24 @@ static bool ocfs2_dinode_has_unexpected_rdev(struct ocfs2_dinode *di)
return !S_ISCHR(mode) && !S_ISBLK(mode) && di->id1.dev1.i_rdev != 0;
}
+static bool ocfs2_dinode_has_size_without_clusters(struct super_block *sb,
+ struct ocfs2_dinode *di)
+{
+ umode_t mode = le16_to_cpu(di->i_mode);
+
+ if (le32_to_cpu(di->i_flags) & OCFS2_SYSTEM_FL)
+ return false;
+ if (le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL)
+ return false;
+ if (!le64_to_cpu(di->i_size) || le32_to_cpu(di->i_clusters))
+ return false;
+
+ if (S_ISDIR(mode))
+ return true;
+
+ return !ocfs2_sparse_alloc(OCFS2_SB(sb)) && S_ISREG(mode);
+}
+
void ocfs2_set_inode_flags(struct inode *inode)
{
unsigned int flags = OCFS2_I(inode)->ip_attr;
@@ -1563,6 +1581,33 @@ int ocfs2_validate_inode_block(struct super_block *sb,
goto bail;
}
+ /*
+ * Non-inline directories must not have i_size without allocated
+ * clusters: directory growth adds storage before advancing i_size,
+ * and readdir walks i_size block-by-block. A forged directory
+ * with zero clusters and a huge i_size would repeatedly fault on
+ * holes while advancing through the claimed size.
+ *
+ * Non-inline regular files have the same invariant on non-sparse
+ * volumes. Sparse regular files are different: truncate can
+ * legitimately grow i_size without allocating clusters, so keep
+ * the sparse-alloc carveout for S_IFREG only. System inodes and
+ * inline-data dinodes have their own storage rules.
+ */
+ if (ocfs2_dinode_has_size_without_clusters(sb, di)) {
+ if (S_ISDIR(le16_to_cpu(di->i_mode)))
+ rc = ocfs2_error(sb,
+ "Invalid dinode #%llu: directory i_size %llu with i_clusters 0 and no inline-data flag\n",
+ (unsigned long long)bh->b_blocknr,
+ (unsigned long long)le64_to_cpu(di->i_size));
+ else
+ 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;
@@ -1712,6 +1757,21 @@ static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
le16_to_cpu(di->i_mode),
(unsigned long long)le64_to_cpu(di->id1.dev1.i_rdev));
rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
+ goto bail;
+ }
+
+ if (ocfs2_dinode_has_size_without_clusters(sb, di)) {
+ if (S_ISDIR(le16_to_cpu(di->i_mode)))
+ mlog(ML_ERROR,
+ "Filecheck: invalid dinode #%llu: directory i_size %llu with i_clusters 0 and no inline-data flag\n",
+ (unsigned long long)bh->b_blocknr,
+ (unsigned long long)le64_to_cpu(di->i_size));
+ else
+ mlog(ML_ERROR,
+ "Filecheck: 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));
+ rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
}
bail:
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v2 1/3] ocfs2: reject dinodes with non-canonical i_mode type
2026-05-19 11:04 ` [PATCH v2 1/3] ocfs2: reject dinodes with non-canonical i_mode type Michael Bommarito
@ 2026-05-19 12:21 ` Joseph Qi
2026-06-01 18:19 ` Joel Becker
1 sibling, 0 replies; 9+ messages in thread
From: Joseph Qi @ 2026-05-19 12:21 UTC (permalink / raw)
To: Michael Bommarito, akpm
Cc: ZhengYuan Huang, ocfs2-devel, linux-fsdevel, linux-kernel,
Mark Fasheh, Joel Becker
On 5/19/26 7:04 PM, Michael Bommarito wrote:
> ocfs2_validate_inode_block() currently accepts any non-zero i_mode value.
> ocfs2_populate_inode() then copies that mode verbatim into inode->i_mode
> and dispatches on i_mode & S_IFMT to the file/dir/symlink/special_file
> iops; an unrecognised type falls through to ocfs2_special_file_iops and
> init_special_inode().
>
> Reject dinodes whose type bits do not name one of the seven canonical
> POSIX file types. Use fs_umode_to_ftype(), the same generic file-type
> conversion helper OCFS2 already uses for directory entries, so the
> accepted inode type set matches the kernel file-type vocabulary instead
> of open-coding a local switch.
>
> Apply the same structural check to the online filecheck read path.
> filecheck keeps its own error namespace, so it reports malformed i_mode
> through the filecheck logger and OCFS2_FILECHECK_ERR_INVALIDINO instead
> of calling ocfs2_error(), but it must not allow a malformed dinode to
> proceed into ocfs2_populate_inode().
>
> Fixes: b657c95c1108 ("ocfs2: Wrap inode block reads in a dedicated function.")
> Cc: stable@vger.kernel.org
> Link: https://sashiko.dev/#/patchset/20260517111015.3187935-1-michael.bommarito%40gmail.com
> Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
> Assisted-by: Claude:claude-opus-4-7
Reviewed-by: Joseph Qi <joseph.qi@linux.alibaba.com>
> ---
> fs/ocfs2/inode.c | 36 ++++++++++++++++++++++++++++++++++--
> 1 file changed, 34 insertions(+), 2 deletions(-)
>
> diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
> index a510a0eb1adcc..e149ccbdc03ce 100644
> --- a/fs/ocfs2/inode.c
> +++ b/fs/ocfs2/inode.c
> @@ -13,6 +13,7 @@
> #include <linux/pagemap.h>
> #include <linux/quotaops.h>
> #include <linux/iversion.h>
> +#include <linux/fs_dirent.h>
>
> #include <asm/byteorder.h>
>
> @@ -64,7 +65,12 @@ static int ocfs2_filecheck_read_inode_block_full(struct inode *inode,
> static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
> struct buffer_head *bh);
> static int ocfs2_filecheck_repair_inode_block(struct super_block *sb,
> - struct buffer_head *bh);
> + struct buffer_head *bh);
> +
> +static bool ocfs2_valid_inode_mode(umode_t mode)
> +{
> + return fs_umode_to_ftype(mode) != FT_UNKNOWN;
> +}
>
> void ocfs2_set_inode_flags(struct inode *inode)
> {
> @@ -1494,6 +1500,24 @@ int ocfs2_validate_inode_block(struct super_block *sb,
> goto bail;
> }
>
> + /*
> + * Reject dinodes whose i_mode does not name one of the seven
> + * canonical POSIX file types. ocfs2_populate_inode() copies
> + * i_mode verbatim into inode->i_mode and then dispatches via
> + * switch (mode & S_IFMT) to file/dir/symlink/special_file iops;
> + * an unrecognised type falls into ocfs2_special_file_iops with
> + * init_special_inode(), which interprets i_rdev. Constrain the
> + * type here so the dispatch only ever sees a value mkfs.ocfs2 /
> + * VFS can produce.
> + */
> + if (!ocfs2_valid_inode_mode(le16_to_cpu(di->i_mode))) {
> + rc = ocfs2_error(sb,
> + "Invalid dinode #%llu: mode 0%o has unknown file type\n",
> + (unsigned long long)bh->b_blocknr,
> + le16_to_cpu(di->i_mode));
> + goto bail;
> + }
> +
> if (le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL) {
> struct ocfs2_inline_data *data = &di->id2.i_data;
>
> @@ -1624,6 +1648,15 @@ static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
> (unsigned long long)bh->b_blocknr,
> le32_to_cpu(di->i_fs_generation));
> rc = -OCFS2_FILECHECK_ERR_GENERATION;
> + goto bail;
> + }
> +
> + if (!ocfs2_valid_inode_mode(le16_to_cpu(di->i_mode))) {
> + mlog(ML_ERROR,
> + "Filecheck: invalid dinode #%llu: mode 0%o has unknown file type\n",
> + (unsigned long long)bh->b_blocknr,
> + le16_to_cpu(di->i_mode));
> + rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
> }
>
> bail:
> @@ -1812,4 +1845,3 @@ const struct ocfs2_caching_operations ocfs2_inode_caching_ops = {
> .co_io_lock = ocfs2_inode_cache_io_lock,
> .co_io_unlock = ocfs2_inode_cache_io_unlock,
> };
> -
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/3] ocfs2: reject dinodes whose i_rdev disagrees with the file type
2026-05-19 11:04 ` [PATCH v2 2/3] ocfs2: reject dinodes whose i_rdev disagrees with the file type Michael Bommarito
@ 2026-05-19 12:21 ` Joseph Qi
2026-06-01 18:21 ` Joel Becker
1 sibling, 0 replies; 9+ messages in thread
From: Joseph Qi @ 2026-05-19 12:21 UTC (permalink / raw)
To: Michael Bommarito, akpm
Cc: ZhengYuan Huang, ocfs2-devel, linux-fsdevel, linux-kernel,
Mark Fasheh, Joel Becker
On 5/19/26 7:04 PM, Michael Bommarito wrote:
> id1.dev1.i_rdev is the device-number arm of the ocfs2_dinode id1 union.
> It is only meaningful for character and block device inodes. For any
> other user-visible file type the on-disk value must be zero.
>
> ocfs2_populate_inode() currently copies id1.dev1.i_rdev into
> inode->i_rdev before the S_IFMT switch decides whether the inode is a
> special file. A non-device inode with a non-zero i_rdev can therefore
> publish stale or attacker-controlled device state into the in-core inode.
>
> System inodes legitimately use other arms of the same union, so keep
> the cross-check restricted to non-system inodes. Factor that predicate
> into a helper and use it in both the normal validator and online
> filecheck path; filecheck reports the malformed dinode through
> OCFS2_FILECHECK_ERR_INVALIDINO instead of ocfs2_error().
>
> 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
Reviewed-by: Joseph Qi <joseph.qi@linux.alibaba.com>
> ---
> fs/ocfs2/inode.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 55 insertions(+)
>
> diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
> index e149ccbdc03ce..992980ea98046 100644
> --- a/fs/ocfs2/inode.c
> +++ b/fs/ocfs2/inode.c
> @@ -72,6 +72,16 @@ static bool ocfs2_valid_inode_mode(umode_t mode)
> return fs_umode_to_ftype(mode) != FT_UNKNOWN;
> }
>
> +static bool ocfs2_dinode_has_unexpected_rdev(struct ocfs2_dinode *di)
> +{
> + umode_t mode = le16_to_cpu(di->i_mode);
> +
> + if (le32_to_cpu(di->i_flags) & OCFS2_SYSTEM_FL)
> + return false;
> +
> + return !S_ISCHR(mode) && !S_ISBLK(mode) && di->id1.dev1.i_rdev != 0;
> +}
> +
> void ocfs2_set_inode_flags(struct inode *inode)
> {
> unsigned int flags = OCFS2_I(inode)->ip_attr;
> @@ -1518,6 +1528,41 @@ int ocfs2_validate_inode_block(struct super_block *sb,
> goto bail;
> }
>
> + /*
> + * id1.dev1.i_rdev is the device-number arm of the id1 union and
> + * is only meaningful for character and block device inodes. For
> + * any other regular user-visible file type the on-disk value
> + * must be zero. ocfs2_populate_inode() currently runs
> + *
> + * inode->i_rdev = huge_decode_dev(le64_to_cpu(fe->id1.dev1.i_rdev));
> + *
> + * unconditionally, before the S_IFMT switch decides whether the
> + * inode is a special file. As a result, an i_rdev value present
> + * on a non-device inode is silently published into the in-core
> + * inode; a subsequent forced re-read or in-core mode mutation
> + * (cluster peer with raw write access to the shared LUN,
> + * on-disk corruption, or a separately forged dinode) can then
> + * expose the attacker-controlled device number to
> + * init_special_inode() without ever showing an unusual i_mode
> + * at validation time.
> + *
> + * System inodes (OCFS2_SYSTEM_FL) legitimately use the bitmap1
> + * and journal1 arms of the same union (allocator i_used /
> + * i_total counters and the journal ij_flags /
> + * ij_recovery_generation pair); those bytes are not an i_rdev
> + * and must not be checked here. Restrict the cross-check to
> + * non-system inodes, which is the full attacker-controllable
> + * surface.
> + */
> + if (ocfs2_dinode_has_unexpected_rdev(di)) {
> + rc = ocfs2_error(sb,
> + "Invalid dinode #%llu: non-device mode 0%o with i_rdev %llu\n",
> + (unsigned long long)bh->b_blocknr,
> + le16_to_cpu(di->i_mode),
> + (unsigned long long)le64_to_cpu(di->id1.dev1.i_rdev));
> + goto bail;
> + }
> +
> if (le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL) {
> struct ocfs2_inline_data *data = &di->id2.i_data;
>
> @@ -1657,6 +1702,16 @@ static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
> (unsigned long long)bh->b_blocknr,
> le16_to_cpu(di->i_mode));
> rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
> + goto bail;
> + }
> +
> + if (ocfs2_dinode_has_unexpected_rdev(di)) {
> + mlog(ML_ERROR,
> + "Filecheck: invalid dinode #%llu: non-device mode 0%o with i_rdev %llu\n",
> + (unsigned long long)bh->b_blocknr,
> + le16_to_cpu(di->i_mode),
> + (unsigned long long)le64_to_cpu(di->id1.dev1.i_rdev));
> + rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
> }
>
> bail:
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 3/3] ocfs2: reject non-inline dinodes with i_size and zero i_clusters
2026-05-19 11:04 ` [PATCH v2 3/3] ocfs2: reject non-inline dinodes with i_size and zero i_clusters Michael Bommarito
@ 2026-05-19 12:22 ` Joseph Qi
0 siblings, 0 replies; 9+ messages in thread
From: Joseph Qi @ 2026-05-19 12:22 UTC (permalink / raw)
To: Michael Bommarito, akpm
Cc: ZhengYuan Huang, ocfs2-devel, linux-fsdevel, linux-kernel,
Mark Fasheh, Joel Becker
On 5/19/26 7:04 PM, Michael Bommarito wrote:
> On a volume mounted without OCFS2_FEATURE_INCOMPAT_SPARSE_ALLOC, a
> non-inline regular file with non-zero i_size and zero i_clusters is
> structurally malformed: the extent map declares no allocated clusters
> yet the size header claims content exists. Keep rejecting that shape,
> but express it through a shared predicate so the same invariant is
> available to normal inode reads and online filecheck.
>
> The same zero-cluster shape is also malformed for non-inline directories.
> ocfs2 directory growth allocates backing storage before advancing
> i_size, and ocfs2_dir_foreach_blk_el() later walks until ctx->pos reaches
> i_size_read(inode). A forged directory dinode with a huge i_size and no
> clusters would repeatedly fail on holes while advancing through the
> claimed size.
>
> Sparse regular files remain exempt: on sparse-alloc volumes, truncate can
> legitimately grow i_size without allocating clusters. System inodes and
> inline-data dinodes also retain their separate storage rules.
>
> Mirror the check in ocfs2_filecheck_validate_inode_block() as well.
> filecheck reports through its own error namespace, so malformed
> size/cluster state is logged as a filecheck invalid-inode result rather
> than via ocfs2_error(), but it must not proceed into
> ocfs2_populate_inode().
>
> Fixes: b657c95c1108 ("ocfs2: Wrap inode block reads in a dedicated function.")
> Cc: stable@vger.kernel.org
> Link: https://sashiko.dev/#/patchset/20260517111015.3187935-1-michael.bommarito%40gmail.com
> Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
> Assisted-by: Claude:claude-opus-4-7
Reviewed-by: Joseph Qi <joseph.qi@linux.alibaba.com>
> ---
> fs/ocfs2/inode.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 60 insertions(+)
>
> diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
> index 992980ea98046..432eac01c1763 100644
> --- a/fs/ocfs2/inode.c
> +++ b/fs/ocfs2/inode.c
> @@ -82,6 +82,24 @@ static bool ocfs2_dinode_has_unexpected_rdev(struct ocfs2_dinode *di)
> return !S_ISCHR(mode) && !S_ISBLK(mode) && di->id1.dev1.i_rdev != 0;
> }
>
> +static bool ocfs2_dinode_has_size_without_clusters(struct super_block *sb,
> + struct ocfs2_dinode *di)
> +{
> + umode_t mode = le16_to_cpu(di->i_mode);
> +
> + if (le32_to_cpu(di->i_flags) & OCFS2_SYSTEM_FL)
> + return false;
> + if (le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL)
> + return false;
> + if (!le64_to_cpu(di->i_size) || le32_to_cpu(di->i_clusters))
> + return false;
> +
> + if (S_ISDIR(mode))
> + return true;
> +
> + return !ocfs2_sparse_alloc(OCFS2_SB(sb)) && S_ISREG(mode);
> +}
> +
> void ocfs2_set_inode_flags(struct inode *inode)
> {
> unsigned int flags = OCFS2_I(inode)->ip_attr;
> @@ -1563,6 +1581,33 @@ int ocfs2_validate_inode_block(struct super_block *sb,
> goto bail;
> }
>
> + /*
> + * Non-inline directories must not have i_size without allocated
> + * clusters: directory growth adds storage before advancing i_size,
> + * and readdir walks i_size block-by-block. A forged directory
> + * with zero clusters and a huge i_size would repeatedly fault on
> + * holes while advancing through the claimed size.
> + *
> + * Non-inline regular files have the same invariant on non-sparse
> + * volumes. Sparse regular files are different: truncate can
> + * legitimately grow i_size without allocating clusters, so keep
> + * the sparse-alloc carveout for S_IFREG only. System inodes and
> + * inline-data dinodes have their own storage rules.
> + */
> + if (ocfs2_dinode_has_size_without_clusters(sb, di)) {
> + if (S_ISDIR(le16_to_cpu(di->i_mode)))
> + rc = ocfs2_error(sb,
> + "Invalid dinode #%llu: directory i_size %llu with i_clusters 0 and no inline-data flag\n",
> + (unsigned long long)bh->b_blocknr,
> + (unsigned long long)le64_to_cpu(di->i_size));
> + else
> + 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;
>
> @@ -1712,6 +1757,21 @@ static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
> le16_to_cpu(di->i_mode),
> (unsigned long long)le64_to_cpu(di->id1.dev1.i_rdev));
> rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
> + goto bail;
> + }
> +
> + if (ocfs2_dinode_has_size_without_clusters(sb, di)) {
> + if (S_ISDIR(le16_to_cpu(di->i_mode)))
> + mlog(ML_ERROR,
> + "Filecheck: invalid dinode #%llu: directory i_size %llu with i_clusters 0 and no inline-data flag\n",
> + (unsigned long long)bh->b_blocknr,
> + (unsigned long long)le64_to_cpu(di->i_size));
> + else
> + mlog(ML_ERROR,
> + "Filecheck: 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));
> + rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
> }
>
> bail:
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 1/3] ocfs2: reject dinodes with non-canonical i_mode type
2026-05-19 11:04 ` [PATCH v2 1/3] ocfs2: reject dinodes with non-canonical i_mode type Michael Bommarito
2026-05-19 12:21 ` Joseph Qi
@ 2026-06-01 18:19 ` Joel Becker
1 sibling, 0 replies; 9+ messages in thread
From: Joel Becker @ 2026-06-01 18:19 UTC (permalink / raw)
To: Michael Bommarito
Cc: Joseph Qi, Mark Fasheh, ZhengYuan Huang, ocfs2-devel,
linux-fsdevel, linux-kernel
On Tue, May 19, 2026 at 07:04:02AM -0400, Michael Bommarito wrote:
> ocfs2_validate_inode_block() currently accepts any non-zero i_mode value.
> ocfs2_populate_inode() then copies that mode verbatim into inode->i_mode
> and dispatches on i_mode & S_IFMT to the file/dir/symlink/special_file
> iops; an unrecognised type falls through to ocfs2_special_file_iops and
> init_special_inode().
>
> Reject dinodes whose type bits do not name one of the seven canonical
> POSIX file types. Use fs_umode_to_ftype(), the same generic file-type
> conversion helper OCFS2 already uses for directory entries, so the
> accepted inode type set matches the kernel file-type vocabulary instead
> of open-coding a local switch.
>
> Apply the same structural check to the online filecheck read path.
> filecheck keeps its own error namespace, so it reports malformed i_mode
> through the filecheck logger and OCFS2_FILECHECK_ERR_INVALIDINO instead
> of calling ocfs2_error(), but it must not allow a malformed dinode to
> proceed into ocfs2_populate_inode().
>
> Fixes: b657c95c1108 ("ocfs2: Wrap inode block reads in a dedicated function.")
> Cc: stable@vger.kernel.org
> Link: https://sashiko.dev/#/patchset/20260517111015.3187935-1-michael.bommarito%40gmail.com
> Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
> Assisted-by: Claude:claude-opus-4-7
Reviewed-by: Joel Becker <jlbec@evilplan.org>
Side question: What "Assistance" did Claude provide? Was the snippet
generated entirely by prompt? No objection, just curious about the
workflow you're using.
Thanks,
Joel
--
"Born under a bad sign.
I been down since I began to crawl.
If it wasn't for bad luck,
I wouldn't have no luck at all."
http://www.jlbec.org/
jlbec@evilplan.org
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/3] ocfs2: reject dinodes whose i_rdev disagrees with the file type
2026-05-19 11:04 ` [PATCH v2 2/3] ocfs2: reject dinodes whose i_rdev disagrees with the file type Michael Bommarito
2026-05-19 12:21 ` Joseph Qi
@ 2026-06-01 18:21 ` Joel Becker
1 sibling, 0 replies; 9+ messages in thread
From: Joel Becker @ 2026-06-01 18:21 UTC (permalink / raw)
To: Michael Bommarito
Cc: Joseph Qi, Mark Fasheh, ZhengYuan Huang, ocfs2-devel,
linux-fsdevel, linux-kernel
On Tue, May 19, 2026 at 07:04:03AM -0400, Michael Bommarito wrote:
> id1.dev1.i_rdev is the device-number arm of the ocfs2_dinode id1 union.
> It is only meaningful for character and block device inodes. For any
> other user-visible file type the on-disk value must be zero.
>
> ocfs2_populate_inode() currently copies id1.dev1.i_rdev into
> inode->i_rdev before the S_IFMT switch decides whether the inode is a
> special file. A non-device inode with a non-zero i_rdev can therefore
> publish stale or attacker-controlled device state into the in-core inode.
>
> System inodes legitimately use other arms of the same union, so keep
> the cross-check restricted to non-system inodes. Factor that predicate
> into a helper and use it in both the normal validator and online
> filecheck path; filecheck reports the malformed dinode through
> OCFS2_FILECHECK_ERR_INVALIDINO instead of ocfs2_error().
>
> 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
Reviewed-by: Joel Becker <jlbec@evilplan.org>
> ---
> fs/ocfs2/inode.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 55 insertions(+)
>
> diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
> index e149ccbdc03ce..992980ea98046 100644
> --- a/fs/ocfs2/inode.c
> +++ b/fs/ocfs2/inode.c
> @@ -72,6 +72,16 @@ static bool ocfs2_valid_inode_mode(umode_t mode)
> return fs_umode_to_ftype(mode) != FT_UNKNOWN;
> }
>
> +static bool ocfs2_dinode_has_unexpected_rdev(struct ocfs2_dinode *di)
> +{
> + umode_t mode = le16_to_cpu(di->i_mode);
> +
> + if (le32_to_cpu(di->i_flags) & OCFS2_SYSTEM_FL)
> + return false;
> +
> + return !S_ISCHR(mode) && !S_ISBLK(mode) && di->id1.dev1.i_rdev != 0;
> +}
> +
> void ocfs2_set_inode_flags(struct inode *inode)
> {
> unsigned int flags = OCFS2_I(inode)->ip_attr;
> @@ -1518,6 +1528,41 @@ int ocfs2_validate_inode_block(struct super_block *sb,
> goto bail;
> }
>
> + /*
> + * id1.dev1.i_rdev is the device-number arm of the id1 union and
> + * is only meaningful for character and block device inodes. For
> + * any other regular user-visible file type the on-disk value
> + * must be zero. ocfs2_populate_inode() currently runs
> + *
> + * inode->i_rdev = huge_decode_dev(le64_to_cpu(fe->id1.dev1.i_rdev));
> + *
> + * unconditionally, before the S_IFMT switch decides whether the
> + * inode is a special file. As a result, an i_rdev value present
> + * on a non-device inode is silently published into the in-core
> + * inode; a subsequent forced re-read or in-core mode mutation
> + * (cluster peer with raw write access to the shared LUN,
> + * on-disk corruption, or a separately forged dinode) can then
> + * expose the attacker-controlled device number to
> + * init_special_inode() without ever showing an unusual i_mode
> + * at validation time.
> + *
> + * System inodes (OCFS2_SYSTEM_FL) legitimately use the bitmap1
> + * and journal1 arms of the same union (allocator i_used /
> + * i_total counters and the journal ij_flags /
> + * ij_recovery_generation pair); those bytes are not an i_rdev
> + * and must not be checked here. Restrict the cross-check to
> + * non-system inodes, which is the full attacker-controllable
> + * surface.
> + */
> + if (ocfs2_dinode_has_unexpected_rdev(di)) {
> + rc = ocfs2_error(sb,
> + "Invalid dinode #%llu: non-device mode 0%o with i_rdev %llu\n",
> + (unsigned long long)bh->b_blocknr,
> + le16_to_cpu(di->i_mode),
> + (unsigned long long)le64_to_cpu(di->id1.dev1.i_rdev));
> + goto bail;
> + }
> +
> if (le16_to_cpu(di->i_dyn_features) & OCFS2_INLINE_DATA_FL) {
> struct ocfs2_inline_data *data = &di->id2.i_data;
>
> @@ -1657,6 +1702,16 @@ static int ocfs2_filecheck_validate_inode_block(struct super_block *sb,
> (unsigned long long)bh->b_blocknr,
> le16_to_cpu(di->i_mode));
> rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
> + goto bail;
> + }
> +
> + if (ocfs2_dinode_has_unexpected_rdev(di)) {
> + mlog(ML_ERROR,
> + "Filecheck: invalid dinode #%llu: non-device mode 0%o with i_rdev %llu\n",
> + (unsigned long long)bh->b_blocknr,
> + le16_to_cpu(di->i_mode),
> + (unsigned long long)le64_to_cpu(di->id1.dev1.i_rdev));
> + rc = -OCFS2_FILECHECK_ERR_INVALIDINO;
> }
>
> bail:
> --
> 2.53.0
--
"Nearly all men can stand adversity, but if you really want to
test a man's character, give him power."
- Abraham Lincoln
http://www.jlbec.org/
jlbec@evilplan.org
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-06-01 18:21 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-19 11:04 [PATCH v2 0/3] ocfs2: harden inode validators against forged metadata Michael Bommarito
2026-05-19 11:04 ` [PATCH v2 1/3] ocfs2: reject dinodes with non-canonical i_mode type Michael Bommarito
2026-05-19 12:21 ` Joseph Qi
2026-06-01 18:19 ` Joel Becker
2026-05-19 11:04 ` [PATCH v2 2/3] ocfs2: reject dinodes whose i_rdev disagrees with the file type Michael Bommarito
2026-05-19 12:21 ` Joseph Qi
2026-06-01 18:21 ` Joel Becker
2026-05-19 11:04 ` [PATCH v2 3/3] ocfs2: reject non-inline dinodes with i_size and zero i_clusters Michael Bommarito
2026-05-19 12:22 ` Joseph Qi
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox