Developer troubleshooting kernel configuration in terminal with system logs displayed on screen
Kernel Development
William  

CONFIG_BPF_SYSCALL: The Kernel Flag eBPF Programs Need

Most "eBPF won't load" problems trace back to one missing kernel flag, and it is usually this one. The config_bpf_syscall meaning is plain once you read the source: CONFIG_BPF_SYSCALL is the build option that compiles the bpf() syscall into your kernel, the single entry point every BPF program load, every bpftool command, and every systemd cgroup or firewall feature that leans on BPF has to go through. Build a kernel without it and no amount of userspace tooling saves you. This is a kernel decision, not a package you install later.

Last updated: 2026-07-21

What does CONFIG_BPF_SYSCALL mean in the kernel config?

It gates the bpf() syscall entry point, nothing more and nothing less. When it is set, the kernel exposes one system call that loads programs, creates maps, and hands you back file descriptors to manage both. Every loader you touch, from bpftool to libbpf to a raw syscall(), funnels through that one call. Read the bpf() man page and you will see the whole surface is that single syscall multiplexed by a command argument.

Do not confuse it with two nearby options. CONFIG_BPF compiles the generic BPF core, the interpreter and infrastructure, but it does not give userspace a way in. CONFIG_BPF_JIT turns bytecode into native machine code for speed. Neither one lets you load a program from userspace. That job belongs to CONFIG_BPF_SYSCALL, and on most configs it is built in with =y rather than a loadable module, because a syscall table entry cannot appear and disappear at runtime.

The option landed in kernel 3.18. Anything older than that has no bpf() syscall at all, so extended BPF simply does not exist there. That is the historical floor, not the practical one.

Checking whether your running kernel has it enabled

Read the config your kernel was actually built with, not a wiki. Run this first:

  1. grep CONFIG_BPF_SYSCALL /boot/config-$(uname -r) on most distros. You want CONFIG_BPF_SYSCALL=y.
  2. If there is no config file in /boot, try zcat /proc/config.gz | grep CONFIG_BPF_SYSCALL. That works when the kernel was built with CONFIG_IKCONFIG_PROC.
  3. Trust neither fully. Probe the real syscall with strace -e bpf bpftool prog show. If the call returns ENOSYS, the syscall is not there, whatever the config file claims.

The strace step matters because config files lie in odd ways. A file gets copied from another build, a hardening layer blocks the call, or seccomp filters it out. The syscall probe tells you what the running kernel does right now. That is the only answer that counts. bpftool feature probe gives you a fuller map of what is compiled in once the basic call works.

What kernel version do you need for working eBPF?

Aim for 5.10 or newer if you want eBPF that actually works in production – Calico Enterprise pins its eBPF data plane at v5.10 as the minimum, and it is the line I hold when someone asks what to run. The syscall existed back in 3.18, but the program types, helpers, BTF, and CO-RE support you need arrived in waves across later releases.

Here is what happens on an old kernel: the syscall accepts your program, then the load fails because the helper or map type you called landed three releases later. The error rarely says "your kernel is too old." It says something vague about an invalid argument. Match the feature you need to the release that shipped it, and record your baseline with uname -r before you build anything.

Do not chase the newest tag either. Pick the oldest kernel you must support, develop against a modern one, and test on that floor. A program that loads on your dev box and rejects in production usually means the target kernel is older or has symbols stripped. I have lost an evening to exactly that gap.

The dependency chain that rides along with this flag

One missing flag cascades, and the errors point everywhere except the cause. CONFIG_BPF_SYSCALL selects CONFIG_BPF automatically, but the features you care about pull in more. Networking hooks need CONFIG_NET. Cgroup-attached programs, the ones systemd and modern firewalls use, need CONFIG_CGROUP_BPF. Native speed needs CONFIG_BPF_JIT.

Config optionWhat it enablesWhat breaks without it
CONFIG_BPF_SYSCALLThe bpf() syscall entry pointEvery program load, every map, all of bpftool
CONFIG_BPF_JITNative compilation of bytecodeSlower interpreter path, some XDP setups reject
CONFIG_CGROUP_BPFCgroup-attached programssystemd IP filtering, cgroup firewalls
CONFIG_NETNetworking hookstc, XDP, socket filters
CONFIG_DEBUG_INFO_BTFBTF type info for CO-REPortable libbpf programs fail to relocate

Turn off CONFIG_CGROUP_BPF to slim a kernel, and your firewall rules silently stop applying. The syscall still works, so your first instinct is to blame the firewall tool. The real problem is one flag upstream of it.

Why journald and your firewall break without this option

Trace the failure back through the logs instead of guessing at the tool. Systemd uses BPF for several things: per-unit IP access lists, device access in cgroups, and accounting. When the kernel lacks CONFIG_BPF_SYSCALL or CONFIG_CGROUP_BPF, systemd cannot install those filters and says so at boot.

Run journalctl -b -p warning and look for lines about BPF firewalling being unavailable or the bpf() call failing. That tells you the unit's IPAddressDeny= and friends are being ignored, so a service you thought was locked down is wide open. Same story with nftables compiled against BPF-backed features. The rule loads, does nothing, and no error fires unless you read the ring buffer.

