[PATCH] squashfs: bound the fragment offset stored in an inode

From: Yuejie Shi

Date: Sun Aug 02 2026 - 23:35:03 EST


Both the REG and LREG inode readers take the fragment offset verbatim
off disk:

frag = le32_to_cpu(sqsh_ino->fragment);
if (frag != SQUASHFS_INVALID_FRAG) {
/*
* the file cannot have a fragment (tailend) and have a
* file size a multiple of the block size
*/
if ((inode->i_size & (msblk->block_size - 1)) == 0) { ... }
frag_offset = le32_to_cpu(sqsh_ino->offset);
...
}
...
squashfs_i(inode)->fragment_offset = frag_offset;

The check added by commit 9ee94bfbe930 ("Squashfs: add additional inode
sanity checking") is about i_size, not about offset, and nothing else
looks at offset at all. It reaches squashfs_copy_data() unchanged, for
instance from squashfs_readahead_fragment():

bytes = squashfs_copy_data(addr, buffer, offset +
squashfs_i(inode)->fragment_offset, avail);

where the parameter is a signed int:

while (offset < entry->length) {
void *buff = entry->data[offset / PAGE_SIZE]
+ (offset % PAGE_SIZE);

An on-disk offset of 0x80000000 arrives as -2147483648, the loop guard
passes, and entry->data[-524288] is loaded from about 4 MiB below the
cache entry's page pointer array.

The tailend of the file has to lie inside the uncompressed fragment
block, and that block is at most msblk->block_size bytes, so check that
when the inode is read -- the offset is on-disk metadata and belongs
validated where it is parsed, not where it eventually faults.

Mounting a crafted image requires CAP_SYS_ADMIN in the initial user
namespace -- SquashFS is not FS_USERNS_MOUNT, so an unprivileged user in
a user namespace cannot mount one either. The realistic threat model is
untrusted images: automounted removable media and udisks2, .snap and
AppImage style container images, mount -o loop from a setuid helper, and
any service that mounts a squashfs it did not build. Note the trigger
is unprivileged and a read-only mount is enough: a single read() of the
crafted file by any user who can open it.

# mksquashfs -noI -noD -noF -noX -b 4096 with one 1-byte file, then
# rewrite that inode's fragment offset to 0x80000000
mount -t squashfs -o ro /dev/loop0 /mnt
cat /mnt/file

BUG: KASAN: wild-memory-access in squashfs_copy_data+0xb8/0x158
Read of size 8 at addr 007f0000c7b1a0a0 by task cat/132
__asan_load8+0x84/0xd0
squashfs_copy_data+0xb8/0x158
squashfs_readahead+0x9b8/0xe60
read_pages+0x134/0x550
page_cache_ra_unbounded+0x264/0x540
filemap_get_pages+0x1d0/0xaa0
filemap_splice_read+0x248/0x548
do_sendfile+0x540/0x618
__arm64_sys_sendfile64+0x1f8/0x220
el0t_64_sync+0x198/0x1a0
Unable to handle kernel paging request at virtual address 007f0000c7b1a0a0
...
Kernel panic - not syncing: Oops: Fatal exception

The KASAN report is immediately followed by a fatal page fault at the
same address, so this is not a sanitizer-only artefact: the same load
oopses on a kernel built without KASAN. (The trace goes through
sendfile(2) because busybox cat uses it; the plain read(2) path reaches
the same squashfs_copy_data() through squashfs_readpage_fragment().)

mksquashfs packs tailends inside a single fragment block, so every image
it produces satisfies the new check.

Fixes: 6545b246a2c8 ("Squashfs: inode operations")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Yuejie Shi <syjcnss@xxxxxxxxx>
---
fs/squashfs/inode.c | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)

diff --git a/fs/squashfs/inode.c b/fs/squashfs/inode.c
index 82b687414e65..4872b28f1b77 100644
--- a/fs/squashfs/inode.c
+++ b/fs/squashfs/inode.c
@@ -156,6 +156,16 @@ int squashfs_read_inode(struct inode *inode, long long
goto failed_read;
}
frag_offset = le32_to_cpu(sqsh_ino->offset);
+ /*
+ * the tailend has to lie within the uncompressed
+ * fragment block, which is at most block_size bytes
+ */
+ if (frag_offset +
+ (inode->i_size & (msblk->block_size - 1)) >
+ msblk->block_size) {
+ err = -EINVAL;
+ goto failed_read;
+ }
frag_size = squashfs_frag_lookup(sb, frag, &frag_blk);
if (frag_size < 0) {
err = frag_size;
@@ -212,6 +222,16 @@ int squashfs_read_inode(struct inode *inode, long long
goto failed_read;
}
frag_offset = le32_to_cpu(sqsh_ino->offset);
+ /*
+ * the tailend has to lie within the uncompressed
+ * fragment block, which is at most block_size bytes
+ */
+ if (frag_offset +
+ (inode->i_size & (msblk->block_size - 1)) >
+ msblk->block_size) {
+ err = -EINVAL;
+ goto failed_read;
+ }
frag_size = squashfs_frag_lookup(sb, frag, &frag_blk);
if (frag_size < 0) {
err = frag_size;
--
2.51.0