syzbot
WARNING in do_epoll_wait (2)
Status:
moderation: reported on 2026/11/12 03:28
Subsystems:
fs
Labels:
prio:low
[Documentation on labels]
Reported-by: syzbot+1f7e1fcbe90a12307de9@syzkaller.appspotmail.com
First crash: 1h25m, last: 1h25m
✨ AI-generated root cause summary:
A lockdep state desynchronization occurs during syscall entry in do_syscall_64() where hardware interrupts are enabled (EFLAGS.IF == 1), but lockdep's per-CPU hardirqs_enabled variable remains 0. When a lockless syscall such as epoll_wait() immediately executes without acquiring locks that would restore irq flags, it reaches lockdep_assert_irqs_enabled() in ep_poll(), triggering a warning because lockdep believes interrupts are disabled. This stems from an interruption or asynchronous transition occurring during or immediately following local_irq_enable() in the entry path (syscall_enter_from_user_mode_randomize_stack) where the exit path leaves hardirqs_enabled cleared.
▶
▼
✨ Full AI root cause analysis
Technical Root Cause Analysis
1. Bug Manifestation & Call Context
The bug report shows a lockdep warning firing at fs/eventpoll.c:2272:
__lockdep_enabled && !this_cpu_read(hardirqs_enabled)
WARNING: fs/eventpoll.c:2272 at ep_poll fs/eventpoll.c:2272 [inline], CPU#1: udevd/2859
WARNING: fs/eventpoll.c:2272 at do_epoll_wait+0x746/0x1790 fs/eventpoll.c:2837, CPU#1: udevd/2859
At the crash site:
- The CPU registers show
EFLAGS: 00010293, where bit 9 (0x200, the Interrupt Flag IF) is set (1), indicating hardware interrupts are actively enabled on CPU#1.
- Register
RBX holds 0, reading this_cpu_read(hardirqs_enabled).
- The system is
Not tainted and __lockdep_enabled (debug_locks && !this_cpu_read(lockdep_recursion)) is true, confirming lockdep is active, debugging locks are enabled, and no prior assertions or warnings have triggered since boot.
- The task
udevd invoked epoll_wait(2), traversing:
entry_SYSCALL_64_after_hwframe → do_syscall_64() → __x64_sys_epoll_wait() → do_epoll_wait() → ep_poll().
- At the top of
ep_poll(), lockdep_assert_irqs_enabled() checks WARN_ON_ONCE(__lockdep_enabled && !this_cpu_read(hardirqs_enabled)) and splats because lockdep's software tracking state disagrees with the hardware CPU flag state.
2. Root Cause Mechanism
-
Syscall Entry State Tracking:
In arch/x86/entry/syscall_64.c, do_syscall_64() invokes syscall_enter_from_user_mode_randomize_stack() (include/linux/entry-common.h:199):
#define syscall_enter_from_user_mode_randomize_stack(regs, syscall) \
({ \
enter_from_user_mode_randomize_stack(regs); \
instrumentation_begin(); \
local_irq_enable(); \
long _ret = syscall_enter_from_user_mode_work(regs, syscall); \
instrumentation_end(); \
_ret; \
})
First, enter_from_user_mode_randomize_stack() → enter_from_user_mode() (include/linux/irq-entry-common.h:98) calls lockdep_hardirqs_off(CALLER_ADDR0), setting hardirqs_enabled = 0 while hardware interrupts are disabled upon entering kernel space from usermode.
-
Interrupt Re-enabling and Lockless Execution in epoll_wait:
At line 204 of include/linux/entry-common.h, local_irq_enable() runs:
trace_hardirqs_on();
raw_local_irq_enable();
Under normal circumstances, trace_hardirqs_on() calls lockdep_hardirqs_on(), which writes __this_cpu_write(hardirqs_enabled, 1).
Following this, do_syscall_64() executes the system call. Unlike many syscalls that acquire spinlocks or mutexes (which repeatedly execute local_irq_save() / local_irq_restore() or lockdep tracking hooks that resynchronize hardirqs_enabled), epoll_wait with valid arguments runs completely locklessly until ep_poll():
CLASS(fd, f)(epfd) acquires an RCU-based or reference-counted file pointer without IRQ-disabling spinlocks.
ep_check_params() performs validation checks.
ep_poll() immediately runs lockdep_assert_irqs_enabled().
-
Desynchronization Mechanism:
Because lockdep_hardirqs_off() in kernel/locking/lockdep.c:4546 asserts DEBUG_LOCKS_WARN_ON(!irqs_disabled()), hardirqs_enabled can only be set to 0 while hardware interrupts are off (IF == 0).
Therefore, either:
- An interrupt or exception occurred immediately around or after
local_irq_enable() in the entry path (include/linux/entry-common.h:204-205), entering via irqentry_enter_from_kernel_mode() (which called lockdep_hardirqs_off()), but returned to the kernel without properly invoking trace_hardirqs_on() / lockdep_hardirqs_on() on the exit path (irqentry_exit_to_kernel_mode_after_preempt()); OR
- A race condition exists in the non-atomic sequence in
local_irq_enable() where trace_hardirqs_on() marks hardirqs_enabled = 1 before raw_local_irq_enable() sets EFLAGS.IF = 1, and an exception/trap firing in that window hits an entry/exit path that inspects regs_irqs_disabled(regs) (finding IF == 0) and skips enabling tracing on exit, returning to raw_local_irq_enable() which sets IF = 1 while leaving hardirqs_enabled = 0.
3. Scope and Incomplete/Unverified Aspects
- Verified:
- Hardware interrupts are definitely enabled (
EFLAGS.IF == 1) at the point of failure.
- No lockdep assertion fired prior to
ep_poll(), and debug_locks was non-zero.
- The failure only manifests visibly on syscalls like
epoll_wait() that do not take spinlocks or change interrupt states before checking lockdep_assert_irqs_enabled().
- Inconclusive / Unverified:
- The exact asynchronous event (e.g. reschedule IPI, timer tick, preemption check via
irqentry_exit_cond_resched, or an in-kernel exception/probe) that executed between local_irq_enable() and ep_poll() leaving hardirqs_enabled cleared could not be definitively pinpointed due to disabled tools, but the bug definitely resides in the entry/irqentry common framework (include/linux/entry-common.h, include/linux/irq-entry-common.h, and kernel/entry/common.c).
Sample crash report:
------------[ cut here ]------------
__lockdep_enabled && !this_cpu_read(hardirqs_enabled)
WARNING: fs/eventpoll.c:2272 at ep_poll fs/eventpoll.c:2272 [inline], CPU#1: udevd/2859
WARNING: fs/eventpoll.c:2272 at do_epoll_wait+0x746/0x1790 fs/eventpoll.c:2837, CPU#1: udevd/2859
Modules linked in:
CPU: 1 UID: 0 PID: 2859 Comm: udevd Not tainted syzkaller #0 PREEMPT(lazy)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 11/06/2026
RIP: 0010:ep_poll fs/eventpoll.c:2272 [inline]
RIP: 0010:do_epoll_wait+0x746/0x1790 fs/eventpoll.c:2837
Code: db 0f 85 17 fb ff ff e8 b8 72 6e ff 65 8b 1d 05 bf e3 0a 31 ff 89 de e8 18 6d 6e ff 85 db 0f 85 fa fa ff ff e8 9b 72 6e ff 90 <0f> 0b 90 e8 92 72 6e ff 48 8b 5c 24 58 48 85 db 0f 85 f1 fa ff ff
RSP: 0018:ffffc90001c6fcc0 EFLAGS: 00010293
RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000
RDX: ffff8881170f9e40 RSI: ffffffff824883a5 RDI: 0000000000000005
RBP: dffffc0000000000 R08: 0000000000000000 R09: 0000000000000000
R10: ffff888116d24003 R11: 0000000000000001 R12: 00007ffffffff000
R13: ffff888117372700 R14: 00007ffd4b58c760 R15: ffff88811739d800
FS: 00007f756c3d3880(0000) GS:ffff888268460000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 000055bfede5cec0 CR3: 000000011723a000 CR4: 00000000003506f0
Call Trace:
<TASK>
__do_sys_epoll_wait fs/eventpoll.c:2845 [inline]
__se_sys_epoll_wait fs/eventpoll.c:2840 [inline]
__x64_sys_epoll_wait+0x194/0x290 fs/eventpoll.c:2840
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x119/0x710 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f756c4c1407
Code: 48 89 fa 4c 89 df e8 38 aa 00 00 8b 93 08 03 00 00 59 5e 48 83 f8 fc 74 1a 5b c3 0f 1f 84 00 00 00 00 00 48 8b 44 24 10 0f 05 <5b> c3 0f 1f 80 00 00 00 00 83 e2 39 83 fa 08 75 de e8 23 ff ff ff
RSP: 002b:00007ffd4b58c600 EFLAGS: 00000202 ORIG_RAX: 00000000000000e8
RAX: ffffffffffffffda RBX: 00007f756c3d3880 RCX: 00007f756c4c1407
RDX: 0000000000000008 RSI: 00007ffd4b58c760 RDI: 000000000000000b
RBP: 00000000000000b9 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000bb8 R11: 0000000000000202 R12: 0000000000000000
R13: 000055bfd6a73100 R14: 000055bfed97d140 R15: 0000000000000000
</TASK>
* Struck through repros no longer work on HEAD.