Check dmesg -T | grep -i bpf when a network policy behaves wrong. The kernel usually logged the rejection already. A firewall that "isn't working" is often a kernel that was never built to enforce it, and the fix is a rebuild, not another rule.

Mounting the BPF filesystem at /sys/fs/bpf

Mount it with mount -t bpf bpf /sys/fs/bpf and understand what it buys you. The BPF filesystem is where you pin programs and maps so they outlive the process that created them. Without a pin, your program and its file descriptors vanish the moment the loader exits. With one, a separate tool can find the map by path and keep reading.

Make it persistent so it survives a reboot. Most systemd distros already mount bpffs for you through a mount unit, so check findmnt /sys/fs/bpf before adding anything. If it is missing, add this line to /etc/fstab:

bpf /sys/fs/bpf bpf defaults 0 0

Then run mount -a and confirm with findmnt /sys/fs/bpf. This whole mechanism depends on CONFIG_BPF_SYSCALL. The filesystem is a way to name objects the syscall creates, so if the syscall is not compiled in, mounting bpffs gets you an empty directory and nothing to pin. Pinning is also how tools like an eBPF-based intrusion detection setup hand maps between a loader and a reader.

How do you fix "bpf() syscall not supported"?

Work the layers in order, bottom up, so you fix the cause and not the symptom. The error text is generic on purpose, so do not trust it to name the problem.

  1. Confirm the syscall exists: strace -e bpf <your loader>. An ENOSYS return means CONFIG_BPF_SYSCALL is off. Nothing in userspace fixes that. You rebuild or boot a kernel that has it.
  2. Confirm privileges. On many kernels loading needs CAP_BPF and CAP_PERFMON, or plain root. A permission denied here is capabilities, not config. Run the smoke test as root first to split the two.
  3. Confirm the mount if you pin: findmnt /sys/fs/bpf. A missing bpffs throws a path error that looks like a load failure but is not.
  4. Read the verifier log and dmesg -T | tail -20. A rejected program logs the exact instruction and reason. That tells you whether the problem is your bytecode or the kernel.

The mistake I see most is treating a config-level failure as a code bug. If strace shows ENOSYS, stop editing your program. No loader, flag, or reinstall reaches a syscall the kernel never compiled. For the deeper program-load rejects, walking how syscalls get traced with eBPF shows what a healthy attach looks like.

A sane BPF config for a custom or hardened kernel

Set these before you compile, then keep a tested fallback kernel so a bad trim does not leave you dark. I build a minimal kernel for eBPF so I can run small, fast programs without surprises, and the approach stays pragmatic: pick a version, enable the needed options, compile, boot safely, and verify each step. Trimming the kernel can break eBPF quietly, so I keep rollback paths and checks at every stage.

A reasonable baseline to have set:

  • CONFIG_BPF_SYSCALL=y and CONFIG_BPF_JIT=y for the core and native speed.
  • CONFIG_CGROUP_BPF=y so systemd and cgroup firewalls work.
  • CONFIG_DEBUG_INFO_BTF=y for CO-RE portability.
  • CONFIG_NET=y plus CONFIG_NET_CLS_BPF and CONFIG_BPF_EVENTS for networking and tracing hooks.
  • Leave CONFIG_BPF_UNPRIV_DEFAULT_OFF=y unless you have a specific reason to allow unprivileged loads.

After the new kernel boots, validate before you trust it: bpftool feature probe for compiled features, uname -r to confirm you booted the right image, and a tiny known-good program to prove a real load and attach. Do the same rollback-and-verify dance if you are building a trimmed kernel for ARM boards, where a missing flag is easy to ship and hard to notice.

FAQ

Can CONFIG_BPF_SYSCALL be built as a loadable module?

No, and that trips people up. A syscall needs a fixed entry in the kernel's syscall table, which is fixed at build time. So this option is either compiled in with =y or absent. You cannot modprobe your way to a working bpf() call, which is exactly why a rebuild is sometimes the only fix.

What limits does the verifier put on a program once the syscall accepts it?

Two hard ceilings you will meet. Earlier kernels capped eBPF programs at 4096 instructions. The verifier itself walks up to 1 million instructions while checking every path for termination and safe memory access. Hit either wall and the load rejects with a size or complexity error, not a syscall error.

Do I need root, or is CAP_BPF enough?

On newer kernels CAP_BPF plus CAP_PERFMON covers most tracing and loading without full root, which shrinks the attack surface of your loader. Older kernels lump everything under CAP_SYS_ADMIN, so there you are effectively root anyway. Start as root to confirm the toolchain works, then drop to the narrow capabilities once you know which your program needs.

How is this different from the old Berkeley Packet Filter?

Classic BPF was a small in-kernel filter that tools like tcpdump used to drop unwanted packets before copying them to userspace. Extended BPF runs verified programs at syscalls, scheduler events, and network hooks, backed by maps and helpers. The bpf() syscall that CONFIG_BPF_SYSCALL enables is the door to that extended world; classic BPF never needed it.

Why does my program load on one machine and reject on another?

Almost always a kernel gap between the two boxes. A helper or map type your program uses shipped in a later release, or the failing machine has BTF disabled or symbols stripped. Compare uname -r on both and run bpftool feature probe side by side. The machine that rejects is missing something the other one compiled in.

Related: eBPF for Windows: Hooks, HVCI, and Safe Deployment