CVE-2024-1086 — a use-after-free in netfilter — gave attackers a reliable local privilege escalation path on kernels up to 6.6. It was exploited in the wild within weeks of disclosure. Before you can defend against or research vulnerabilities like this, you need to understand exactly where the kernel exposes itself to user-space code.
This post walks through the real attack surface: the interfaces user-space touches, how you enumerate them, and what an attacker does with that information.
Mapping the Kernel Attack Surface
The kernel exposes itself through a handful of well-defined interfaces: system calls, virtual filesystems like /proc and /sys, device nodes, and subsystems like eBPF and io_uring. Each one is a potential entry point. Your first job — as either an attacker or a defender — is to know what’s exposed on a given target.
Start by checking which syscalls are available and what kernel version you’re dealing with. Version matters enormously because a bug fixed in 6.1 may still be live on an unpatched RHEL 8 box.
# Logged in as: jmorrow@prod-web-01 (192.0.2.44)
$ uname -r
5.14.0-427.13.1.el9_4.x86_64
$ cat /proc/sys/kernel/dmesg_restrict
0
$ cat /proc/sys/kernel/kptr_restrict
0
dmesg_restrict = 0 means any unprivileged user can read kernel log output — that includes memory addresses printed during initialization. kptr_restrict = 0 means kernel pointers in /proc are not masked. Together, these two settings hand an attacker a significant advantage: they can read live kernel addresses without needing an infoleak vulnerability.
An attacker would immediately follow up with dmesg | grep -i "kernel base\|_text\|ffffffff" or read /proc/kallsyms to harvest symbol addresses needed to defeat KASLR — kernel address space layout randomization.
$ head -5 /proc/kallsyms
ffffffff81000000 T _stext
ffffffff81000000 T _text
ffffffff81001000 T startup_64
ffffffff81001000 T _stext
ffffffff81002000 T startup_secondary_64
Those ffffffff8xxxxxxx addresses are real kernel symbol locations. With KASLR disabled or bypassed, an exploit can hardcode jump targets. With these addresses readable, KASLR is already neutralized — no heap spray needed to leak the base.
Enumerating eBPF and io_uring Exposure
eBPF is the kernel’s programmable subsystem — it lets user-space load and run bytecode inside the kernel. It was designed for performance-sensitive tracing and networking. It has also produced a steady stream of critical CVEs, including CVE-2021-3490 (out-of-bounds write in the eBPF ALU32 verifier) and CVE-2022-23222 (pointer arithmetic bypass).
Check whether unprivileged eBPF is enabled on your target:
# jmorrow@prod-web-01
$ cat /proc/sys/kernel/unprivileged_bpf_disabled
0
$ cat /proc/sys/kernel/perf_event_paranoid
1
unprivileged_bpf_disabled = 0 means any local user can load eBPF programs. Combined with perf_event_paranoid = 1, unprivileged users can also attach perf events — another common primitive for kernel exploits that need a timing side-channel or a way to trigger a specific code path repeatedly.
A researcher would next check which eBPF program types and map types are accessible. The tool bpftool shows loaded programs and their privileges:
$ bpftool prog list
17: kprobe name sys_enter_hook tag 3b185187f1855c4c gpl
loaded_at 2026-09-18T04:12:31+0000 uid 1001
xlated 208B jited 132B memlock 4096B
$ bpftool map list
3: hash name blocked_ips flags 0x0
key 4B value 4B max_entries 1024 memlock 8192B
That uid 1001 tells you a non-root user loaded a kprobe program. That’s your signal. On a hardened system, eBPF should require CAP_BPF at minimum. If a low-privilege user can load kprobes, they can attach to arbitrary kernel functions, inspect arguments, and — when a verifier bug exists — corrupt kernel memory.
For io_uring, check if it’s enabled and whether the kernel version predates the hardening work done post-6.1:
$ cat /proc/sys/kernel/io_uring_disabled
0
Zero means io_uring is wide open. io_uring has been the source of multiple container escape techniques and privilege escalation chains since 2022. Google disabled it in ChromeOS and Android. Some hardened Linux deployments set this to 2 to block it entirely.
What Defenders Do With This Information
Knowing the attack surface lets you harden it systematically. Start with the sysctl values — restrict them in /etc/sysctl.d/99-hardening.conf:
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
kernel.unprivileged_bpf_disabled = 1
kernel.perf_event_paranoid = 3
kernel.io_uring_disabled = 2
Apply immediately with sysctl --system. These settings don’t break most production workloads but they eliminate whole classes of infoleak and exploitation primitives.
Pair that with seccomp profiles on any service that doesn’t need broad syscall access. A web server doesn’t need bpf(), perf_event_open(), or io_uring_setup(). Blocking them at the syscall level means even a compromised process can’t reach those subsystems.
Use ausyscall --dump to see the full syscall table and identify which ones your application actually uses. Then build a whitelist. It’s tedious once and protective forever.
What To Do Now
Right now, SSH into a Linux box you’re responsible for and run cat /proc/sys/kernel/kptr_restrict /proc/sys/kernel/unprivileged_bpf_disabled /proc/sys/kernel/io_uring_disabled. If any of those return 0, you have exposure that a local attacker — or a compromised container — can use as a launching pad. Fix the sysctl values today, then audit your kernel version against the NVD for any unpatched local privilege escalation CVEs in your exact build.
