Below the Syscall Line: How eBPF and io_uring Rootkits Slip Past Linux EDR
Most Linux runtime security watches system calls. A growing class of stealth techniques, eBPF abuse and the io_uring asynchronous-I/O interface, does its work without the syscalls those tools hook.
- Linux Rootkits

Bottom line. If your Linux detection relies on system-call monitoring, assume it can be blind to io_uring-based activity. This is a demonstrated technique (ARMO’s Curing proof-of-concept), not yet a confirmed in-the-wild campaign, but the gap is real today. Prioritise LSM/KRSI-based visibility, baseline and alert on anomalous io_uring use, and stop treating syscall-only telemetry as complete.
Background
io_uring is a Linux asynchronous-I/O interface introduced in 2019 with kernel 5.1. Instead of issuing a system call per operation, a process submits requests into ring buffers shared between user space and the kernel, which are serviced asynchronously. Because the work no longer flows through per-operation syscalls, security tools that monitor syscalls (via eBPF, ptrace, seccomp or kernel modules) can be completely blind to it. io_uring supports 61 operation types, file reads and writes, creating and accepting network connections, changing file permissions and reading directories, which is more than enough surface to build a full rootkit.
The risk is well established: Google disabled io_uring by default on Android and ChromeOS, and researchers (notably Daniel Teixeira at Form3 in 2022) showed it bypassing syscall monitoring years ago. In April 2025, ARMO turned the idea into a working proof-of-concept rootkit, ‘Curing’, and demonstrated the blind spot against widely used tools. Separately, eBPF. The technology most modern Linux EDRs are built on, can itself be undermined (ARMO has shown eBPF map tampering and time-of-check/time-of-use attacks), a reminder that observability instrumentation is not the same as tamper-resistant security enforcement.
What we observed
What the public research actually demonstrates:
- Syscall-free I/O: io_uring submits operations through shared ring buffers rather than individual system calls. Its 61 op types cover file read/write, network connect/accept, file-permission changes and directory reads, enough to operate without the syscalls (openat, read, write, connect) that EDRs hook.
- The Curing PoC: ARMO’s rootkit (written in Go and C, conceived at the 38C3 conference) pulls commands from a remote C2 and reads files, writes files and creates symbolic links entirely over io_uring. Running it under strace shows no attack-relevant syscalls; process execution is currently the one io_uring operation still blocked upstream.
- A real tool-coverage gap: under default configuration ARMO found Falco entirely blind (even with custom rules) and Microsoft Defender for Endpoint on Linux missing sensitive-file reads, an EICAR drop, XMRig execution and a low-reputation network callout, only its fanotify-based File Integrity Monitoring caught file changes. Tetragon can detect io_uring activity but only when operators add Kprobe/LSM hooks; SentinelOne confirmed its agent was unaffected.
- eBPF cuts both ways: most modern Linux EDRs are eBPF-based, but if they attach at the syscall layer they inherit the same blind spot. The durable fix, KRSI (Kernel Runtime Security Instrumentation), is itself eBPF attached to LSM hooks, which sit in the kernel’s enforcement path and are far harder to sidestep than syscall hooks.
How io_uring evades syscall monitoring
The durable fix is LSM/KRSI visibility, hooks in the kernel’s enforcement path that see the operation regardless of whether a syscall was used.
Attribution
This is security research and a public proof-of-concept, not an attributed intrusion. There is no confirmed in-the-wild io_uring rootkit at time of writing, and no threat actor is named, Curing is a defensive PoC released by ARMO so teams can test their own tooling. The honest read is that this is an emerging capability with a low barrier to weaponisation (the code is public and the interface ships in every modern Linux kernel), so defenders should close the visibility gap proactively rather than wait for a confirmed campaign.
Confidence: 90%.
You cannot alert on a system call that never happens, and io_uring is built so it never happens.
Tactics, techniques & procedures (MITRE ATT&CK)
Techniques below are drawn directly from the analysed samples/reporting – no technique is asserted without evidence:
| Tactic | Technique | ID | Evidence |
|---|---|---|---|
| Defense Evasion | Rootkit | T1014 | ARMO’s Curing operates as a rootkit that performs its actions over io_uring to stay invisible to host security tooling; the same class covers eBPF-based rootkits. |
| Defense Evasion | Impair Defenses: Indicator Blocking | T1562.006 | io_uring performs file and network I/O via shared ring buffers, so syscall-monitoring EDRs (e.g. Falco, Defender for Endpoint) never observe the operations, telemetry is effectively suppressed at the source. |
| Command and Control | Application Layer Protocol | T1071 | The Curing client pulls commands from a remote C2 using io_uring network operations instead of socket syscalls. |
| Collection | Data from Local System | T1005 | Curing reads local files (including sensitive paths) via io_uring read operations, bypassing read()/openat() syscall visibility. |
Detection
Copy-ready hunting query:
# io_uring is *set up* via a syscall even though its operations are not -- audit the setup call.
# x86_64 syscall numbers: io_uring_setup=425, io_uring_enter=426, io_uring_register=427
auditctl -a always,exit -F arch=b64 -S io_uring_setup -S io_uring_enter -k io_uring_activity
ausearch -k io_uring_activity | aureport -syscall -summary
# Baseline + alert on unexpected io_uring users (most workloads never call it):
# Flag any process invoking io_uring_setup that is NOT on a known-good allowlist
# (databases, proxies and some language runtimes legitimately use io_uring).
# Prefer LSM / KRSI visibility over syscall hooks so io_uring ops are still seen:
# Falco LSM plugin, or Tetragon with LSM + Kprobe hooks on file_open,
# bprm_check_security and socket_connect (the kernel enforcement path).
# Reduce attack surface where io_uring is unused:
# sysctl -w kernel.io_uring_disabled=2 # kernel 6.6+, fully disables io_uring
# or seccomp-filter io_uring_setup/enter/register for workloads that don't need it.
Recommendations
- Stop relying on syscall-only monitoring: add LSM-based visibility (KRSI / eBPF-on-LSM) via Falco’s LSM plugin or Tetragon’s LSM + Kprobe hooks so io_uring operations are observed in the kernel’s enforcement path.
- Baseline io_uring usage and alert on anomalies, most workloads never touch it, so a process that suddenly starts using io_uring is a strong signal (tune out databases, proxies and runtimes that legitimately use it).
- Disable io_uring where it is not needed: set sysctl kernel.io_uring_disabled=2 (kernel 6.6+) or seccomp-filter the io_uring_setup/enter/register syscalls; Google already disables it by default on Android and ChromeOS.
- Validate your EDR honestly: run ARMO’s open-source Curing PoC against your stack to see what your tooling actually catches, and keep fanotify-based File Integrity Monitoring as a backstop for file changes.
- Keep kernels patched (io_uring has a long vulnerability history) and track KRSI/LSM-hook support in your distribution as the strategic detection foundation.



