Engineering Playbook · Deep Dive Session · 2026E presenter · S notes · Q QR · P pdfeBPF DEEP DIVE01 / 39
Engineering Playbook · Deep Dive Session
eBPF — Programming the Kernel
No reboots. No crashes. Sandboxed programs running safely inside the kernel —
dissected in three chapters, from the virtual machine internals to production on EKS.
Three chapters, one question — "What is happening inside the kernel?"
01
Core Internals
eBPF VM · Verifier · JIT · Maps · CO-RE/BTF · Ring Buffer — grounding the safety guarantees at the kernel-source level
FOUNDATION
02
Observability & Security
Product landscape · memleak · Off-CPU · CPU throttling · Falco vs Tetragon — how eBPF is used in the field
PRACTICE
03
eBPF on EKS
VPC CNI NetworkPolicy · Cilium ENI · Hubble · hands-on benchmarks — EKS already runs on eBPF
AWS
01
Chapter one
A virtual machine inside the kernel
origins · vm · verifier · jit · maps · co-re
The problem
Two ways to extend the kernel
The traditional way to put code in the kernel is a kernel module — powerful, but its fate is tied to the kernel's.
Kernel module (LKM · Loadable Kernel Module)
One bug = kernel panic, whole system down
Recompile & revalidate for every kernel version — no ABI (Application Binary Interface) guarantee
Signing, distribution, audit burden — a hard sell to the security team
Unrestricted memory access — no isolation whatsoever
eBPF program
The Verifier proves safety before load — crashing is impossible
Runs in a register-based sandboxed VM, JIT (Just-In-Time) compiled to native speed
CO-RE (Compile Once – Run Everywhere) ports across kernel versions — no recompilation
Attach / detach instantly — no reboot, no module load
Instrumentation faces the same dilemma — ptrace/strace stop the target and round-trip a context switch per event; untenable in production"Program the kernel safely" — the prototype of this design existed 30 years ago
Prior art — 1992
The prototype answer — tcpdump's packet filter
tcpdump does not run its filter in userspace — it compiles the expression to bytecode,
pushes it down into the kernel, and gets back only the packets that match.
Three things eBPF inherited
Move code to where the data is — inject the filter into the kernel instead of copying every packet out to filter it
A verifiable, restricted instruction set — bytecode, not arbitrary machine code
Programmability without kernel changes — an in-kernel engine does the execution
eBPF keeps these principles intact and redesigns only the engine — the 'e' is for extended
cBPF (classic Berkeley Packet Filter) — 2 registers · 32-bit · sockets only → eBPF (extended BPF) — 11 registers · 64-bit · kernel-wideTo this day, tcpdump filters are translated to eBPF and executed in the kernel
Evolution — 1992 to today
From packet filter to general-purpose kernel VM
The backdrop: a kernel feature takes years to travel from mainline to your distro —
the kernel release cycle could not keep up with container-era networking, observability, and security demands.
Kernel release cycle ≫ the pace at which infra requirements changeModules are dangerous; forking the kernel is a maintenance sinkPrior art DTrace was CDDL-licensed — un-mergeable into the GPL kernel, forcing an independent pathThe answer — build a safe programming layer into the kernel itself
The concept
The core idea — event-driven programs reacting to kernel events
① Event-driven — not a daemon, but a code fragment reacting to kernel events② Sandboxed — only Verifier-proven programs ever run③ Shared state — all data exchange goes through Maps
"Virtual machine"?
What the 'VM' really is — like the JVM, not VMware
The eBPF VM is not hardware virtualization — it is an instruction execution model defined by the kernel.
The bytecode intermediate representation is what makes static analysis by the Verifier possible.
Hardware VM — KVM · VMware
eBPF VM
What is virtualized
Entire CPU · memory · devices
An instruction-set spec — 11 registers · 64-bit · 512 B stack
Reason to exist
OS-level isolation
A verifiable intermediate representation + architecture independence (x86 · ARM · RISC-V)
Execution
Via hypervisor
JIT-compiled → resides in the kernel as native machine code
Runtime overhead
Always present
Effectively zero — same cost as a kernel function call, no interpreter
Four reasons it earns the name "VM"① A dedicated bytecode + 11-register execution layer② Built-in JIT — virtual instructions to native machine code③ Verifier — a sandbox that proves safety before execution④ Maps and Helpers/KFuncs are the only doors — arbitrary kernel memory access is blocked
The real cost is paid once at load time — Verifier analysis + JIT compilationRuntime limits constrain expressiveness, not speed — the 512 B stack and instruction cap are the price of the proof
Register machine
11 registers — designed to map 1:1 onto hardware
Instructions are fixed 8-byte; registers correspond directly to x86_64/ARM64 registers —
which is why the JIT emits native machine code with no extra translation layer.
R0
Return value
Holds the program's exit code and the result of helper function calls
RETURN
R1–R5
Argument passing
Helper call arguments — on entry, the context pointer arrives in R1
ARGUMENTS
R6–R9
Callee-saved
Registers whose values survive across helper calls — home for local state
PRESERVED
R10
Read-only frame pointer
Points to the dedicated 512 B stack and is non-writable — kernel-memory corruption via stack-pointer manipulation is ruled out by design
READ-ONLY
Fixed-length instructions + 1:1 register mapping — minimal JIT translation cost is baked into the designProof point — Meta Strobelight: one code-line fix found via eBPF profiling saved the equivalent of 15,000 servers a year
From C to kernel
From source to kernel — the execution pipeline
11 registers · R0–R10R10 = read-only frame pointer512 B stackNothing runs without passing the Verifier
The gatekeeper
The Verifier — proving safety before execution
Not runtime isolation but load-time static verification. If the proof fails, the kernel refuses the program.
What it checks
Exhaustive CFG (Control Flow Graph) walk — simulates every branch path; one unprovable path rejects the whole program
Termination proof — statically guarantees the program finishes; infinite loops are impossible
Register & stack state tracking — scalar/pointer types and value ranges, including types of values spilled to the stack
PERCPU_* — per-CPU copies for lock-free, high-speed aggregation
STACK_TRACE — the call-stack store populated by bpf_get_stackid()
RINGBUF — the standard event stream since kernel v5.8+
Doors into kernel functionality
Helper functions — a stable, fixed-ABI API (bpf_map_lookup_elem, bpf_ktime_get_ns …)
KFuncs — kernel functions exposed directly; flexible but no stability guarantee
Helpers grow slowly; new capabilities tend to ship as kfuncs first
Each program type has a different callable helper set — enforced by the Verifier
Data structure primer
The ring buffer — an infinite queue from a fixed array and two pointers
eBPF did not invent this — printk/dmesg logs · NIC RX/TX rings · ftrace · perf · io_uring:
a data structure proven for decades all over the kernel.
Why the kernel loves this structure
No malloc on hot paths like interrupts or the scheduler — only writes into pre-allocated slots
Memory usage has a ceiling guaranteed at design time — even a flood stops at the buffer size
Decouples ns-scale producers from ms-scale consumers — the buffer absorbs the gap
One contiguous memory block — shared with userspace via mmap, read without copying
The full-buffer policy defines the use case — logs (dmesg) overwrite the oldest; event pipes (perf · eBPF) drop the new event and count itPerf Buffer and BPF Ring Buffer both descend from this structure — what changed is the next four slides
Legacy path — how it worked
The old path — per-CPU rings: assemble → copy → notify every time
Perf Buffer takes the ring from the last slide and puts one on every CPU — each BPF program writes
only to its own CPU's ring, and the consumer reads and merges as many rings as there are CPUs.
The ring itself is unchanged from the last slide — what differs is the arrangement: one per CPU, records assembled then copied inFour costs fall straight out of this picture (next slide)
Legacy — perf buffer
Legacy Perf Buffer — the price of per-CPU buffers
The legacy channel for shipping events from kernel to userspace was an independent buffer per CPU —
all four limitations derive from that single design choice.
01
Memory over-allocation
Reserves CPU count × buffer size — one buffer overflows while another sits empty, wasted either way
OVER-ALLOC
02
Event reordering
No ordering across CPUs — if a process migrates, fork → exec can arrive reversed
REORDER
03
Late loss detection
Out-of-space is only discovered when copying the record built on the stack — the assembly cost is already paid
LATE FAIL
04
Wakeup storms
Unconditional signal per event, regardless of consumer state — at high event rates the notification cost dominates
SIGNAL STORM
The fix converges to one move — merge the buffers into a single global one (next slide)
New path — how it works
The new path — reserve → write in place → commit, notify only when needed
One global ring — every CPU claims space first with reserve(), writes directly into it,
then publishes with submit(). The copy disappears, and ordering is fixed by reservation order.
The consumer stops at a BUSY record — only committed records are visible, in reservation orderSingle ring · reserve · in-place writes · adaptive notification — what each one fixes is the next slide
Kernel → userspace
BPF Ring Buffer — the new architecture that became the default (v5.8+)
An MPSC (Multi-Producer Single-Consumer) design where all CPUs share one buffer —
answering Perf Buffer's four limitations with four mechanisms.
MPSC
One global shared buffer
Every CPU writes to one buffer; userspace reads it — global event ordering and memory efficiency at once.
ORDER + MEMORY
RESERVE / SUBMIT
Two-phase zero-copy
reserve() claims space first — if short, it fails immediately so the program can react. Write directly into buffer memory, then submit() — the intermediate copy is gone.
§11 REF TRACKING
ADAPTIVE
Adaptive notification
If the consumer is already processing, the signal is suppressed; wake only when it lags — notification cost scales with consumer state, not data volume.
NO SIGNAL STORM
DOUBLE MAP
Virtual-memory double mapping
The same physical pages are mapped twice, back to back — records straddling the boundary read contiguously, no wrap-around handling.
ZERO WRAP COST
Every reserve must end in submit/discard — enforced by the Verifier's reference tracking (§11)Reordering · over-allocation · late loss detection · wakeup storms — all resolved by the single-buffer design
Side by side
Ring buffer generations — same name, different design
Perf Buffer is a ring buffer inside, too — what the eBPF Ring Buffer changed is not the ring itself but
everything about how rings are placed, written, and read.
Legacy ring — Perf Buffer
BPF Ring Buffer (v5.8+)
Layout
Independent ring per CPU — as many rings as CPUs
One global ring — MPSC, shared by all CPUs
Write path
Assemble record on the stack, then copy into the buffer
reserve() → write directly into the buffer → submit() — zero-copy
Out of space
Discovered only at copy time — assembly cost already paid
reserve() fails immediately — the program can react
Event ordering
No cross-CPU guarantee — fork → exec can reverse
Single buffer — global order guaranteed
Boundary handling
Records split at the boundary — read twice and reassemble
Virtual-memory double mapping — contiguous read, no split
Consumer notification
Unconditional signal per event
Adaptive — wake only when the consumer lags
Consumer APIs look alike — perf_buffer__poll ↔ ring_buffer__poll; migration is cheapRing Buffer is the default for new development — Perf Buffer only for pre-v5.8 kernel compatibility
Compile once — run everywhere
CO-RE — compile once, run everywhere
The root problem: kernel struct offsets differ across versions and configs. CO-RE recomputes the offsets at runtime.
BTF dedup in 5 stages — strings → non-reference types → struct/union canonicalization → reference types → compaction; hundreds of MB of DWARF down to a few MBMeasured — x86_64 pt_regs offsets (di=112 · si=104 · dx=96) patched into the bytecode at load timeSlim static binaries with no runtime compilation — unlike BCC (BPF Compiler Collection) embedding clang
The whole picture
The integrated flow — where the pieces interlock
Overlaying everything so far: the load path (compile → verify → JIT) and the runtime channels (Helpers · Maps · Ring Buffer) on one slide.
Top row is the load path, bottom row the runtime channels — § numbers point to the detail slidesWithout patching the kernel — verified bytecode lives inside it
The frontier
Recent features pushing the boundary
Privilege delegation, large memory, userspace execution — eBPF's constraints are being lifted one by one.
KERNEL v6.9+
BPF Token
Instead of granting all of CAP_BPF, issue a delegation token — a container inside a user namespace
loads only the allowed program and map types. Least privilege for multi-tenant environments.
DELEGATION
KERNEL v6.9+
BPF Arena
A large memory region where kernel and userspace share the same pages.
Enables pointer-based data structures (trees, lists) instead of the map API.
SHARED MEMORY
EXPERIMENTAL
bpftime
Kernel uprobes trap via int3 with a heavy kernel round-trip —
bpftime rewrites the binary to run probes in userspace, roughly 10× faster. Kernel entry is removed entirely.
DSL (Domain-Specific Language) one-liners — the standard for ad-hoc diagnosis. Use like awk, then throw away. bpftrace -e 'kprobe:do_sys_open {...}'
AD-HOC
·
BCC
Python + C framework with proven tools like memleak and offcputime. Caveat: runtime clang compilation — heavy to ship.
TOOLBOX
·
libbpf + CO-RE
The production standard — slim static binaries, no per-kernel recompilation. Cilium, Tetragon, and most commercial agents ship this way.
PRODUCTION
↓
bpftool
The standard CLI to inspect and dump programs, maps, and BTF loaded in the kernel. The answer to "what is attached to my kernel right now?"
INTROSPECTION
← quick experimentsproduction rollout →
Ecosystem
Product landscape — observe · detect · enforce
Products on the same eBPF stand in different places — how far they see, and where they start blocking, is the axis.
OBSERVE
Observability · profiling
Pixie — auto-instruments HTTP/gRPC/SQL with zero code change, builds service maps. Parca — always-on low-rate stack sampling, continuous profiling. Hubble — sidecar-free L3–L7 visibility (§36).
NO-INSTRUMENT
DETECT
Runtime detection
Falco — rule-based syscall behavior detection with the largest ruleset (§29). Tracee — Aqua Security; forensics-oriented detection with precise kernel event capture.
ALERT-ONLY
ENFORCE
In-kernel blocking
Tetragon — synchronous verdicts and blocking at LSM hooks (§30). KubeArmor — workload hardening that pre-empts file/process/socket access via BPF-LSM and AppArmor.
IN-KERNEL BLOCK
CORRELATE
Multi-dimensional — CADR
ARMO and peers — correlate kernel eBPF signals with cloud APIs, the K8s control plane, and app code; reconstruct attack stories instead of isolated alerts.
ATTACK STORY
The classification axis — does it ship events to userspace to 'report', or does it 'decide' inside the kernel?The structural gap between detect ↔ enforce is dissected in §29–30
Case study № 1
Tracking memory leaks — all aggregation stays in the kernel
How it works
Probes on the allocation paths — uprobes for user malloc·calloc·realloc·mmap, kprobes for kernel kmalloc → record address → (stack ID · size · timestamp) in a map
On free, delete the entry — long-lived entries = leak candidates
bpf_get_stackid() stores the call stack in a STACK_TRACE map
Userspace only reads the map periodically — no event streaming, minimal overhead
# track outstanding allocations of PID 1234 every 10 s
$ memleak -p 1234 --interval 10
[13:42:01] Top 10 stacks with outstanding allocations:524288 bytes in 128 allocations from stack
malloc+0x1f
grow_request_buffer+0x42
handle_connection+0x8d
start_thread+0xd9
# --older: only "true leaks" unfreed for 60 s+ · omit -p to trace kernel leaks
$ memleak -p 1234 --older 60000
Unlike Valgrind, no restart needed — attaches dynamically to a running process; in-kernel aggregation, no CPU emulationThe cost of uprobes — an int3 breakpoint trap round-trips the kernel per call; at high malloc rates the trap itself is the bottleneckThe workaround removes kernel entry — bpftime runs the probe as a userspace inline hook, ~10× faster (§22)
Case study № 2
Off-CPU analysis — seeing the time spent off the CPU
Why it matters
On-CPU profiles see only half the story — waiting time never shows up in samples
Lock contention, disk I/O, page faults are often the real latency culprits
Record switch-out time → on return, aggregate Δt per stack, in-kernel
The deliverable is an Off-CPU Flame Graph — which stacks waited, and for how long
Case study № 3
Diagnosing CPU throttling — runnable but not allowed to run
Why the usual metrics miss it
The CPU utilization graph sits flat against the limit — busy and strangled look identical
cAdvisor throttle counters give counts and totals only — no idea which code path got delayed by how much
runqlat spikes + Off-CPU stacks (§27) showing no blocking traces (no io_schedule, no futex_wait) → not I/O, not locks: throttling, confirmed
The cross-reading of the two tools is your quantitative basis for resizing limits
Observed combination
Diagnosis
Prescription
runqlat spikes + no blocking traces in stacks — awake but can't run
CFS throttling — CPU allocation too small
Raising limits.cpu is effective
runqlat normal + stacks ending in io_schedule/futex_wait — went to sleep voluntarily
I/O / lock bottleneck — unrelated to CPU
Raising limits does nothing — fix storage or lock design
Runtime security № 1
Falco — sees everything, stops nothing
Detection is strong — the largest ruleset and ecosystemBut blocking is after the fact — by kill time, it's already too late
Runtime security № 2
Tetragon — decides in the kernel, blocks on the spot
Falco = after-the-fact detectionTetragon = synchronous execution blocking (enforcement)Kubernetes-aware — per-Pod/namespace policies via the TracingPolicy CRD (Custom Resource Definition)The next step is correlation — weaving kernel eBPF data with cloud API and app signals into full attack stories (ARMO CADR · Cloud Application Detection & Response)
Operations
Production checklist
01
JIT settings
net.core.bpf_jit_enable=1 — the default on most modern distros. bpf_jit_harden blinds constants against JIT spraying but costs performance — choose by trust boundary.
SYSCTL
02
Memory accounting — a kernel-version fork
Before v5.11: map memory counts against RLIMIT_MEMLOCK — raising ulimit -l is mandatory. v5.11+: switched to cgroup memory accounting — container memory limits now include BPF maps, so size them accordingly.
MEMLOCK
03
Shipping strategy
libbpf CO-RE static binaries instead of BCC runtime compilation. For older kernels without BTF, inject external BTF from BTFHub.
CO-RE
04
Overhead management
Filter as much as possible inside the kernel — cost jumps the moment events cross into userspace. Aggregate in per-CPU maps to eliminate lock contention.
OVERHEAD
03
Chapter three
eBPF on EKS
vpc cni · cilium · hubble · benchmarks
Already in production
The EKS eBPF landscape — already in production
eBPF is not something to adopt — it is already running in your cluster. Understanding it adds a debugging layer.
AWS · v1.14+
VPC CNI NetworkPolicy
The node agent attaches eBPF probes to each Pod's host veth. Policy enforcement with no iptables chains.
POLICY
CNCF
Cilium
The whole CNI (Container Network Interface) in eBPF — kube-proxy replacement, ENI (Elastic Network Interface) routing, O(1) lookups.
CNI
CILIUM
Hubble
L3/L4/L7 flow observability from the same eBPF datapath — service maps with zero extra instrumentation.
OBSERVABILITY
AWS MANAGED
Network Flow Monitor
An agent DaemonSet collecting TCP retransmits and RTT (Round-Trip Time) via the sock_ops hook.
MONITORING
eBPF on EKS № 1
VPC CNI NetworkPolicy — a division of labor: controller and agent
No iptables chains — rule traversal cost does not grow linearly with policy countTroubleshooting step one: check the PolicyEndpoints CRD, not the NetworkPolicyScope — Pod eth0 only · does not apply to host networking / Windows / Fargate
eBPF on EKS № 2
Network Flow Monitor — reading AWS's own eBPF in the source
The NFM agent is open source (Rust · Apache-2.0) — it shows exactly how chapter 1's concepts are used in production code.
HOOK
A single sock_ops program
BPF_PROG_TYPE_SOCK_OPS on cgroup v2 — no packet capture, only TCP-stack callbacks (connect · RTT · retransmit). Always returns BPF_OK — never touches customer connections (source comment)
§12 APPLIED
MAP
Map polling instead of a Ring Buffer
Per-socket stats accumulate in maps; userspace collects every 500 ms — not an event stream → the ring buffer isn't the answer
§18 COUNTERPOINT
SAMPLE
Sampling at the entrance only
Sampling is decided once, at new-socket entry — every event of a tracked socket is collected: internally consistent flow stats
CONSISTENCY
CAP
Drops its own privileges
After loading, drops CAP_SYS_ADMIN/CAP_NET_ADMIN, keeping only CAP_BPF for map reads — least privilege, executed
LEAST PRIV
License history rhymes — GPL-only-helper kernels (SUSE 15 SP5 · Ubuntu 20.04) reject the Apache-2.0 agent; the DTrace CDDL wall again (§6)Ops detail (NHI · enrichment · rollout) — Playbook NFM doc
eBPF on EKS № 3
Cilium ENI mode — an eBPF datapath with no overlay
ENI IPAM (IP Address Management) — Pods get VPC IPs directly, overlay overhead removed
Hubble — flow observability for free, from the same datapath
Absorbs mesh features — Gateway API · mTLS · Bandwidth Manager
Numbers, not vibes
eBPF in numbers — hands-on benchmarks
Measured by this playbook — EKS 1.31 · m6i.xlarge · 5 scenarios, median of 3 runs.
Metric
A · VPC CNI (default)
E · Cilium ENI + full tuning
Reading
TCP throughput
12.41 Gbps
12.40 Gbps
Every scenario saturates the NIC (12.5 Gbps) — throughput doesn't differentiate
Pod-to-Pod RTT p50
4,894 µs
3,135 µs
eBPF datapath + tuning — ~36% lower
UDP loss (iperf3 saturation)
20.4%
0.03%
Bandwidth Manager — EDT (Earliest Departure Time) rate limiting; a feature difference, not performance
HTTP p99 @1k QPS
10.9 ms
9.9 ms
The gap narrows at the application level — the CNI is only part of the bottleneck
Conclusion: pick a CNI on latency · features · operating model — not throughputService meshes follow the same arc — sidecars (a proxy per Pod) → eBPF + node proxies (Cilium Mesh · Istio Ambient)
Take these home
Read the kernel, and the cluster comes into focus
1
The Verifier is the contract
eBPF's safety is a load-time proof, not runtime isolation. Constraints like the 512 B stack and bounded loops are the price of that proof.
2
CO-RE changed shipping
BTF relocation ended per-kernel recompilation. libbpf static binaries are the production standard — keep BCC for experiments.
3
On EKS it's already table stakes
The NetworkPolicy agent, Cilium, and NFM all run on eBPF — and NFM's source is public. The moment you understand it, you gain a debugging layer.
One sentence through all three chapters — eBPF is a verified programming layer that changes the kernel's behavior without changing the kernel.
Wrap-up
The kernel is no longer a black box
Resources to go deeper — questions and discussion welcome.