Problem
uvloop vendors libuv 1.48.0 (vendor/libuv submodule SHA e9f29cb9… → tag v1.48.0, 2024-02-07). On kernels ≥ 5.10.186 that libuv turns on io_uring by default — an IORING_SETUP_SQPOLL ring for async file ops and an IORING_OP_EPOLL_CTL batching ring for epoll_ctl. Nothing in the stack opts in:
app → uvloop.run() → uvloop → bundled libuv 1.48 → io_uring_enter
Observed via an eBPF tracepoint on io_uring_setup/io_uring_enter: uvloop-driven processes issue those syscalls during normal startup, before any app file I/O.
Why
io_uring bypasses the syscall boundary seccomp filters on. Ops inside the ring (openat, read, write, connect) aren't individual syscalls — seccomp can only allow/deny io_uring_enter wholesale, not the ops inside. So if you relax the profile to silence the inevitable EPERM noise, you punch a hole through the whole seccomp profile. The default Kubernetes RuntimeDefault profile denies io_uring_setup (#426), which is correct — but means uvloop-driven processes noisily fail into libuv's epoll+threadpool fallback on every loop init.
libuv already agreed: v1.49.0 disabled SQPOLL io_uring by default (#4492). The gap isn't libuv — it's consumers still bundling 1.48.
uvloop is the layer that controls io_uring exposure for most of the Python ecosystem (via uvicorn[standard] → uvloop). Today that choice is "io_uring on by default, suppressed only by seccomp."
Proposal
- Bump
vendor/libuv to ≥ 1.49.0. Consistent with prior uvloop libuv bumps (v0.15.0, v0.14.0, etc.). Removes the SQPOLL ring from the default footprint.
- Document the io_uring behavior + the
UV_USE_IO_URING=0 escape hatch in the README. (Note: the env var fully gates io_uring in 1.48; in 1.49+ the IORING_OP_EPOLL_CTL batching ring is no longer gated by it — verify against whatever version you ship.)
- Optionally expose a uvloop-level switch (
UVLOOP_USE_IO_URING=0) so apps don't have to reach into libuv's env var.
References
Problem
uvloopvendors libuv 1.48.0 (vendor/libuvsubmodule SHAe9f29cb9…→ tag v1.48.0, 2024-02-07). On kernels ≥ 5.10.186 that libuv turns on io_uring by default — anIORING_SETUP_SQPOLLring for async file ops and anIORING_OP_EPOLL_CTLbatching ring forepoll_ctl. Nothing in the stack opts in:Observed via an eBPF tracepoint on
io_uring_setup/io_uring_enter: uvloop-driven processes issue those syscalls during normal startup, before any app file I/O.Why
io_uring bypasses the syscall boundary seccomp filters on. Ops inside the ring (openat, read, write, connect) aren't individual syscalls — seccomp can only allow/deny
io_uring_enterwholesale, not the ops inside. So if you relax the profile to silence the inevitable EPERM noise, you punch a hole through the whole seccomp profile. The default KubernetesRuntimeDefaultprofile deniesio_uring_setup(#426), which is correct — but means uvloop-driven processes noisily fail into libuv's epoll+threadpool fallback on every loop init.libuv already agreed: v1.49.0 disabled SQPOLL io_uring by default (#4492). The gap isn't libuv — it's consumers still bundling 1.48.
uvloop is the layer that controls io_uring exposure for most of the Python ecosystem (via
uvicorn[standard]→ uvloop). Today that choice is "io_uring on by default, suppressed only by seccomp."Proposal
vendor/libuvto ≥ 1.49.0. Consistent with prior uvloop libuv bumps (v0.15.0, v0.14.0, etc.). Removes the SQPOLL ring from the default footprint.UV_USE_IO_URING=0escape hatch in the README. (Note: the env var fully gates io_uring in 1.48; in 1.49+ theIORING_OP_EPOLL_CTLbatching ring is no longer gated by it — verify against whatever version you ship.)UVLOOP_USE_IO_URING=0) so apps don't have to reach into libuv's env var.References