[PATCH] adfs: bound the F+ directory entry name length
Yuejie Shi <[email protected]> Mon, 3 Aug 2026 11:24:14 +0800
| Newsgroups | org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel,org.kernel.vger.stable |
|---|---|
| Message-ID | <[email protected]> |
adfs_fplus_getnext() copies a directory entry name into
struct object_info::name, which is a fixed char[ADFS_MAX_NAME_LEN]
(260 bytes) living on adfs_fplus_iterate()'s stack:
obj->name_len = le32_to_cpu(bde.bigdirobnamelen);
offset = adfs_fplus_offset(h, le32_to_cpu(h->bigdirentries));
offset += le32_to_cpu(bde.bigdirobnameptr);
ret = adfs_dir_copyfrom(obj->name, dir, offset, obj->name_len);
bigdirobnamelen is a raw on-disk __le32 and is not checked against
anything -- not against the 260-byte destination, not against
ADFS_FPLUS_NAME_LEN (255), not even against bigdirnamesize.
adfs_fplus_validate_header() checks bigdirnamelen, bigdirnamesize and
bigdirentries, but nothing validates the per-entry name length.
adfs_dir_copyfrom() does not save us either. It bounds only the source,
and even that bound is incomplete: the test
if (index + (remain < len) >= dir->nr_buffers)
return -EINVAL;
covers at most one extra buffer, while the tail
memcpy(dst, dir->bhs[index]->b_data + offset, len);
is not capped at the remaining size of that buffer. So a large
bigdirobnamelen is an out-of-bounds read of the directory buffer_heads
*and* a linear out-of-bounds write of the same, attacker-chosen, 32-bit
length past object_info::name on the kernel stack.
adfs_object_fixup() then makes it slightly worse: it loops over the same
name_len, and with the "ftsuffix" mount option appends four more bytes at
obj->name[obj->name_len++], which overflows even for name_len == 260.
Reject a name length that cannot fit, and while here make
adfs_dir_copyfrom() check the whole requested range against the
directory's buffers and copy through a loop, so no single memcpy can run
off the end of a buffer_head.
Mounting a crafted image requires CAP_SYS_ADMIN in the initial user
namespace -- ADFS is FS_REQUIRES_DEV and not FS_USERNS_MOUNT -- so this
is not reachable by a plain unprivileged local user. It matters for the
usual untrusted-media paths: automounted removable media, disk images
handed to a privileged mounting agent (container/VM image tooling), and
forensic or CI systems that mount images to inspect them. After the
mount, the trigger is a single getdents64() on the root directory, i.e.
"ls /mnt".
losetup /dev/loop0 adfs-w4.img # F+ image, bigdirobnamelen=300
mount -t adfs -o ro /dev/loop0 /mnt
ls -la /mnt
BUG: KASAN: stack-out-of-bounds in adfs_dir_copyfrom+0xcc/0x150
Write of size 300 at addr ffff80008ac6799c by task ls/132
__asan_memcpy+0x54/0xa0
adfs_dir_copyfrom+0xcc/0x150
adfs_fplus_getnext+0x200/0x240
adfs_fplus_iterate+0x144/0x1b8
adfs_iterate+0x12c/0x2c0
iterate_dir+0x12c/0x400
__arm64_sys_getdents64+0xf0/0x230
followed by cascading stack-out-of-bounds reports in adfs_object_fixup()
and filldir64() walking the smashed frame. 300 is only the smallest
value that demonstrates it; the field is 32 bits wide.
Valid F+ directories are unaffected: RISC OS caps an F+ object name at
ADFS_FPLUS_NAME_LEN (255) bytes, which still leaves room for the four
byte ",xyz" filetype suffix inside ADFS_MAX_NAME_LEN, and the reworked
adfs_dir_copyfrom() copies exactly the same bytes as before for any
request that was in bounds.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: [email protected]
Signed-off-by: Yuejie Shi <[email protected]>
---
fs/adfs/dir.c | 15 +++++++++------
fs/adfs/dir_fplus.c | 2 ++
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/fs/adfs/dir.c b/fs/adfs/dir.c
index 11afa9e157aa..44953247acad 100644
--- a/fs/adfs/dir.c
+++ b/fs/adfs/dir.c
@@ -19,15 +19,20 @@ int adfs_dir_copyfrom(void *dst, struct adfs_dir *dir,
size_t len)
{
struct super_block *sb = dir->sb;
+ size_t size = (size_t)dir->nr_buffers << sb->s_blocksize_bits;
unsigned int index, remain;
+ if (offset >= size || len > size - offset)
+ return -EINVAL;
+
index = offset >> sb->s_blocksize_bits;
offset &= sb->s_blocksize - 1;
- remain = sb->s_blocksize - offset;
- if (index + (remain < len) >= dir->nr_buffers)
- return -EINVAL;
- if (remain < len) {
+ while (len) {
+ remain = sb->s_blocksize - offset;
+ if (remain > len)
+ remain = len;
+
memcpy(dst, dir->bhs[index]->b_data + offset, remain);
dst += remain;
len -= remain;
@@ -35,8 +40,6 @@ int adfs_dir_copyfrom(void *dst, struct adfs_dir *dir,
offset = 0;
}
- memcpy(dst, dir->bhs[index]->b_data + offset, len);
-
return 0;
}
diff --git a/fs/adfs/dir_fplus.c b/fs/adfs/dir_fplus.c
index 4a15924014da..517ffcc91429 100644
--- a/fs/adfs/dir_fplus.c
+++ b/fs/adfs/dir_fplus.c
@@ -192,6 +192,8 @@ adfs_fplus_getnext(struct adfs_dir *dir, struct object
obj->indaddr = le32_to_cpu(bde.bigdirindaddr);
obj->attr = le32_to_cpu(bde.bigdirattr);
obj->name_len = le32_to_cpu(bde.bigdirobnamelen);
+ if (obj->name_len > ADFS_FPLUS_NAME_LEN)
+ return -EIO;
offset = adfs_fplus_offset(h, le32_to_cpu(h->bigdirentries));
offset += le32_to_cpu(bde.bigdirobnameptr);
--
2.51.0