From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-qk1-f172.google.com (mail-qk1-f172.google.com [209.85.222.172]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 5BD984A07 for ; Wed, 15 Apr 2026 12:48:32 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.222.172 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776257316; cv=none; b=jyYws/11n912TGwdlek8o/FyYX98MNwckAcLcQTECkWd6gOPk2eslmU0K30LabmKjUtnH5EicCdGZ6QAq5qspPUMKU4qWjEY4ggxnYyhG0ab2v5s96jcdXNcM33sfXLpepKD3i53sfvPQnmDKbubgw4E1BLCXaXdb4P7kXiebH4= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776257316; c=relaxed/simple; bh=cfV+tXFX3fbRcKkidMaCzHWx7xRYreVRzrk/zy3ZYEk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=SLq5e7fYHIebIeFOfLiFl59KP2Axq8b0zgknD0bbt1Uuc+8wTRANak/agAVw4tg3EyggSqPWu63dOY06ZecbHmJep80WhpmPXAj+nGWMFX78nz7BDq7nKwD10t0mVuCtk6jxzHAfyfHffFjWDrrDrEWgsMlC6hFLd0oJsOTZ/C0= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=r4ri8vwE; arc=none smtp.client-ip=209.85.222.172 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="r4ri8vwE" Received: by mail-qk1-f172.google.com with SMTP id af79cd13be357-8d7e7f48499so657824685a.1 for ; Wed, 15 Apr 2026 05:48:31 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1776257310; x=1776862110; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=kjxJXStMU5U0NlRTJ2IsoioP7S3PUNh6UBqJINw6hTA=; b=r4ri8vwEymcOJwmpiR8F/al4Idc4y+8EcPIdm2WLI/RtTGGOUSLufgj6ciJ8ATUSja LGBaCHHavEEpqSLDMEuO+BZHwrpiHXeZLLQcMAZvXkp8jbk/VPCMYUtdQ8LL/WTluDYm Yyz4Jm0ID419SGZysy6PdgO+t8Rjozb8RRzj4tVPD3S4vjZQCyIbFMruKYByAOKyNM/j hGoSDSLUcMPPUtuYZ3gqbTNUIJIUNJrP4aWBRjdUYLHnUWAydM6nx5p5QCWwx9eAyDLL pTk6DX0I/+ObWSdYtJgZWJqXibwGBvZNFIZvN45zVtIX/7c7cXT3XsuLXH3LGexAs5t7 AqAA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1776257310; x=1776862110; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=kjxJXStMU5U0NlRTJ2IsoioP7S3PUNh6UBqJINw6hTA=; b=iC21SLkZvesTa9PNFK1983dpiZbcbOFCrnitwuNSEV6q1D+ppN03qF0U8FiUWolCrl PXgdY2zAsTiFPsOfyoJOT4Y1XV1hTid8twuxP8vbmgjDPMrWo4i3nsU0uWt8t0oQg1de TTE/zCjoLP/vtg8ixWqwH/SXgonRsWo4MZ+pRSwyi5A3TjBbG2EcqpYtFSyGjtOTNcC+ Kq63Wfm5Jxwvy9TjwqvxKRCUJCBdux48y00sBi+wTR5Uyt9HHg7nn06U5PzloxrHZSFW ZVl4Auc7HRoBHNICGmqS9LoI+AfspFcOixEBPE3f4oA4IqK3uIHNPYUOxYKW6SPMe+Yc j7Ig== X-Forwarded-Encrypted: i=1; AFNElJ+++YXLHV2ZSQe+PfdAvlzoxB5y3yb5SkxH1cwrDZ6sJgGkssqcmaVnjpgv92JBeaKls9ooNqRIjyUPJ8s=@vger.kernel.org X-Gm-Message-State: AOJu0Yy7jwmDx2Le03cIAwyc0d5hfvhT4Ps6orG2cjMaZTSUQ8BTY1lu 4e20cvoB+SJtK1olH6Dqf5RuZHJgteWcoptG967Rpha/eHf61SQ9TsEl5HLT4A== X-Gm-Gg: AeBDiev6q6PQejLjcvQJwLGlXXIXBnoWBw/VeidkcZyZVXJlgQMAb/HlbCBHW6xcyYz Y6dVZ6faReof7dRadBOWCddd+eCaHhxmooUB8v1N0Z7mmJOf1uy4dgd/ZuHcmNvmp8Jw6/bfRrg ram3FCYCh/tMjBAwW303mZvvUE7qCNzz1/iCOqneb1u2QNzu68W5EQK647lxPnSiY82hY7lmHS6 21NsuJJQtgmZxr7Tenom4DeplCf7p1qC3sR5BKQtWlwWaUVRoRl++Bzj5NjqPJzO5Kl1ipxVxkD p/6WIqoOC0YRXC0sgcc4Mj/ysveXbLDGzCYBA+ggb5ps78vMTTZ97p5+e1a3TJeclbODxHkMVFn g2IcX+FGPqYTo3dWobfjulu3emtfti9BvE/1DEDcf+YAcgLpKFEh4WXat/sueRfiVj9zZf3pfh9 imWszwkiURa3dzNdSe9kwsSZO6/wWxxZRFWfGLVVka9AnNNJ2SqVx1HdHg8tJpIHyGYN6i4LpoO dQJLDEVYe17DIXNOojwDS1UypIVcCMVBEek5ZauQDA8lvcUy0s4xQ== X-Received: by 2002:a05:620a:450b:b0:8cd:b620:f3ed with SMTP id af79cd13be357-8ddcf2bf6e4mr3124006385a.38.1776257310347; Wed, 15 Apr 2026 05:48:30 -0700 (PDT) Received: from server0.tail6e7dd.ts.net (c-68-48-65-54.hsd1.mi.comcast.net. [68.48.65.54]) by smtp.gmail.com with ESMTPSA id 6a1803df08f44-8ae6ceb891csm10614016d6.48.2026.04.15.05.48.29 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 15 Apr 2026 05:48:29 -0700 (PDT) From: Michael Bommarito To: linux-mtd@lists.infradead.org, David Woodhouse , Richard Weinberger Cc: Zhihao Cheng , Artem Sadovnikov , Kees Cook , 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 Message-ID: <20260415124813.246588-3-michael.bommarito@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260415124813.246588-1-michael.bommarito@gmail.com> References: <20260415124813.246588-1-michael.bommarito@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit 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 --- 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; isum_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