Root Cause Analysis
1. Involved Components
- File:
fs/squashfs/namei.c
- Function:
squashfs_lookup()
- VFS Caller:
lookup_one_qstr_excl() in fs/namei.c (called via start_dirop() / filename_create())
- Underlying Mechanism: Handling of error return values across
squashfs_read_metadata() and squashfs_dir_space().
2. Technical Breakdown and Mechanism
In fs/squashfs/namei.c, squashfs_lookup() iterates over directory headers and directory entries to locate an inode matching the requested filename:
static struct dentry *squashfs_lookup(struct inode *dir, struct dentry *dentry,
unsigned int flags)
{
...
int err;
...
while (length < i_size_read(dir)) {
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)
goto read_failure;
length += sizeof(dirh);
dir_count = le32_to_cpu(dirh.count) + 1;
...
while (dir_count--) {
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;
...
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 += size;
...
When reading metadata succeeds, squashfs_read_metadata() returns the number of bytes read as a positive integer:
sizeof(dirh) (12 bytes)
sizeof(*dire) (8 bytes)
size (dire->size + 1, between 1 and 257 bytes; in this report, 6 bytes for a 5-byte filename).
When reading metadata succeeds, err holds this positive integer.
If a subsequent boundary check via squashfs_dir_space() fails (at line 205, line 224, or line 239) due to a malformed or truncated directory layout on disk, execution jumps directly to read_failure:
data_error:
err = -EIO;
read_failure:
ERROR("Unable to read directory block [%llx:%x]\n",
squashfs_i(dir)->start, squashfs_i(dir)->offset);
failed:
kfree(dire);
return ERR_PTR(err);
Notice that while data_error sets err = -EIO before falling into read_failure, jumping directly to read_failure does not set err = -EIO or any negative error code. Consequently, err retains the positive integer from the previous successful squashfs_read_metadata() call (or 0 if stack initialization zeroed it prior to any read).
3. How the Failure Manifests
squashfs_lookup() returns ERR_PTR(err). Because err is a positive number (such as 6), ERR_PTR(6) evaluates to (void *)(long)6, i.e., 0x0000000000000006.
The VFS caller in fs/namei.c (lookup_one_qstr_excl()):
old = dir->i_op->lookup(dir, dentry, flags);
if (unlikely(old)) {
dput(dentry);
dentry = old;
}
found:
if (IS_ERR(dentry))
return dentry;
if (d_is_negative(dentry) && !(flags & LOOKUP_CREATE)) {
Because old is non-NULL (0x6), dentry is set to 0x6.
Next, IS_ERR(dentry) tests whether (unsigned long)dentry >= (unsigned long)-MAX_ERRNO (>= 0xfffffffffffff001). Since 0x6 < -MAX_ERRNO, IS_ERR() evaluates to false.
The VFS assumes dentry is a valid pointer and calls d_is_negative(dentry):
static inline unsigned __d_entry_type(const struct dentry *dentry)
{
return dentry->d_flags & DCACHE_ENTRY_TYPE;
}
This attempts to read dentry->d_flags at offset 0 (0x0000000000000006). Under KASAN, checking shadow memory for 0x6 >> 3 == 0 causes an access to KASAN_SHADOW_OFFSET (0xdffffc0000000000), triggering a General Protection Fault (#GP) reported as KASAN: null-ptr-deref in range [0x0000000000000000-0x0000000000000007]. In a kernel without sanitizers, this results in an unhandled supervisor read page fault at address 0x0000000000000006, halting the process or panicking the system.