[PATCH v2 20/22] Squashfs: fix variables in directory code and add more sanity checks
From: Phillip Lougher
Date: Wed Nov 11 2026 - 18:50:42 EST
A directory larger than a metadata block (8192 bytes) will have a
directory index to allow faster reading and lookup of names. This
index allows the code to jump to the desired compressed metadata block,
without requiring the directory to be scanned from the beginning.
But the index handling code has a defect in that multiple variables
including those used to read and track directory indexes from disk are
signed ints. This means any index which is 2^31 or larger will be
treated as a negative value. The knock on efect of this is that the
offset into the desired metadata block is incorrectly computed, with the
result the directory fails to be read, and an I/O error is returned.
This commit corrects the variables (and function return value) to be
unsigned int, and it also adds more sanity checking of the values read
off disk (including directory size to avoid underflow in subtraction).
Now if a directory index is found to be incorrect, the index is
discarded and the previous index read from disk is used. This allows
the directory to be read correctly without throwing an I/O error.
Reproducer
==========
The following reproducer creates a Squashfs filesystem with a root
directory which has a directory index. The reproducer then corrupts
root directory index 0 with an index value of -1. The filesystem is
then mounted, and stat run on file /mnt/filename-490.
If the exploit succeeds stat will report an I/O error and dmesg will
contain Squashfs error reports.
=====================================================================
#!/bin/bash
finish() {
popd 2>&1 > /dev/null
rm -rf $DIR
exit 1
}
DIR=$(mktemp -d)
pushd $DIR 2>&1 > /dev/null
for i in $(seq 1 1000); do
echo "filename-$i f 0700 0 0 echo hello" >> pseudo
done
echo "Create a Squashfs filesystem"
if ! mksquashfs - img.sqsh -pf pseudo -pd "d 0777 0 0" -noI -no-progress -quiet; then
echo Creating the Squashfs filesystem failed
finish
fi
# Write -1 in little-endian format to file
/usr/bin/echo -en "\xff\xff\xff\xff" > file
echo "Over-write the root directory index 0, with an index value of -1."
echo "This because the metadata is uncompressed can be over-written directly,"
echo "but, normally the metadata will be compressed."
echo "The location of the 4 byte index is 32150"
dd bs=1 seek=32150 if=file of=img.sqsh count=4 conv=notrunc status=none
echo
echo "Mount the filesystem on /mnt"
if ! mount -t squashfs img.sqsh /mnt; then
echo Mount of Squashfs filesystem failed
finish
fi
echo
echo "Run stat /mnt/filename-490 on the mounted filesystem"
echo
echo "If the exploit works stat will report I/O error, and dmesg will"
echo "contain Squashfs error reports"
echo
stat /mnt/filename-490
umount /mnt
popd 2>&1 > /dev/null
rm -rf $DIR
exit 0
=====================================================================
Fixes: 07972dde75c32 ("Squashfs: directory readdir operations")
Fixes: c88da2c979369 ("Squashfs: directory lookup operations")
Signed-off-by: Phillip Lougher <phillip@xxxxxxxxxxxxxxx>
---
fs/squashfs/dir.c | 73 ++++++++++++++++++++------
fs/squashfs/inode.c | 12 ++++-
fs/squashfs/namei.c | 95 ++++++++++++++++++++++++++--------
fs/squashfs/squashfs_helpers.h | 7 +++
4 files changed, 147 insertions(+), 40 deletions(-)
diff --git a/fs/squashfs/dir.c b/fs/squashfs/dir.c
index f6454f0d5b5c..33b8225fcdaa 100644
--- a/fs/squashfs/dir.c
+++ b/fs/squashfs/dir.c
@@ -37,17 +37,17 @@ static const unsigned char squashfs_filetype_table[] = {
* (if any) we have managed to read - the index isn't essential, just
* quicker.
*/
-static int get_dir_index_using_offset(struct super_block *sb,
+static unsigned int get_dir_index_using_offset(struct super_block *sb,
u64 *next_block, int *next_offset, u64 index_start, int index_offset,
- int i_count, u64 f_pos)
+ unsigned int i_count, loff_t max_index, u64 f_pos)
{
struct squashfs_sb_info *msblk = sb->s_fs_info;
s64 res;
- int err, i, index, length = 0;
- unsigned int size;
+ int err;
+ unsigned int i, index, length = 0, size, start_block, block;
struct squashfs_dir_index dir_index;
- TRACE("Entered get_dir_index_using_offset, i_count %d, f_pos %lld\n",
+ TRACE("Entered get_dir_index_using_offset, i_count %u, f_pos %lld\n",
i_count, f_pos);
/*
@@ -58,6 +58,18 @@ static int get_dir_index_using_offset(struct super_block *sb,
if (f_pos <= 3)
return f_pos;
f_pos -= 3;
+ max_index -= 3;
+
+ /*
+ * A valid index starts at a directory header, and there must be enough
+ * room for a directory header and one directory entry.
+ */
+ if (max_index < sizeof(struct squashfs_dir_header) +
+ sizeof(struct squashfs_dir_entry) + 1)
+ return 3;
+
+ max_index -= sizeof(struct squashfs_dir_header) +
+ sizeof(struct squashfs_dir_entry) + 1;
for (i = 0; i < i_count; i++) {
err = squashfs_read_metadata(sb, &dir_index, &index_start,
@@ -65,33 +77,51 @@ static int get_dir_index_using_offset(struct super_block *sb,
if (err < 0)
break;
+ /* Sanity check the directory index contents */
+
+ /* size should never be larger than SQUASHFS_NAME_LEN */
+ size = le32_to_cpu(dir_index.size) + 1;
+ if (size > SQUASHFS_NAME_LEN)
+ break;
+
+ /* The stored index should not be larger than max_index */
index = le32_to_cpu(dir_index.index);
+ if (index > max_index)
+ break;
+
+ block = le32_to_cpu(dir_index.start_block);
+
+ if (i > 0) {
+ /* The stored index should be larger than the previous index */
+ if (index <= length)
+ break;
+
+ /* The start_block should not be less than the previous start_block */
+ if (block < start_block)
+ break;
+ }
+
if (index > f_pos)
/*
* Found the index we're looking for.
*/
break;
- size = le32_to_cpu(dir_index.size) + 1;
-
- /* size should never be larger than SQUASHFS_NAME_LEN */
- if (size > SQUASHFS_NAME_LEN)
- break;
-
err = squashfs_read_metadata(sb, NULL, &index_start,
&index_offset, size);
if (err < 0)
break;
- res = squashfs_dir_start(le32_to_cpu(dir_index.start_block), msblk);
+ res = squashfs_dir_start(block, msblk);
if (res < 0)
break;
+ start_block = block;
length = index;
*next_block = res;
}
- *next_offset = (length + *next_offset) % SQUASHFS_METADATA_SIZE;
+ *next_offset = ((u64)length + *next_offset) % SQUASHFS_METADATA_SIZE;
/*
* Translate back from internal f_pos to external f_pos.
@@ -105,7 +135,8 @@ static int squashfs_readdir(struct file *file, struct dir_context *ctx)
struct inode *inode = file_inode(file);
struct squashfs_sb_info *msblk = inode->i_sb->s_fs_info;
s64 block = squashfs_dir_start(squashfs_i(inode)->start, msblk);
- int offset = squashfs_i(inode)->offset, length, err;
+ int offset = squashfs_i(inode)->offset, err;
+ loff_t length;
unsigned int inode_number, dir_count, size, type;
struct squashfs_dir_header dirh;
struct squashfs_dir_entry *dire = NULL;
@@ -154,12 +185,15 @@ static int squashfs_readdir(struct file *file, struct dir_context *ctx)
squashfs_i(inode)->dir_idx_start,
squashfs_i(inode)->dir_idx_offset,
squashfs_i(inode)->dir_idx_cnt,
- ctx->pos);
+ i_size_read(inode), ctx->pos);
while (length < i_size_read(inode)) {
/*
* Read directory header
*/
+ if (!squashfs_dir_space(inode, length, sizeof(dirh)))
+ goto failed_read;
+
err = squashfs_read_metadata(inode->i_sb, &dirh, &block,
&offset, sizeof(dirh));
if (err < 0)
@@ -176,23 +210,30 @@ static int squashfs_readdir(struct file *file, struct dir_context *ctx)
/*
* Read directory entry.
*/
+ if (!squashfs_dir_space(inode, length, sizeof(*dire)))
+ goto failed_read;
+
err = squashfs_read_metadata(inode->i_sb, dire, &block,
&offset, sizeof(*dire));
if (err < 0)
goto failed_read;
+ length += sizeof(*dire);
size = le16_to_cpu(dire->size) + 1;
/* size should never be larger than SQUASHFS_NAME_LEN */
if (size > SQUASHFS_NAME_LEN)
goto failed_read;
+ if (!squashfs_dir_space(inode, length, size))
+ goto failed_read;
+
err = squashfs_read_metadata(inode->i_sb, dire->name,
&block, &offset, size);
if (err < 0)
goto failed_read;
- length += sizeof(*dire) + size;
+ length += size;
if (ctx->pos >= length)
continue;
diff --git a/fs/squashfs/inode.c b/fs/squashfs/inode.c
index a512e8d00c41..5404d7ba6fc0 100644
--- a/fs/squashfs/inode.c
+++ b/fs/squashfs/inode.c
@@ -253,8 +253,12 @@ int squashfs_read_inode(struct inode *inode, long long ino)
if (err < 0)
goto failed_read;
- set_nlink(inode, le32_to_cpu(sqsh_ino->nlink));
+ /* The directory size must be 3 or larger, as it includes "." and ".." entries */
inode->i_size = le16_to_cpu(sqsh_ino->file_size);
+ if (inode->i_size < 3)
+ goto failed_read;
+
+ set_nlink(inode, le32_to_cpu(sqsh_ino->nlink));
inode->i_op = &squashfs_dir_inode_ops;
inode->i_fop = &squashfs_dir_ops;
inode->i_mode |= S_IFDIR;
@@ -278,9 +282,13 @@ int squashfs_read_inode(struct inode *inode, long long ino)
if (err < 0)
goto failed_read;
+ /* The directory size must be 3 or larger, as it includes "." and ".." entries */
+ inode->i_size = le32_to_cpu(sqsh_ino->file_size);
+ if (inode->i_size < 3)
+ goto failed_read;
+
xattr_id = le32_to_cpu(sqsh_ino->xattr);
set_nlink(inode, le32_to_cpu(sqsh_ino->nlink));
- inode->i_size = le32_to_cpu(sqsh_ino->file_size);
inode->i_op = &squashfs_dir_inode_ops;
inode->i_fop = &squashfs_dir_ops;
inode->i_mode |= S_IFDIR;
diff --git a/fs/squashfs/namei.c b/fs/squashfs/namei.c
index 9f6f6792b549..89d5a09544dc 100644
--- a/fs/squashfs/namei.c
+++ b/fs/squashfs/namei.c
@@ -61,57 +61,96 @@
* (if any) we have managed to read - the index isn't essential, just
* quicker.
*/
-static int get_dir_index_using_name(struct super_block *sb,
+static unsigned int get_dir_index_using_name(struct super_block *sb,
u64 *next_block, int *next_offset, u64 index_start,
- int index_offset, int i_count, const char *name)
+ int index_offset, unsigned int i_count,
+ loff_t max_index, const char *name)
{
struct squashfs_sb_info *msblk = sb->s_fs_info;
s64 res;
- int i, length = 0, err;
- unsigned int size;
- struct squashfs_dir_index *index;
+ int err;
+ unsigned int i, index, length = 0, size, start_block, block;
+ struct squashfs_dir_index *dir_index;
- TRACE("Entered get_dir_index_using_name, i_count %d\n", i_count);
+ TRACE("Entered get_dir_index_using_name, i_count %u\n", i_count);
- index = kmalloc(sizeof(*index) + SQUASHFS_NAME_LEN + 1, GFP_KERNEL);
- if (index == NULL) {
+ dir_index = kmalloc(sizeof(*dir_index) + SQUASHFS_NAME_LEN + 1, GFP_KERNEL);
+ if (dir_index == NULL) {
ERROR("Failed to allocate squashfs_dir_index\n");
goto out;
}
+ /*
+ * Translate from external index to internal index. This is offset by
+ * 3 because we invent "." and ".." entries.
+ */
+ max_index -= 3;
+
+ /*
+ * A valid index starts at a directory header, and there must be enough
+ * room for a directory header and one directory entry.
+ */
+ if (max_index < sizeof(struct squashfs_dir_header) +
+ sizeof(struct squashfs_dir_entry) + 1)
+ goto out;
+
+ max_index -= sizeof(struct squashfs_dir_header) +
+ sizeof(struct squashfs_dir_entry) + 1;
+
for (i = 0; i < i_count; i++) {
- err = squashfs_read_metadata(sb, index, &index_start,
- &index_offset, sizeof(*index));
+ err = squashfs_read_metadata(sb, dir_index, &index_start,
+ &index_offset, sizeof(*dir_index));
if (err < 0)
break;
+ /* Sanity check the directory index contents */
- size = le32_to_cpu(index->size) + 1;
+ /* size should never be larger than SQUASHFS_NAME_LEN */
+ size = le32_to_cpu(dir_index->size) + 1;
if (size > SQUASHFS_NAME_LEN)
break;
- err = squashfs_read_metadata(sb, index->name, &index_start,
+ /* The stored index should not be larger than max_index */
+ index = le32_to_cpu(dir_index->index);
+ if (index > max_index)
+ break;
+
+ block = le32_to_cpu(dir_index->start_block);
+
+ if (i > 0) {
+ /* The stored index should be larger than the previous index */
+ if (index <= length)
+ break;
+
+ /* The start_block should not be less than the previous start_block */
+ if (block < start_block)
+ break;
+ }
+
+ err = squashfs_read_metadata(sb, dir_index->name, &index_start,
&index_offset, size);
if (err < 0)
break;
- index->name[size] = '\0';
+ dir_index->name[size] = '\0';
- if (strcmp(index->name, name) > 0)
+ if (strcmp(dir_index->name, name) > 0)
+ /* Found the index we're looking for. */
break;
- res = squashfs_dir_start(le32_to_cpu(index->start_block), msblk);
+ res = squashfs_dir_start(block, msblk);
if (res < 0)
break;
- length = le32_to_cpu(index->index);
+ start_block = block;
+ length = index;
*next_block = res;
}
- *next_offset = (length + *next_offset) % SQUASHFS_METADATA_SIZE;
- kfree(index);
-
out:
+ *next_offset = ((u64)length + *next_offset) % SQUASHFS_METADATA_SIZE;
+ kfree(dir_index);
+
/*
* Return index (f_pos) of the looked up metadata block. Translate
* from internal f_pos to external f_pos which is offset by 3 because
@@ -133,7 +172,8 @@ static struct dentry *squashfs_lookup(struct inode *dir, struct dentry *dentry,
struct squashfs_dir_entry *dire = NULL;
u64 block = squashfs_dir_start(squashfs_i(dir)->start, msblk);
int offset = squashfs_i(dir)->offset;
- int err, length;
+ int err;
+ loff_t length;
unsigned int dir_count, size;
TRACE("Entered squashfs_lookup [%llx:%x]\n", squashfs_i(dir)->start, offset);
@@ -155,12 +195,16 @@ static struct dentry *squashfs_lookup(struct inode *dir, struct dentry *dentry,
length = get_dir_index_using_name(dir->i_sb, &block, &offset,
squashfs_i(dir)->dir_idx_start,
squashfs_i(dir)->dir_idx_offset,
- squashfs_i(dir)->dir_idx_cnt, name);
+ squashfs_i(dir)->dir_idx_cnt,
+ i_size_read(dir), name);
while (length < i_size_read(dir)) {
/*
* Read directory header.
*/
+ if (!squashfs_dir_space(dir, length, sizeof(dirh)))
+ goto read_failure;
+
err = squashfs_read_metadata(dir->i_sb, &dirh, &block,
&offset, sizeof(dirh));
if (err < 0)
@@ -177,23 +221,30 @@ static struct dentry *squashfs_lookup(struct inode *dir, struct dentry *dentry,
/*
* Read directory entry.
*/
+ if (!squashfs_dir_space(dir, length, sizeof(*dire)))
+ goto read_failure;
+
err = squashfs_read_metadata(dir->i_sb, dire, &block,
&offset, sizeof(*dire));
if (err < 0)
goto read_failure;
+ length += sizeof(*dire);
size = le16_to_cpu(dire->size) + 1;
/* size should never be larger than SQUASHFS_NAME_LEN */
if (size > SQUASHFS_NAME_LEN)
goto data_error;
+ if (!squashfs_dir_space(dir, length, size))
+ goto read_failure;
+
err = squashfs_read_metadata(dir->i_sb, dire->name,
&block, &offset, size);
if (err < 0)
goto read_failure;
- length += sizeof(*dire) + size;
+ length += size;
if (name[0] < dire->name[0])
goto exit_lookup;
diff --git a/fs/squashfs/squashfs_helpers.h b/fs/squashfs/squashfs_helpers.h
index c25f5918b8fa..07a9e1cdd4e4 100644
--- a/fs/squashfs/squashfs_helpers.h
+++ b/fs/squashfs/squashfs_helpers.h
@@ -353,4 +353,11 @@ static inline s64 squashfs_add_indexes(u64 block, s64 blks, struct squashfs_sb_i
return block + blks;
}
+
+static inline bool squashfs_dir_space(struct inode *inode, loff_t length, int size)
+{
+ loff_t space = i_size_read(inode) - length;
+
+ return space >= size;
+}
#endif
--
2.47.3