[PATCH] affs: compact the linear extension cache, not the associative one
From: Matthias Goergens
Date: Sat Sep 26 2026 - 02:28:02 EST
When a file's extension-block count outgrows the linear cache,
affs_grow_extcache() raises i_lc_shift and then "shrinks the old cache
to make more space":
off = 1 << (lc_shift - AFFS_I(inode)->i_lc_shift);
for (i = 1, j = off; j < AFFS_LC_SIZE; i++, j += off)
AFFS_I(inode)->i_ac[i] = AFFS_I(inode)->i_ac[j];
That loop is the compaction of the linear cache i_lc, which is indexed
by ext >> i_lc_shift and has AFFS_LC_SIZE entries. The associative
cache i_ac is keyed by the extension number stored in each entry and
needs no compaction. Applying the loop to i_ac has two effects:
- i_ac has AFFS_AC_SIZE entries, half of AFFS_LC_SIZE, so the loop
reads up to about 2 KiB past the end of the cache page.
- i_lc is never compacted, so after the shift changes, lookups through
it return the wrong extension block, and reads return wrong data.
This happens once an open file grows past AFFS_LC_SIZE extension blocks
(512 with 4K pages, about 18 MB with 512-byte blocks).
Compact i_lc instead; the bound stays the same.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Signed-off-by: Matthias Goergens <matthias.goergens@xxxxxxxxx>
---
Reproduced under QEMU with KASAN on a crafted 45 MiB FFS image with
512-byte blocks: after an open file grows from 320 to 700 extension
blocks, KASAN reports a slab-use-after-free read of 8 bytes in
affs_get_extblock_slow() at the shift change, and 3990 of 4000 random
reads return data from the wrong block. With this patch, KASAN reports
nothing in affs and all 4000 reads are correct. Image generator, test
program and QEMU runner:
https://github.com/matthiasgoergens/linux/tree/reproducer/2026-09-26-affs-extcache
fs/affs/file.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fs/affs/file.c b/fs/affs/file.c
index 23e088a7ed4f..4fd9ec3b3d79 100644
--- a/fs/affs/file.c
+++ b/fs/affs/file.c
@@ -82,7 +82,7 @@ affs_grow_extcache(struct inode *inode, u32 lc_idx)
/* first shrink old cache to make more space */
off = 1 << (lc_shift - AFFS_I(inode)->i_lc_shift);
for (i = 1, j = off; j < AFFS_LC_SIZE; i++, j += off)
- AFFS_I(inode)->i_ac[i] = AFFS_I(inode)->i_ac[j];
+ AFFS_I(inode)->i_lc[i] = AFFS_I(inode)->i_lc[j];
AFFS_I(inode)->i_lc_shift = lc_shift;
AFFS_I(inode)->i_lc_mask = lc_mask;
base-commit: 6812ce4e4379ffc99c52401ec28f0d7ffbc36206
--
2.55.0