public inbox for linux-mtd@lists.infradead.org
 help / color / mirror / Atom feed
From: Michael Bommarito <michael.bommarito@gmail.com>
To: linux-mtd@lists.infradead.org,
	David Woodhouse <dwmw2@infradead.org>,
	Richard Weinberger <richard@nod.at>
Cc: Zhihao Cheng <chengzhihao1@huawei.com>,
	Artem Sadovnikov <a.sadovnikov@ispras.ru>,
	Kees Cook <kees@kernel.org>,
	linux-kernel@vger.kernel.org
Subject: [PATCH 2/2] jffs2: bound summary entry walks against the payload
Date: Wed, 15 Apr 2026 08:48:13 -0400	[thread overview]
Message-ID: <20260415124813.246588-3-michael.bommarito@gmail.com> (raw)
In-Reply-To: <20260415124813.246588-1-michael.bommarito@gmail.com>

jffs2_sum_process_sum_data() iterates summary->sum_num times, reading
the next entry's nodetype from the current sp and dispatching into
type-specific handlers that advance sp by a fixed or nsize-dependent
amount.  There is no upper bound on sum_num from the writer side, and
on read the scanner trusts the on-flash value unchecked.

A crafted flash image can therefore set sum_num > (actual entries
that fit in the payload).  Once sp runs off the end of the summary
buffer the nodetype read at summary.c:407 lands on adjacent slab
memory.  If those bytes happen to decode as one of the known types
(JFFS2_NODETYPE_INODE / _DIRENT / _XATTR / _XREF) the handler calls
sum_link_node_ref() with offset / totlen pulled from whatever slab
neighbor is next to the scan buffer.

Reproduced on v7.0-rc7 under UML + CONFIG_KASAN=y with a crafted
image carrying one real INODE entry and sum_num=2:

  BUG: KASAN: slab-out-of-bounds in jffs2_sum_scan_sumnode+0x6bd
  Read of size 2 at addr 00000000621fb000 by task mount/31
  Located 0 bytes to the right of allocated 4096-byte region

The matching sum_num=1 image (same bytes, honest sum_num) mounts
without a KASAN report, so the OOB is sum_num-specific.

Pass sumsize into jffs2_sum_process_sum_data() and bound sp against
summary + sumsize - sizeof(struct jffs2_sum_marker) before every
nodetype read and before every type-specific field access.  If the
advance would leave the payload, warn and fall back to a full scan
via -ENOTRECOVERABLE.

Scope note on impact: demonstrated effect is a mount-time OOB read
and a default-case warning path that reclaims the jeb.  The
type-specific handlers run with attacker-influenced offset/totlen
pulled from the OOB bytes and do call sum_link_node_ref(), but
persistent write/state-corruption requires adjacent slab content to
decode as a known nodetype and the mount to complete cleanly;
neither is reliably reproducible without heap-spray primitives.
This patch closes the confirmed OOB-read sites.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
 fs/jffs2/summary.c | 35 ++++++++++++++++++++++++++++++++---
 1 file changed, 32 insertions(+), 3 deletions(-)

