[BUG] affs: possible recursive locking in affs_remove_header() on rmdir of crafted image

From: Farhad Alemi

Date: Tue May 26 2026 - 15:52:23 EST


Hello David and the linux-fsdevel team,

I am reporting an AFFS lockdep warning found by syzkaller.

Summary:
On a crafted AFFS image, an rmdir() that walks into affs_remove_header()
acquires the same lock class &ei->i_ext_lock/1 twice in the same task --
once on the parent dir at fs/affs/amigaffs.c:289 and again on the child
inode at :296. With CONFIG_LOCKDEP this lights up as "possible recursive
locking detected"; with CONFIG_PANIC_ON_WARN (the syz-manager default) it
terminates the kernel.

Observed on:
- Linux v6.17.8-dirty (where the bug was originally found) and verified still
present in linus/master at commit e8c2f9fdadee (v7.1-rc4-754-ge8c2f9fdadee),
x86_64, QEMU Q35
- KASAN + lockdep enabled; panic_on_warn set
- The only local dirty file in my tree is drivers/tty/serial/serial_core.c,
containing a local ttyS0 console guard for the fuzzing harness. It is
unrelated to fs/affs/.
- Trigger requires the ability to mount a crafted filesystem image,
normally CAP_SYS_ADMIN/root or an equivalent syzkaller test environment.
- The two affs_lock_dir() call sites in affs_remove_header() are
unchanged in linus/master HEAD; bug remains reachable on current
mainline.

Impact:
A malformed AFFS image triggers a lockdep recursive-locking warning during
rmdir(), which panic_on_warn promotes to a kernel panic:

WARNING: possible recursive locking detected
syz.2.17/3573 is trying to acquire lock:
ffff888123db0778 (&ei->i_ext_lock/1){+.+.}-{4:4}, at: affs_lock_dir

affs_remove_header+0x72f fs/affs/amigaffs.c:296
but task is already holding lock:
ffff888123db0100 (&ei->i_ext_lock/1){+.+.}-{4:4}, at: affs_lock_dir

affs_remove_header+0x261 fs/affs/amigaffs.c:289

The two lock instances in the trace are distinct inodes (db0778 vs db0100),
so the immediate failure is a missing subclass distinction in the lockdep
annotation. A related self-deadlock should also be reachable: if the
on-disk hash table contains an entry whose i_ino equals the containing
directory's own block number, affs_iget() returns the same in-memory inode
for both d_inode(dentry) and d_inode(dentry->d_parent), and the two
affs_lock_dir() calls take the same mutex twice -- a real deadlock that
no subclass distinction would prevent. I have not separately reproduced
this case; the reasoning follows from affs_iget()'s iget_locked() caching.

Relevant stack:

__mutex_lock+0x198/0xfb0 kernel/locking/mutex.c:760
affs_lock_dir fs/affs/affs.h:311 [inline]
affs_remove_header+0x72f/0x1ab0 fs/affs/amigaffs.c:296
vfs_rmdir fs/namei.c:4469 [inline]
vfs_rmdir+0x20b/0x6a0 fs/namei.c:4446
do_rmdir+0x2ed/0x3c0 fs/namei.c:4524
__x64_sys_unlinkat+0xf4/0x140 fs/namei.c:4692

Expected behavior:
The crafted image should either be rejected at mount, or rmdir() should
fail cleanly without lockdep splat and without risking a real self-deadlock
on the i_hash_lock.

Reproducer:
I attached the generated C reproducer as reproducer.c. I also attached the
syzkaller program as reproducer.syz and the console report as
crash-report.txt.

Novelty check:
I searched syzbot dashboard data across upstream, fixed, invalid, stable,
and Android namespaces, and searched lore.kernel.org for
"affs_remove_header", "i_ext_lock", and "possible recursive locking" +
"affs". I did not find an exact match.

A proposed patch will follow as a reply to this email shortly.

I appreciate your time and consideration, and I'm grateful for your work
on this subsystem.

Regards,
Farhad
loop2: detected capacity change from 0 to 128
============================================
WARNING: possible recursive locking detected
6.17.8-dirty #5 Not tainted
--------------------------------------------
syz.2.17/3573 is trying to acquire lock:
ffff888123db0778 (&ei->i_ext_lock/1){+.+.}-{4:4}, at: affs_lock_dir fs/affs/affs.h:311 [inline]
ffff888123db0778 (&ei->i_ext_lock/1){+.+.}-{4:4}, at: affs_remove_header+0x72f/0x1ab0 fs/affs/amigaffs.c:296

