sqfs_find_inode() walks the uncompressed inode table for sblk->inodes iterations, advancing 'offset' by sqfs_inode_size() each step. Both the inode count and every per-inode size are taken verbatim from the on-disk (attacker-controlled) SquashFS image, but the walk was never bounded against the end of the heap buffer allocated in sqfs_read_inode_table() (metablks_count * SQFS_METADATA_BLOCK_SIZE bytes). A malformed image can therefore drive 'base = inode_table + offset' and the field reads in sqfs_inode_size() past the buffer, an out-of-bounds read reachable from any path lookup (e.g. 'ls'/'load' of a squashfs filesystem).
The SQFS_LDIR_TYPE branch of sqfs_inode_size() makes this worse: it loops i_count times reading di->size and advancing 'di' by sizeof(*di) + size with no bounds check at all, so a single crafted extended-directory inode walks 'di' arbitrarily far past the buffer. Propagate the inode-table buffer size from sqfs_read_inode_table() into the dir stream and on into sqfs_find_inode(), and: - reject any base inode whose fixed fields fall outside the table; - pass the buffer end to sqfs_inode_size() and bound the LDIR index walk against it; - reject any computed inode size that pushes 'offset' past the buffer. Reproduced with a standalone libc + AddressSanitizer harness replicating the verbatim sqfs_find_inode()/sqfs_inode_size() logic: the unpatched code faults on an OOB read inside the LDIR loop, the patched code rejects the malformed inode and returns NULL cleanly. Signed-off-by: Hem Parekh <[email protected]> --- fs/squashfs/sqfs.c | 29 +++++++++++++++++------------ fs/squashfs/sqfs_filesystem.h | 5 +++-- fs/squashfs/sqfs_inode.c | 20 ++++++++++++++++---- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/fs/squashfs/sqfs.c b/fs/squashfs/sqfs.c index 0768fc4a7b2..d92f8869284 100644 --- a/fs/squashfs/sqfs.c +++ b/fs/squashfs/sqfs.c @@ -486,8 +486,9 @@ static int sqfs_search_dir(struct squashfs_dir_stream *dirs, char **token_list, dirsp = (struct fs_dir_stream *)dirs; /* Start by root inode */ - table = sqfs_find_inode(dirs->inode_table, le32_to_cpu(sblk->inodes), - sblk->inodes, sblk->block_size); + table = sqfs_find_inode(dirs->inode_table, dirs->inode_table_size, + le32_to_cpu(sblk->inodes), sblk->inodes, + sblk->block_size); if (!table) return -EINVAL; @@ -543,8 +544,9 @@ static int sqfs_search_dir(struct squashfs_dir_stream *dirs, char **token_list, dirs->dir_header->inode_number; /* Get reference to inode in the inode table */ - table = sqfs_find_inode(dirs->inode_table, new_inode_number, - sblk->inodes, sblk->block_size); + table = sqfs_find_inode(dirs->inode_table, dirs->inode_table_size, + new_inode_number, sblk->inodes, + sblk->block_size); if (!table) return -EINVAL; dir = (struct squashfs_dir_inode *)table; @@ -719,7 +721,8 @@ static int sqfs_get_metablk_pos(u32 *pos_list, void *table, u32 offset, return ret; } -static int sqfs_read_inode_table(unsigned char **inode_table) +static int sqfs_read_inode_table(unsigned char **inode_table, + size_t *inode_table_size) { struct squashfs_super_block *sblk = ctxt.sblk; u64 start, n_blks, table_offset, table_size; @@ -773,6 +776,8 @@ static int sqfs_read_inode_table(unsigned char **inode_table) goto free_itb; } + *inode_table_size = (size_t)metablks_count * SQFS_METADATA_BLOCK_SIZE; + src_table = itb + table_offset + SQFS_HEADER_SIZE; /* Extract compressed Inode table */ @@ -933,7 +938,7 @@ static int sqfs_opendir_nest(const char *filename, struct fs_dir_stream **dirsp) dirs->inode_table = NULL; dirs->dir_table = NULL; - ret = sqfs_read_inode_table(&inode_table); + ret = sqfs_read_inode_table(&inode_table, &dirs->inode_table_size); if (ret) { ret = -EINVAL; goto out; @@ -1071,8 +1076,8 @@ static int sqfs_readdir_nest(struct fs_dir_stream *fs_dirs, struct fs_dirent **d } i_number = dirs->dir_header->inode_number + dirs->entry->inode_offset; - ipos = sqfs_find_inode(dirs->inode_table, i_number, sblk->inodes, - sblk->block_size); + ipos = sqfs_find_inode(dirs->inode_table, dirs->inode_table_size, + i_number, sblk->inodes, sblk->block_size); if (!ipos) return -SQFS_STOP_READDIR; @@ -1430,8 +1435,8 @@ static int sqfs_read_nest(const char *filename, void *buf, loff_t offset, } i_number = dirs->dir_header->inode_number + dirs->entry->inode_offset; - ipos = sqfs_find_inode(dirs->inode_table, i_number, sblk->inodes, - sblk->block_size); + ipos = sqfs_find_inode(dirs->inode_table, dirs->inode_table_size, + i_number, sblk->inodes, sblk->block_size); if (!ipos) { ret = -EINVAL; goto out; @@ -1699,8 +1704,8 @@ static int sqfs_size_nest(const char *filename, loff_t *size) } i_number = dirs->dir_header->inode_number + dirs->entry->inode_offset; - ipos = sqfs_find_inode(dirs->inode_table, i_number, sblk->inodes, - sblk->block_size); + ipos = sqfs_find_inode(dirs->inode_table, dirs->inode_table_size, + i_number, sblk->inodes, sblk->block_size); if (!ipos) { *size = 0; diff --git a/fs/squashfs/sqfs_filesystem.h b/fs/squashfs/sqfs_filesystem.h index be56498a5e3..2e4224ea78a 100644 --- a/fs/squashfs/sqfs_filesystem.h +++ b/fs/squashfs/sqfs_filesystem.h @@ -275,6 +275,7 @@ struct squashfs_dir_stream { * sqfs_opendir() and freed in sqfs_closedir(). */ unsigned char *inode_table; + size_t inode_table_size; unsigned char *dir_table; }; @@ -293,8 +294,8 @@ struct squashfs_file_info { bool comp; }; -void *sqfs_find_inode(void *inode_table, int inode_number, __le32 inode_count, - __le32 block_size); +void *sqfs_find_inode(void *inode_table, size_t inode_table_size, + int inode_number, __le32 inode_count, __le32 block_size); int sqfs_dir_offset(void *dir_i, u32 *m_list, int m_count); diff --git a/fs/squashfs/sqfs_inode.c b/fs/squashfs/sqfs_inode.c index ce9a8ff8e2a..da347becaff 100644 --- a/fs/squashfs/sqfs_inode.c +++ b/fs/squashfs/sqfs_inode.c @@ -17,7 +17,8 @@ #include "sqfs_filesystem.h" #include "sqfs_utils.h" -int sqfs_inode_size(struct squashfs_base_inode *inode, u32 blk_size) +int sqfs_inode_size(struct squashfs_base_inode *inode, u32 blk_size, + const void *end) { u16 inode_type = get_unaligned_le16(&inode->inode_type); @@ -53,9 +54,13 @@ int sqfs_inode_size(struct squashfs_base_inode *inode, u32 blk_size) di = ldir->index; while (l < i_count) { + if ((void *)di + sizeof(*di) > end) + return -EINVAL; sz = get_unaligned_le32(&di->size) + 1; index_list_size += sz; di = (void *)di + sizeof(*di) + sz; + if ((void *)di > end || (void *)di < (void *)ldir) + return -EINVAL; l++; } @@ -114,9 +119,10 @@ int sqfs_inode_size(struct squashfs_base_inode *inode, u32 blk_size) * Given the uncompressed inode table, the inode to be found and the number of * inodes in the table, return inode position in case of success. */ -void *sqfs_find_inode(void *inode_table, int inode_number, __le32 inode_count, - __le32 block_size) +void *sqfs_find_inode(void *inode_table, size_t inode_table_size, + int inode_number, __le32 inode_count, __le32 block_size) { + const void *end = inode_table + inode_table_size; struct squashfs_base_inode *base; unsigned int offset = 0, k; int sz; @@ -128,14 +134,20 @@ void *sqfs_find_inode(void *inode_table, int inode_number, __le32 inode_count, for (k = 0; k < le32_to_cpu(inode_count); k++) { base = inode_table + offset; + /* Ensure the base inode fields lie within the table */ + if ((void *)(base + 1) > end) + return NULL; if (get_unaligned_le32(&base->inode_number) == inode_number) return inode_table + offset; - sz = sqfs_inode_size(base, le32_to_cpu(block_size)); + sz = sqfs_inode_size(base, le32_to_cpu(block_size), end); if (sz < 0) return NULL; offset += sz; + /* The computed inode must not run past the table buffer */ + if (offset > inode_table_size) + return NULL; } printf("Inode not found.\n"); -- 2.50.1 (Apple Git-155)