diff --git a/fs/jffs2/summary.c b/fs/jffs2/summary.c
index 150a9c83cb05..09677b931010 100644
--- a/fs/jffs2/summary.c
+++ b/fs/jffs2/summary.c
@@ -384,21 +384,33 @@ static struct jffs2_raw_node_ref *sum_link_node_ref(struct jffs2_sb_info *c,
 /* Process the stored summary information - helper function for jffs2_sum_scan_sumnode() */
 
 static int jffs2_sum_process_sum_data(struct jffs2_sb_info *c, struct jffs2_eraseblock *jeb,
-				struct jffs2_raw_summary *summary, uint32_t *pseudo_random)
+				struct jffs2_raw_summary *summary, uint32_t sumsize,
+				uint32_t *pseudo_random)
 {
 	struct jffs2_inode_cache *ic;
 	struct jffs2_full_dirent *fd;
-	void *sp;
+	void *sp, *sum_end;
 	int i, ino;
 	int err;
 
 	sp = summary->sum;
+	/* Entries must fit before the trailing jffs2_sum_marker. */
+	sum_end = (char *)summary + sumsize - sizeof(struct jffs2_sum_marker);
 
 	for (i=0; i<je32_to_cpu(summary->sum_num); i++) {
 		dbg_summary("processing summary index %d\n", i);
 
 		cond_resched();
 
+		/* Make sure the nodetype dispatched on is in-bounds; each
+		 * case re-checks the specific entry size before advancing
+		 * sp past the node's fields. */
+		if ((char *)sp + sizeof(struct jffs2_sum_unknown_flash) > (char *)sum_end) {
+			JFFS2_WARNING("Summary entry %d nodetype past payload (sum_num=%u)\n",
+				      i, je32_to_cpu(summary->sum_num));
+			return -ENOTRECOVERABLE;
+		}
+
 		/* Make sure there's a spare ref for dirty space */
 		err = jffs2_prealloc_raw_node_refs(c, jeb, 2);
 		if (err)
@@ -407,6 +419,9 @@ static int jffs2_sum_process_sum_data(struct jffs2_sb_info *c, struct jffs2_eras
 		switch (je16_to_cpu(((struct jffs2_sum_unknown_flash *)sp)->nodetype)) {
 			case JFFS2_NODETYPE_INODE: {
 				struct jffs2_sum_inode_flash *spi;
+
+				if ((char *)sp + JFFS2_SUMMARY_INODE_SIZE > (char *)sum_end)
+					goto ent_past_end;
 				spi = sp;
 
 				ino = je32_to_cpu(spi->inode);
@@ -434,7 +449,12 @@ static int jffs2_sum_process_sum_data(struct jffs2_sb_info *c, struct jffs2_eras
 			case JFFS2_NODETYPE_DIRENT: {
 				struct jffs2_sum_dirent_flash *spd;
 				int checkedlen;
+
+				if ((char *)sp + sizeof(*spd) > (char *)sum_end)
+					goto ent_past_end;
 				spd = sp;
+				if ((char *)sp + JFFS2_SUMMARY_DIRENT_SIZE(spd->nsize) > (char *)sum_end)
+					goto ent_past_end;
 
 				dbg_summary("Dirent at 0x%08x-0x%08x\n",
 					    jeb->offset + je32_to_cpu(spd->offset),
@@ -492,6 +512,8 @@ static int jffs2_sum_process_sum_data(struct jffs2_sb_info *c, struct jffs2_eras
 				struct jffs2_xattr_datum *xd;
 				struct jffs2_sum_xattr_flash *spx;
 
+				if ((char *)sp + JFFS2_SUMMARY_XATTR_SIZE > (char *)sum_end)
+					goto ent_past_end;
 				spx = (struct jffs2_sum_xattr_flash *)sp;
 				dbg_summary("xattr at %#08x-%#08x (xid=%u, version=%u)\n", 
 					    jeb->offset + je32_to_cpu(spx->offset),
@@ -523,6 +545,8 @@ static int jffs2_sum_process_sum_data(struct jffs2_sb_info *c, struct jffs2_eras
 				struct jffs2_xattr_ref *ref;
 				struct jffs2_sum_xref_flash *spr;
 
+				if ((char *)sp + JFFS2_SUMMARY_XREF_SIZE > (char *)sum_end)
+					goto ent_past_end;
 				spr = (struct jffs2_sum_xref_flash *)sp;
 				dbg_summary("xref at %#08x-%#08x\n",
 					    jeb->offset + je32_to_cpu(spr->offset),
@@ -566,6 +590,11 @@ static int jffs2_sum_process_sum_data(struct jffs2_sb_info *c, struct jffs2_eras
 		}
 	}
 	return 0;
+
+ent_past_end:
+	JFFS2_WARNING("Summary entry %d past payload end (sum_num=%u)\n",
+		      i, je32_to_cpu(summary->sum_num));
+	return -ENOTRECOVERABLE;
 }
 
 /* Process the summary node - called from jffs2_scan_eraseblock() */
@@ -646,7 +675,7 @@ int jffs2_sum_scan_sumnode(struct jffs2_sb_info *c, struct jffs2_eraseblock *jeb
 		}
 	}
 
-	ret = jffs2_sum_process_sum_data(c, jeb, summary, pseudo_random);
+	ret = jffs2_sum_process_sum_data(c, jeb, summary, sumsize, pseudo_random);
 	/* -ENOTRECOVERABLE isn't a fatal error -- it means we should do a full
 	   scan of this eraseblock. So return zero */
 	if (ret == -ENOTRECOVERABLE)
-- 
2.53.0


______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/

      parent reply	other threads:[~2026-04-15 12:48 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-15 12:48 [PATCH 0/2] jffs2: bound summary reads on crafted flash Michael Bommarito
2026-04-15 12:48 ` [PATCH 1/2] jffs2: reject truncated summary node before header validation Michael Bommarito
2026-04-15 12:48 ` Michael Bommarito [this message]

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=20260415124813.246588-3-michael.bommarito@gmail.com \
    --to=michael.bommarito@gmail.com \
    --cc=a.sadovnikov@ispras.ru \
    --cc=chengzhihao1@huawei.com \
    --cc=dwmw2@infradead.org \
    --cc=kees@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mtd@lists.infradead.org \
    --cc=richard@nod.at \
    /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