* [Linux Kernel Bug] KASAN: use-after-free Write in olq_alloc_dquot
@ 2026-10-15 14:19 Jiaming Zhang
2026-10-19 15:47 ` [PATCH] ocfs2: validate the block count of a local quota file Jiaming Zhang
0 siblings, 1 reply; 2+ messages in thread
From: Jiaming Zhang @ 2026-10-15 14:19 UTC (permalink / raw)
To: mark, jlbec, joseph.qi, ocfs2-devel
Cc: heming.zhao, linux-kernel, syzkaller, r772577952
Dear Linux kernel developers and maintainers,
We are writing to report an issue discovered in the OCFS2 subsystem. The
issue is reproducible on the latest version of linux (v7.3-rc7, commit
24a19b953e2a3d0ea5a8c4951a0ab8f32720c907). It is triggered by mounting a
crafted ocfs2 image that has quotas enabled (which needs CAP_SYS_ADMIN)
and then creating a file on it. Below is the kernel report:
==================================================================
BUG: KASAN: use-after-free in instrument_write
include/linux/instrumented.h:41 [inline]
BUG: KASAN: use-after-free in ___set_bit
include/asm-generic/bitops/instrumented-non-atomic.h:28 [inline]
BUG: KASAN: use-after-free in __set_bit_le
include/asm-generic/bitops/le.h:36 [inline]
BUG: KASAN: use-after-free in _ocfs2_set_bit fs/ocfs2/ocfs2.h:930 [inline]
BUG: KASAN: use-after-free in ocfs2_set_bit_unaligned
fs/ocfs2/ocfs2.h:961 [inline]
BUG: KASAN: use-after-free in olq_alloc_dquot+0x7a/0xd0
fs/ocfs2/quota_local.c:1211
Write of size 8 at addr ffff88806b34fda0 by task syz.0.17/12574
CPU: 0 UID: 0 PID: 12574 Comm: syz.0.17 Not tainted 7.3.0-rc7 #56 PREEMPT(full)
Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix,
1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
Call Trace:
<TASK>
__dump_stack lib/dump_stack.c:94 [inline]
dump_stack_lvl+0x10e/0x190 lib/dump_stack.c:120
print_address_description mm/kasan/report.c:378 [inline]
print_report+0x153/0x7e0 mm/kasan/report.c:482
kasan_report+0x147/0x180 mm/kasan/report.c:595
check_region_inline mm/kasan/generic.c:-1 [inline]
kasan_check_range+0x2b0/0x2c0 mm/kasan/generic.c:200
instrument_write include/linux/instrumented.h:41 [inline]
___set_bit include/asm-generic/bitops/instrumented-non-atomic.h:28 [inline]
__set_bit_le include/asm-generic/bitops/le.h:36 [inline]
_ocfs2_set_bit fs/ocfs2/ocfs2.h:930 [inline]
ocfs2_set_bit_unaligned fs/ocfs2/ocfs2.h:961 [inline]
olq_alloc_dquot+0x7a/0xd0 fs/ocfs2/quota_local.c:1211
ocfs2_modify_bh+0x240/0x470 fs/ocfs2/quota_local.c:116
ocfs2_create_local_dquot+0x1511/0x1a90 fs/ocfs2/quota_local.c:1261
ocfs2_acquire_dquot+0x7dc/0xb20 fs/ocfs2/quota_global.c:897
dqget+0x73c/0xc50 fs/quota/dquot.c:1002
__dquot_initialize+0x3b3/0xcb0 fs/quota/dquot.c:1462
ocfs2_get_init_inode+0x13b/0x1b0 fs/ocfs2/namei.c:206
ocfs2_mknod+0x88d/0x2000 fs/ocfs2/namei.c:315
ocfs2_create+0x195/0x450 fs/ocfs2/namei.c:678
lookup_open+0xf95/0x18e0 fs/namei.c:4596
open_last_lookups fs/namei.c:4796 [inline]
path_openat+0x6b1/0x1f20 fs/namei.c:5026
do_file_open+0x203/0x440 fs/namei.c:5058
do_sys_openat2+0x107/0x200 fs/open.c:1417
do_sys_open fs/open.c:1423 [inline]
__do_sys_openat fs/open.c:1439 [inline]
__se_sys_openat fs/open.c:1434 [inline]
__x64_sys_openat+0x138/0x170 fs/open.c:1434
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x170/0x540 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f3ad6ba778d
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48
89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d
01 f0 ff ff 73 01 c3 48 c7 c1 b0 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f3ad7b32018 EFLAGS: 00000246 ORIG_RAX: 0000000000000101
RAX: ffffffffffffffda RBX: 00007f3ad6e35fa0 RCX: 00007f3ad6ba778d
RDX: 0000000000000042 RSI: 0000200000000040 RDI: ffffffffffffff9c
RBP: 00007f3ad6c4eb3d R08: 0000000000000000 R09: 0000000000000000
R10: 00000000000001ff R11: 0000000000000246 R12: 0000000000000000
R13: 00007f3ad6e36038 R14: 00007f3ad6e35fa0 R15: 00007ffe36ca1410
</TASK>
The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x7f0af21c8
pfn:0x6b34f
flags: 0x4fff00000000000(node=1|zone=1|lastcpupid=0x7ff)
raw: 04fff00000000000 ffffea0001acd7c8 ffffea0001d459c8 0000000000000000
raw: 00000007f0af21c8 0000000000000000 00000000ffffffff 0000000000000000
page dumped because: kasan: bad access detected
page_owner tracks the page as freed
page last allocated via order 0, migratetype Movable, gfp_mask
0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), pid 12556, tgid 12556 (9),
ts 160210821197
set_page_owner include/linux/page_owner.h:33 [inline]
post_alloc_hook+0x205/0x260 mm/page_alloc.c:1871
prep_new_page mm/page_alloc.c:1879 [inline]
get_page_from_freelist+0x235b/0x23e0 mm/page_alloc.c:3943
__alloc_frozen_pages_noprof+0x209/0x5a0 mm/page_alloc.c:5436
alloc_pages_mpol+0x241/0x4a0 mm/mempolicy.c:2486
folio_alloc_mpol_noprof+0x39/0x160 mm/mempolicy.c:2505
vma_alloc_folio_noprof+0xdb/0x1e0 mm/mempolicy.c:2540
folio_prealloc+0x30/0x180 mm/memory.c:-1
do_cow_fault mm/memory.c:6024 [inline]
do_fault mm/memory.c:6136 [inline]
do_pte_missing+0x519/0x3350 mm/memory.c:4700
handle_pte_fault mm/memory.c:6558 [inline]
__handle_mm_fault mm/memory.c:6707 [inline]
handle_mm_fault+0x19bb/0x36d0 mm/memory.c:6876
do_user_addr_fault+0xa7e/0x1360 arch/x86/mm/fault.c:1343
handle_page_fault arch/x86/mm/fault.c:1483 [inline]
exc_page_fault+0x71/0xd0 arch/x86/mm/fault.c:1536
asm_exc_page_fault+0x26/0x30 arch/x86/include/asm/idtentry.h:595
page last free pid 12556 tgid 12556 ts 160322719585 stack trace:
reset_page_owner include/linux/page_owner.h:26 [inline]
__free_pages_prepare mm/page_alloc.c:1418 [inline]
free_unref_folios+0xd80/0x14c0 mm/page_alloc.c:3022
folios_put_refs+0x7b4/0x8a0 mm/folio.c:1036
free_pages_and_swap_cache+0x25e/0x440 mm/swap_state.c:592
__tlb_batch_free_encoded_pages mm/mmu_gather.c:138 [inline]
tlb_batch_pages_flush mm/mmu_gather.c:151 [inline]
tlb_flush_mmu_free mm/mmu_gather.c:417 [inline]
tlb_flush_mmu+0x3a2/0x680 mm/mmu_gather.c:424
tlb_finish_mmu+0xf9/0x230 mm/mmu_gather.c:549
exit_mmap+0x473/0x9c0 mm/mmap.c:1328
__mmput+0x118/0x430 kernel/fork.c:1193
exec_mm_put_old fs/exec.c:920 [inline]
setup_new_exec+0x30e/0x360 fs/exec.c:1400
load_elf_binary+0xbd1/0x2860 fs/binfmt_elf.c:1024
search_binary_handler fs/exec.c:1779 [inline]
exec_binprm fs/exec.c:1811 [inline]
bprm_execve+0x933/0x15c0 fs/exec.c:1867
do_execveat_common+0x522/0x6b0 fs/exec.c:1965
__do_sys_execve fs/exec.c:2038 [inline]
__se_sys_execve fs/exec.c:2032 [inline]
__x64_sys_execve+0x97/0xc0 fs/exec.c:2032
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x170/0x540 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
Memory state around the buggy address:
ffff88806b34fc80: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
ffff88806b34fd00: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
>ffff88806b34fd80: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
^
ffff88806b34fe00: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
ffff88806b34fe80: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
==================================================================
Following is the root cause analysis for this issue, note that the analysis
is performed with the assistance of LLM, but we try our best to ensure the
accuracy.
Every ocfs2 node keeps a local quota file, divided into chunks. Each chunk
starts with a header block whose bitmap records which quota entries of that
chunk are in use. When ocfs2 hands out a new entry, olq_alloc_dquot() sets
the entry's bit in that bitmap. The issue occurs because the bit it is
asked to set lies far past the end of the bitmap, which fits in a single
block.
The bad bit position comes from dqi_blocks, the number of blocks the local
quota file says it uses. ocfs2_local_read_info() reads it from disk at
mount time and never checks that it agrees with dqi_chunks, the number of
chunks. Later, when no chunk has a free entry,
ocfs2_extend_local_quota_file() works out how many entry blocks the last
chunk already has from dqi_blocks, and places the new entry right after
them:
chunk_blocks = oinfo->dqi_blocks - ol_quota_chunk_block(sb,
chunk->qc_num) - 1; ... *offset = chunk_blocks * epb;
A chunk can hold at most ol_chunk_blocks() entry blocks, so for a valid
file this number is small. On a crafted image whose dqi_blocks is much
larger than the chunks can account for, the last chunk appears to hold
thousands of entry blocks. The resulting bit position then points
kilobytes past the header block, and setting it corrupts unrelated memory.
To fix this issue, we can check the local quota file header in
ocfs2_local_read_info(), right after dqi_chunks and dqi_blocks are read
from disk. The two values agree when:
- the last chunk has its header block and at most ol_chunk_blocks() entry
blocks, or
- there are no chunks, and the file consists of the info block only.
A file that fails the check should be rejected with ocfs2_error(), the same
way ocfs2_read_quota_block() handles a corrupted quota file.
If this solution is acceptable, we will submit a formal patch.
The kernel console output, kernel config, syzkaller reproducer, and C
reproducer are available at google drive:
https://drive.google.com/drive/folders/1apOZQcX2fl3fQYSipzCoQy9HI1PYDwwf?usp=sharing
Please let us know if any further information is required.
Best Regards, Jiaming Zhang
^ permalink raw reply [flat|nested] 2+ messages in thread
* [PATCH] ocfs2: validate the block count of a local quota file
2026-10-15 14:19 [Linux Kernel Bug] KASAN: use-after-free Write in olq_alloc_dquot Jiaming Zhang
@ 2026-10-19 15:47 ` Jiaming Zhang
0 siblings, 0 replies; 2+ messages in thread
From: Jiaming Zhang @ 2026-10-19 15:47 UTC (permalink / raw)
To: jlbec, joseph.qi, mark, ocfs2-devel
Cc: r772577952, heming.zhao, linux-kernel, syzkaller, stable
Every ocfs2 node keeps a local quota file made of chunks. A chunk is a
header block, whose bitmap marks the entries in use, followed by up to
ol_chunk_blocks() blocks of entries, and only the last chunk may be
partly filled. The file header records the number of chunks in
dqi_chunks and the number of blocks in dqi_blocks. When no chunk has a
free entry, ocfs2_extend_local_quota_file() works out from dqi_blocks
how many entry blocks the last chunk already has, and puts the new entry
right after them:
chunk_blocks = oinfo->dqi_blocks - ol_quota_chunk_block(sb,
chunk->qc_num) - 1; ... *offset = chunk_blocks * epb;
olq_alloc_dquot() then sets that bit in the chunk's bitmap, which fits
in a single block. This is only safe while chunk_blocks is at most
ol_chunk_blocks().
ocfs2_local_read_info() reads dqi_chunks and dqi_blocks from disk at
mount time and never checks that they agree. With one chunk and a
dqi_blocks of a few thousand, the last chunk appears to hold thousands
of entry blocks, and the bit lands kilobytes past the end of the bitmap.
A dqi_blocks below the last chunk's header block makes chunk_blocks, an
unsigned int, wrap around to the same effect.
The stray write corrupts unrelated kernel memory. KASAN calls it a
use-after-free only because the page it hits happens to be free; nothing
in ocfs2 was freed. Reaching it takes mounting a crafted image with
quotas enabled, which needs CAP_SYS_ADMIN, and creating a file. The
issue was found by fuzzing and not seen in production, the reproducer
can be found in the linked report.
Check in ocfs2_local_read_info() that the last chunk has its header
block and at most ol_chunk_blocks() entry blocks, and that a file
without chunks holds only its info block, since the first new chunk is
placed at block dqi_blocks. Refuse the file with ocfs2_error()
otherwise, as ocfs2_read_quota_block() does for a corrupted quota file.
The header block of the last chunk is computed in 64 bits because a
crafted dqi_chunks can wrap ol_quota_chunk_block().
Fixes: 9e33d69f553a ("ocfs2: Implementation of local and global quota file handling")
Closes: https://lore.kernel.org/lkml/CANypQFbjcxXjiigMCCyErN=DSEh3ZG3NMWRpq=Hic8KJ76cwew@mail.gmail.com/
Cc: stable@vger.kernel.org
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Jiaming Zhang <r772577952@gmail.com>
---
fs/ocfs2/quota_local.c | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/fs/ocfs2/quota_local.c b/fs/ocfs2/quota_local.c
index f55810c59b1b..bad913d8bc4f 100644
--- a/fs/ocfs2/quota_local.c
+++ b/fs/ocfs2/quota_local.c
@@ -678,6 +678,30 @@ int ocfs2_finish_quota_recovery(struct ocfs2_super *osb,
return status;
}
+/* Check that dqi_blocks agrees with dqi_chunks in local quota file info */
+static int ocfs2_local_check_info(struct super_block *sb, struct inode *lqinode,
+ struct ocfs2_mem_dqinfo *oinfo)
+{
+ u64 hdr_blk;
+
+ if (!oinfo->dqi_chunks) {
+ if (oinfo->dqi_blocks == ol_quota_chunk_block(sb, 0))
+ return 0;
+ } else {
+ /* Same as ol_quota_chunk_block(), but in 64 bits */
+ hdr_blk = 1 + (u64)(ol_chunk_blocks(sb) + 1) *
+ (oinfo->dqi_chunks - 1);
+ if (oinfo->dqi_blocks > hdr_blk &&
+ oinfo->dqi_blocks - hdr_blk - 1 <= ol_chunk_blocks(sb))
+ return 0;
+ }
+
+ return ocfs2_error(sb,
+ "corrupted local quota file %llu (chunks=%u blocks=%u)\n",
+ (u64)OCFS2_I(lqinode)->ip_blkno,
+ oinfo->dqi_chunks, oinfo->dqi_blocks);
+}
+
/* Read information header from quota file */
static int ocfs2_local_read_info(struct super_block *sb, int type)
{
@@ -733,6 +757,10 @@ static int ocfs2_local_read_info(struct super_block *sb, int type)
oinfo->dqi_blocks = le32_to_cpu(ldinfo->dqi_blocks);
oinfo->dqi_libh = bh;
+ status = ocfs2_local_check_info(sb, lqinode, oinfo);
+ if (status)
+ goto out_err;
+
/* We crashed when using local quota file? */
if (!(oinfo->dqi_flags & OLQF_CLEAN)) {
rec = OCFS2_SB(sb)->quota_rec;
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-19 15:47 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-15 14:19 [Linux Kernel Bug] KASAN: use-after-free Write in olq_alloc_dquot Jiaming Zhang
2026-10-19 15:47 ` [PATCH] ocfs2: validate the block count of a local quota file Jiaming Zhang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®