* [PATCH 1/2] jffs2: reject truncated summary node before header validation
2026-04-15 12:48 [PATCH 0/2] jffs2: bound summary reads on crafted flash Michael Bommarito
@ 2026-04-15 12:48 ` Michael Bommarito
2026-04-15 12:48 ` [PATCH 2/2] jffs2: bound summary entry walks against the payload Michael Bommarito
1 sibling, 0 replies; 3+ messages in thread
From: Michael Bommarito @ 2026-04-15 12:48 UTC (permalink / raw)
To: linux-mtd, David Woodhouse, Richard Weinberger
Cc: Zhihao Cheng, Artem Sadovnikov, Kees Cook, linux-kernel
jffs2_sum_scan_sumnode() is called from jffs2_scan_eraseblock() with
sumsize derived from the on-flash jffs2_sum_marker::offset:
sumlen = c->sector_size - je32_to_cpu(sm->offset);
A crafted flash image can set sm->offset so that
sumsize < JFFS2_SUMMARY_FRAME_SIZE
(= sizeof(struct jffs2_raw_summary) + sizeof(struct jffs2_sum_marker)
= 40, the minimum frame the writer at jffs2_sum_write_sumnode() emits
and the minimum sumlen that corresponds to a legitimate on-flash
layout). The function then reads the summary header unchecked:
crcnode.totlen = summary->totlen; /* offset +4 */
crc = crc32(0, &crcnode, sizeof(crcnode)-4);
if (je32_to_cpu(summary->hdr_crc) != crc) /* offset +8 */
goto crc_err;
if (je32_to_cpu(summary->totlen) != sumsize)
goto crc_err;
crc = crc32(0, summary, sizeof(struct jffs2_raw_summary)-8);
if (je32_to_cpu(summary->node_crc) != crc) /* offset +28 */
goto crc_err;
crc = crc32(0, summary->sum,
sumsize - sizeof(struct jffs2_raw_summary));
Each header read at offset +4, +8 and +28 of a too-small buffer is a
slab out-of-bounds read. Worse, sumsize - sizeof(struct
jffs2_raw_summary) underflows in size_t and the final crc32() walks
~16 EiB of memory, which translates to a kernel oops on mount once the
walk hits unmapped memory.
Reachable whenever a crafted JFFS2 flash image is mounted: typical in
embedded systems where flash can be rewritten out-of-band (JTAG, SPI
flasher, hostile firmware update) and the device auto-mounts JFFS2 on
boot, or any CAP_SYS_ADMIN context that supplies the MTD backing.
Bounding on JFFS2_SUMMARY_FRAME_SIZE matches the actual on-flash frame
layout the writer emits and does not reject any legitimate image.
Reproduced on v7.0-rc7 under UML + CONFIG_KASAN=y with a 16 MiB
block2mtd-backed image whose first erase block's jffs2_sum_marker
points at sector_size; pre-fix:
BUG: KASAN: slab-out-of-bounds in jffs2_sum_scan_sumnode+0x131/0x1611
Read of size 4 at addr 00000000621fb004 by task mount/31
Allocated by mtd_kmalloc_up_to via jffs2_scan_medium+0x246
Located 4 bytes to the right of allocated 4096-byte region
Post-fix the same image is rejected cleanly with a warning and mount
falls back to the full scan path.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
fs/jffs2/summary.c | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/fs/jffs2/summary.c b/fs/jffs2/summary.c
index 4521a7723f30..150a9c83cb05 100644
--- a/fs/jffs2/summary.c
+++ b/fs/jffs2/summary.c
@@ -577,6 +577,15 @@ int jffs2_sum_scan_sumnode(struct jffs2_sb_info *c, struct jffs2_eraseblock *jeb
int ret, ofs;
uint32_t crc;
+ /* Reject frames that can't hold the header + marker the writer
+ * always emits (also blocks the sumsize - sizeof(*summary)
+ * size_t underflow at the sum_crc check below). */
+ if (sumsize < JFFS2_SUMMARY_FRAME_SIZE) {
+ JFFS2_WARNING("Summary node too small (%u bytes), skipping.\n",
+ sumsize);
+ return 0;
+ }
+
ofs = c->sector_size - sumsize;
dbg_summary("summary found for 0x%08x at 0x%08x (0x%x bytes)\n",
--
2.53.0
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply related [flat|nested] 3+ messages in thread* [PATCH 2/2] jffs2: bound summary entry walks against the payload
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
1 sibling, 0 replies; 3+ messages in thread
From: Michael Bommarito @ 2026-04-15 12:48 UTC (permalink / raw)
To: linux-mtd, David Woodhouse, Richard Weinberger
Cc: Zhihao Cheng, Artem Sadovnikov, Kees Cook, linux-kernel
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/
^ permalink raw reply related [flat|nested] 3+ messages in thread