mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Yuejie Shi <syjcnss@gmail.com>
To: Russell King <rmk+kernel@armlinux.org.uk>,
	Christian Brauner <brauner@kernel.org>,
	Alexander Viro <viro@zeniv.linux.org.uk>
Cc: Bae Yeonju <iwasbaeyz@gmail.com>, Jan Kara <jack@suse.cz>,
	linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
	stable@vger.kernel.org
Subject: [PATCH] adfs: validate zone_spare in adfs_checkdiscrecord()
Date: Mon,  3 Aug 2026 11:26:06 +0800	[thread overview]
Message-ID: <20260803032606.81686-1-syjcnss@gmail.com> (raw)

The size of a free space map zone is derived from two on-disk fields:

	zone_size = (8 << dr->log2secsize) - le16_to_cpu(dr->zone_spare);

log2secsize is validated by adfs_checkdiscrecord() to be 8, 9 or 10, so
the left-hand term is at most 8192.  zone_spare is a plain 16-bit
on-disk field that nothing checks at all, so any value above the number
of bits in a zone makes this unsigned subtraction wrap to nearly 4G.

zone_size is then used as the scan limit of every map zone:

	dm[zone].dm_endbit = 32 + zone_size;

and both bitmap walkers use dm_endbit as the bound of a buffer_head that
is only (1 << log2secsize) bytes long:

	fragend = find_next_bit_le(map, endbit, start + idlen);

so lookup_zone() and scan_free_map() scan hundreds of megabytes past the
end of the map buffer.  The fragment end they come back with feeds
adfs_map_lookup()'s returned block number, so this is not only a crash:
it is an out-of-bounds read whose result steers which disc block is read
next.

The same expression also divides:

	asb->s_ids_per_zone = zone_size / (asb->s_idlen + 1);

and adfs_map_lookup() does "zone = frag_id / asb->s_ids_per_zone", so a
zone_spare that leaves fewer than idlen + 1 bits is a divide-by-zero as
well.

Reject both in adfs_checkdiscrecord(), which is the single gate both
adfs_validate_bblk() and adfs_validate_dr0() run before adfs_read_map()
is reached, by requiring zone_spare to leave room for at least one
fragment id.

Mounting a crafted image requires CAP_SYS_ADMIN in the initial user
namespace -- ADFS is FS_REQUIRES_DEV and not FS_USERNS_MOUNT -- so a
plain unprivileged local user cannot reach this.  The realistic threat
model is untrusted media and images: automounted removable media,
container/VM disk images opened by a privileged mounting agent, and
forensic or CI tooling.  Once mounted read-only, an ordinary
getdents64() is enough to fire it.

  # 4 GiB sparse image, log2secsize=10, nzones=2, zone_spare=9000
  mount -t adfs -o ro /dev/vda /mnt
  ls -la /mnt

  BUG: KASAN: use-after-free in _find_next_bit+0x68/0xd0
  Read of size 8 at addr ffff0000ccc6d000 by task ls/136
   __asan_load8+0xcc/0xd0
   _find_next_bit+0x68/0xd0
   adfs_map_lookup+0x1b8/0x378
   adfs_dir_read_buffers+0x178/0x2b0
   adfs_fplus_read+0x50/0x2c8
   adfs_dir_read_inode+0xa8/0x120
   adfs_iterate+0xf0/0x2c0
   iterate_dir+0x12c/0x400
   __arm64_sys_getdents64+0xf0/0x230

(KASAN calls it use-after-free only because the scan leaves the
buffer_head's page and lands in freed pages; there is no lifetime bug in
ADFS.)  Hundreds of further reports follow, including reads from inside
adfs_map_lookup() itself.

Real ADFS images use a small zone_spare -- a handful of bits of padding
per zone -- so this rejects nothing that mounts today.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Signed-off-by: Yuejie Shi <syjcnss@gmail.com>
---
 fs/adfs/super.c |  9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/fs/adfs/super.c b/fs/adfs/super.c
index a4cd0a5159dd..1ab810f96edb 100644
--- a/fs/adfs/super.c
+++ b/fs/adfs/super.c
@@ -82,6 +82,15 @@ static int adfs_checkdiscrecord(struct adfs_discrecord
 	if (dr->idlen > max_idlen)
 		return 1;
 
+	/*
+	 * zone_spare is subtracted from the number of bits in a map zone to
+	 * give the zone size, which is computed as an unsigned quantity and
+	 * is also the divisor for the number of ids per zone.  Require it to
+	 * leave room for at least one fragment id.
+	 */
+	if (le16_to_cpu(dr->zone_spare) > (8 << dr->log2secsize) - dr->idlen - 1)
+		return 1;
+
 	/* reserved bytes should be zero */
 	for (i = 0; i < sizeof(dr->unused52); i++)
 		if (dr->unused52[i] != 0)
--
2.51.0

                 reply	other threads:[~2026-08-03  3:26 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

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=20260803032606.81686-1-syjcnss@gmail.com \
    --to=syjcnss@gmail.com \
    --cc=brauner@kernel.org \
    --cc=iwasbaeyz@gmail.com \
    --cc=jack@suse.cz \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=rmk+kernel@armlinux.org.uk \
    --cc=stable@vger.kernel.org \
    --cc=viro@zeniv.linux.org.uk \
    /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

all inboxes | Powered by JetHome®