but task is already holding lock:
ffff888123db0100 (&ei->i_ext_lock/1){+.+.}-{4:4}, at: affs_lock_dir fs/affs/affs.h:311 [inline]
ffff888123db0100 (&ei->i_ext_lock/1){+.+.}-{4:4}, at: affs_remove_header+0x261/0x1ab0 fs/affs/amigaffs.c:289

other info that might help us debug this:
Possible unsafe locking scenario:

CPU0
----
lock(&ei->i_ext_lock/1);
lock(&ei->i_ext_lock/1);

*** DEADLOCK ***

May be due to missing lock nesting notation

5 locks held by syz.2.17/3573:
#0: ffff88811305e418 (sb_writers#13){.+.+}-{0:0}, at: do_rmdir+0x1e7/0x3c0 fs/namei.c:4512
#1: ffff888123db02c0 (&type->i_mutex_dir_key#7/1){+.+.}-{4:4}, at: inode_lock_nested include/linux/fs.h:916 [inline]
#1: ffff888123db02c0 (&type->i_mutex_dir_key#7/1){+.+.}-{4:4}, at: do_rmdir+0x238/0x3c0 fs/namei.c:4516
#2: ffff888123db0938 (&sb->s_type->i_mutex_key#20){+.+.}-{4:4}, at: inode_lock include/linux/fs.h:871 [inline]
#2: ffff888123db0938 (&sb->s_type->i_mutex_key#20){+.+.}-{4:4}, at: vfs_rmdir fs/namei.c:4458 [inline]
#2: ffff888123db0938 (&sb->s_type->i_mutex_key#20){+.+.}-{4:4}, at: vfs_rmdir+0xee/0x6a0 fs/namei.c:4446
#3: ffff888123db06e8 (&ei->i_link_lock){+.+.}-{4:4}, at: affs_lock_link fs/affs/affs.h:301 [inline]
#3: ffff888123db06e8 (&ei->i_link_lock){+.+.}-{4:4}, at: affs_remove_header+0x248/0x1ab0 fs/affs/amigaffs.c:288
#4: ffff888123db0100 (&ei->i_ext_lock/1){+.+.}-{4:4}, at: affs_lock_dir fs/affs/affs.h:311 [inline]
#4: ffff888123db0100 (&ei->i_ext_lock/1){+.+.}-{4:4}, at: affs_remove_header+0x261/0x1ab0 fs/affs/amigaffs.c:289

stack backtrace:
CPU: 0 UID: 0 PID: 3573 Comm: syz.2.17 Not tainted 6.17.8-dirty #5 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), 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/0x1f0 lib/dump_stack.c:120
print_deadlock_bug+0x1e9/0x240 kernel/locking/lockdep.c:3041
check_deadlock kernel/locking/lockdep.c:3093 [inline]
validate_chain kernel/locking/lockdep.c:3895 [inline]
__lock_acquire+0xfe7/0x1b50 kernel/locking/lockdep.c:5237
lock_acquire.part.0+0xb5/0x240 kernel/locking/lockdep.c:5868
__mutex_lock_common kernel/locking/mutex.c:598 [inline]
__mutex_lock+0x198/0xfb0 kernel/locking/mutex.c:760
affs_lock_dir fs/affs/affs.h:311 [inline]
affs_remove_header+0x72f/0x1ab0 fs/affs/amigaffs.c:296
vfs_rmdir fs/namei.c:4469 [inline]
vfs_rmdir+0x20b/0x6a0 fs/namei.c:4446
do_rmdir+0x2ed/0x3c0 fs/namei.c:4524
__do_sys_unlinkat fs/namei.c:4698 [inline]
__se_sys_unlinkat fs/namei.c:4692 [inline]
__x64_sys_unlinkat+0xf4/0x140 fs/namei.c:4692
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0xc9/0x490 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f883789770d
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:00007fff24575708 EFLAGS: 00000246 ORIG_RAX: 0000000000000107
RAX: ffffffffffffffda RBX: 00007f8837b25fa0 RCX: 00007f883789770d
RDX: 0000000000000200 RSI: 0000200000000880 RDI: ffffffffffffff9c
RBP: 00007f883793eb37 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f8837b25fa0 R14: 00007f8837b25fa0 R15: 0000000000001e76
</TASK>

Attachment: reproducer.c
Description: Binary data

Attachment: reproducer.syz
Description: Binary data