[PATCH] adfs: validate zone_spare in adfs_checkdiscrecord()

Yuejie Shi <[email protected]>
Newsgroups gmane.linux.kernel.stable,gmane.linux.file-systems,gmane.linux.kernel
Message-ID <[email protected]>
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: [email protected]
Signed-off-by: Yuejie Shi <[email protected]>
---
 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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.