* [PATCH 0/2] jffs2: bound summary reads on crafted flash
@ 2026-04-15 12:48 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 ` [PATCH 2/2] jffs2: bound summary entry walks against the payload Michael Bommarito
0 siblings, 2 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
Hi,
Two mount-time out-of-bounds reads in fs/jffs2/summary.c that are
reachable when the kernel mounts a crafted JFFS2 flash image. Both
reproduced on v7.0-rc7 under UML with CONFIG_KASAN=y and
CONFIG_MTD_BLOCK2MTD=y; pre-fix each oopses in
jffs2_sum_scan_sumnode, post-fix the same images are rejected with a
warning and the scanner falls back to the full scan path.
1/2 -- jffs2_sum_scan_sumnode() computes
crc = crc32(0, summary->sum,
sumsize - sizeof(struct jffs2_raw_summary));
If a crafted on-flash jffs2_sum_marker.offset drives sumsize
below sizeof(struct jffs2_raw_summary) (= 32), the subtraction
underflows in size_t and crc32() walks ~16 EiB. The earlier
header reads of summary->totlen / ->hdr_crc / ->node_crc are
OOB for the same class of sumsize values. Bound sumsize at
JFFS2_SUMMARY_FRAME_SIZE (header + marker = 40) which is the
minimum frame the writer at jffs2_sum_write_sumnode() emits.
KASAN evidence:
BUG: KASAN: slab-out-of-bounds in
jffs2_sum_scan_sumnode+0x131/0x1611
Read of size 4 at addr 00000000621fb004 by task mount/31
Located 4 bytes to the right of 4096-byte region
2/2 -- jffs2_sum_process_sum_data() iterates summary->sum_num times
with no bounds check on the remaining payload. Crafted
sum_num > (actual entries) walks sp off the summary buffer;
nodetype is then read from adjacent slab memory, and if those
bytes decode as one of the known case labels the handler
calls sum_link_node_ref() with offset/totlen pulled from the
OOB bytes. Pass sumsize into the helper and bound sp before
every nodetype read and every type-specific field access.
KASAN evidence (patch 1 applied so the bug is reached):
BUG: KASAN: slab-out-of-bounds in
jffs2_sum_scan_sumnode+0x6bd/0x16bf
Read of size 2 at addr 00000000621fb000 by task mount/31
Located 0 bytes to the right of 4096-byte region
A matching sum_num=1 image (same bytes, honest sum_num) does
not splat.
Impact:
Mount-time only, CAP_SYS_ADMIN required to attach the MTD and
call mount(2). Not reachable from unprivileged users, user
namespaces, FUSE, or network. Relevant practically on embedded
devices that auto-mount JFFS2 on boot when the flash is writable
out-of-band.
1/2 is an OOB read / DoS on mount.
2/2 is not just an OOB read: the type-specific handlers run past
the buffer boundary before sp is bounded, so corrupted in-memory
jeb state can persist past the faulting iteration rather than
cleanly oopsing. Closing the bound prevents that sequence. No
controlled kernel write, no RCE primitive in evidence.
Reproduction artefacts (craft scripts, UML init, pre/post KASAN
logs) are on the reporter side on request.
Thanks,
Mike
Michael Bommarito (2):
jffs2: reject truncated summary node before header validation
jffs2: bound summary entry walks against the payload
fs/jffs2/summary.c | 44 +++++++++++++++++++++++++++++++++++++++++---
1 file changed, 41 insertions(+), 3 deletions(-)
--
2.53.0
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
^ permalink raw reply [flat|nested] 3+ messages in thread
* [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
end of thread, other threads:[~2026-04-15 12:48 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH 2/2] jffs2: bound summary entry walks against the payload Michael Bommarito
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox