Skip to content

ChannelException for Bad file descriptor thrown in IoUringIoHandler.wakeup #16716

@tsegismont

Description

@tsegismont

In the Vert.x core CI job for io_uring, there are intermittent failures like this:

026-04-27T15:06:35.6064561Z [globalEventExecutor-1-30] WARN io.netty.util.concurrent.DefaultPromise - An exception was thrown by io.vertx.core.impl.VertxImpl$2.operationComplete()
2026-04-27T15:06:35.6067535Z io.netty.channel.ChannelException: eventfd_write(...) failed: Bad file descriptor
2026-04-27T15:06:35.6069139Z 	at io.netty.channel.uring.Native.eventFdWrite(Native Method)
2026-04-27T15:06:35.6070941Z 	at io.netty.channel.uring.IoUringIoHandler.wakeup(IoUringIoHandler.java:634)
2026-04-27T15:06:35.6072882Z 	at io.netty.channel.SingleThreadIoEventLoop.wakeup(SingleThreadIoEventLoop.java:265)
2026-04-27T15:06:35.6074531Z 	at io.netty.util.concurrent.SingleThreadEventExecutor.shutdown0(SingleThreadEventExecutor.java:821)
2026-04-27T15:06:35.6077200Z 	at io.netty.util.concurrent.SingleThreadEventExecutor.shutdownGracefully(SingleThreadEventExecutor.java:835)
2026-04-27T15:06:35.6079283Z 	at io.netty.util.concurrent.MultithreadEventExecutorGroup.shutdownGracefully(MultithreadEventExecutorGroup.java:191)
2026-04-27T15:06:35.6081023Z 	at io.vertx.core.impl.VertxImpl$2.operationComplete(VertxImpl.java:995)
2026-04-27T15:06:35.6082401Z 	at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:604)
2026-04-27T15:06:35.6083799Z 	at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:571)
2026-04-27T15:06:35.6085138Z 	at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:506)
2026-04-27T15:06:35.6086387Z 	at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:650)
2026-04-27T15:06:35.6087947Z 	at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:639)
2026-04-27T15:06:35.6089258Z 	at io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:111)
2026-04-27T15:06:35.6090723Z 	at io.netty.util.concurrent.MultithreadEventExecutorGroup.lambda$new$0(MultithreadEventExecutorGroup.java:119)
2026-04-27T15:06:35.6092273Z 	at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:604)
2026-04-27T15:06:35.6093595Z 	at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:571)
2026-04-27T15:06:35.6094877Z 	at io.netty.util.concurrent.DefaultPromise.access$200(DefaultPromise.java:37)
2026-04-27T15:06:35.6096035Z 	at io.netty.util.concurrent.DefaultPromise$1.run(DefaultPromise.java:517)
2026-04-27T15:06:35.6097582Z 	at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:148)
2026-04-27T15:06:35.6098990Z 	at io.netty.util.concurrent.GlobalEventExecutor$TaskRunner.run(GlobalEventExecutor.java:286)
2026-04-27T15:06:35.6100273Z 	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
2026-04-27T15:06:35.6101558Z 	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
2026-04-27T15:06:35.6102744Z 	at java.base/java.lang.Thread.run(Thread.java:829)

I think the following could be the suite of events.

During shutdown, the global event executor tries to wake up the IoHandler. In IoUringIoHandler, an attempt to write to event file descriptor is made, because the value of eventfdAsyncNotify was false when read.

    @Override
    public void wakeup() {
        if (!executor.isExecutorThread(Thread.currentThread()) &&
                !eventfdAsyncNotify.getAndSet(true)) { // <-- was false when read at this point
            // write to the eventfd which will then trigger an eventfd read completion.
            Native.eventFdWrite(eventfd.intValue(), 1L);
        }
    }

In theory, submitted events are gated by eventfdAsyncNotify, as the comment suggests above io.netty.channel.uring.IoUringIoHandler#drainEventFd.

But if the globlal event executor thread is preempted before the Native.eventFdWrite(eventfd.intValue(), 1L); call, the event loop thread can:

  • enter IoUringIoHandler#prepareToDestroy
  • invoke Native.eventFdWrite(eventfd.intValue(), 1L);
  • enter IoUringIoHandler#handleEventFdRead
  • reset eventfdAsyncNotify to false
  • enter IoUringIoHandler#drainEventFd
  • see no event pending (wrong)
  • enter IoUringIoHandler#completeRingClose
  • close the file descriptor

Then when the global event executor thread is rescheduled, it tries to write to a deleted file descriptor.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions