Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
RuntimeAugust 17, 202612 min read

Runtime Reachability Beyond eBPF

eBPF is a strong runtime reachability primitive where you can reach the kernel. Here's what to do on the compute where you can't.

O
O3 Security Team
Research & Engineering
Reachability illustration
Key takeaways
  • eBPF confirms a vulnerable function actually executed by hooking the kernel function it calls into, but it only works where a program can be loaded into the kernel.
  • AWS Fargate and similar serverless container compute give tasks no access to the underlying host, so kernel-level tools including eBPF-based agents structurally cannot run there.
  • Runtime reachability doesn't require a kernel hook. Reading a process's own memory mappings and loaded packages from outside the application works identically whether or not the kernel is reachable.
  • Function-level precision, not just library-level detection, is achievable for interpreted languages like Python and Node by hooking the language runtime itself, with instrumentation that removes itself after the first observed call.
  • Runtime reachability, with or without eBPF, proves a path was reached. It cannot prove a path is unreachable, since code that hasn't fired yet during observation isn't the same as code that never will.

A scanner flags a critical CVE across your fleet: some of it on bare EC2 hosts, some in Kubernetes, some on ECS running on EC2, and some on ECS Fargate. You want to know which of those instances are actually calling the vulnerable function right now, not just shipping it. On three of those four compute models, a kernel-level tool can answer that. On the fourth, it can't get near the kernel at all.

Runtime reachability means watching a running system and recording which code paths actually fire, then checking that against your vulnerability list. eBPF is one way to do that: a set of kernel primitives that let you observe execution from inside the kernel without modifying the application. It's a good way, and a widely used one. It's also not the only way, and it has a real ceiling: it needs kernel access, and not every place your workload runs gives you that.

This piece covers eBPF fairly first: what a kprobe and a tracepoint actually are, how a program attaches to the kernel safely, and why so many runtime security tools reach for it. Then it covers the constraint that breaks eBPF-only approaches on compute like Fargate, and what runtime reachability looks like when it's built to work whether or not the kernel is reachable.

eBPF, from the kernel up

eBPF stands for extended Berkeley Packet Filter, a name that undersells what it does today. The original BPF, from 1992, was a small bytecode format for filtering packets in the kernel without a context switch to userspace for every one. eBPF generalized that idea. It's a restricted, verifiable instruction set that runs inside the kernel, triggered by events. It now attaches to far more than packets: syscalls, function entry and exit, scheduler decisions, and USDT probes in userspace binaries.

Three things make this a genuinely good primitive for observability, security tooling included:

  1. You don't recompile the kernel. eBPF programs get JIT-compiled and loaded into a running kernel at runtime.
  2. You don't modify the application. The hook attaches from outside the process, so there's no library to link, no agent to inject into the runtime, no sidecar proxy in the data path.
  3. You don't get to crash the kernel. Every program passes through an in-kernel verifier before it's allowed to load, which is the part that makes 'let third-party code run in kernel space' something you can actually ship in production.

Kprobes: dynamic hooks into any kernel function

A kprobe is a dynamic hook. It lets you attach eBPF code to the entry or return of almost any kernel function, by name, at load time. The kernel patches in a trap at that instruction address; when execution hits it, control diverts into your eBPF program, then returns to the original code path. No recompilation, no reboot.

The tradeoff is stability. Kprobes hook internal kernel functions, and internal kernel functions are not a stable ABI. A function name or its argument layout can change between kernel versions, which means a kprobe written against one kernel release can silently stop matching, or match the wrong thing, on another. That's the cost of the flexibility: you can hook almost anything, but you're coupling to implementation detail, not a contract.

Tracepoints: stable hooks, narrower coverage

Tracepoints are the other end of that tradeoff. They're hook points the kernel maintainers deliberately placed and committed to keeping stable across releases, exposed under /sys/kernel/tracing/events/. You get far fewer of them than you'd have kprobe targets, but the ones that exist won't move under you.

In practice, an eBPF-based agent uses both: tracepoints where one exists and covers the event you need, kprobes for everything else, including the deep, code-path-specific hooks that no tracepoint was ever going to cover.

Note

Uprobes work the same way as kprobes but target userspace binaries instead of kernel functions, useful for hooking library calls inside an application process without modifying it. LSM BPF hooks into Linux Security Module decision points for policy enforcement rather than pure observation. Both extend the same attach-without-modifying model beyond the kernel/syscall boundary, and both still need the same thing kprobes need: a kernel to attach to.

The verifier: why this is safe to run in production

None of the above matters if loading a bad eBPF program can panic the kernel. That's what the verifier exists to prevent. Before any program is allowed to load, the kernel statically walks every possible execution path through it. It rejects the program unless it can prove three things: the program terminates, it never touches memory out of bounds, and it never reads an uninitialized value. Verification is a safety check, not a security policy. It guarantees the program won't crash or corrupt the kernel, not that its logic is doing something sane. That distinction is why eBPF-based agents are viable in latency-sensitive production environments on the hosts they can reach.

That last clause matters more than it sounds like it should. Everything above, kprobes, tracepoints, the verifier, assumes you have a kernel to load a program into and the privilege to load it. That assumption holds on a bare host, on a Kubernetes node you or your cloud provider manages, and on ECS tasks running on EC2. It doesn't hold everywhere.

The constraint that breaks eBPF-only tooling

AWS Fargate runs each ECS or EKS task on infrastructure AWS controls end to end, and that isolation is the point of the product: no privileged containers, no CAP_SYS_ADMIN or CAP_NET_ADMIN, and per AWS's own documentation, no access to the underlying host at all, not for the customer and not for AWS operators working on your behalf. That's a deliberate security boundary, not an oversight. It also means there is no kernel to attach a kprobe to, because there's no host-level access to attach one from.

This isn't a fringe edge case. Fargate and equivalent serverless container compute are a normal, common deployment target precisely because they remove host management from the picture, and vendors that ship eBPF-based runtime security have had to build separate, non-eBPF fallbacks to cover it. Datadog's own workload protection docs describe a ptrace-based agent specifically for 'eBPF disabled environments, such as AWS Fargate,' which is a tell: the eBPF story and the Fargate story are two different code paths even for vendors who lead with eBPF everywhere else.

So a fleet spanning bare host, Kubernetes, ECS on EC2, and ECS on Fargate has a real split down the middle. Three of those four give you kernel access. The fourth, by design, does not. A runtime reachability strategy that depends on eBPF as its core mechanism inherits that split: full visibility on most of the fleet, and a blind spot on exactly the compute model chosen because it needed the least operational overhead.

What static reachability actually gets you, and where it stops

Static reachability analysis reads source code, without running it, and tries to prove whether a call path exists from an entry point to a specific vulnerable function. It's a real technique that answers a real question: is a call path even possible. Our earlier deep dive on this, built against a deliberately vulnerable FastAPI app called DropVault, covers how that call graph gets constructed and where it breaks down (see the related read below).

The honest summary: static analysis has to choose between soundness (catch every real call) and completeness (never report a fake one), and in a dynamic language it can't fully have both. Three specific patterns break it consistently:

  • Dynamic dispatch. When the method actually invoked depends on a runtime type or a config value, a static call graph has to either guess or enumerate every possibility, and both produce noise.
  • Reflection and metaprogramming. Code that calls a function by string name, or a framework that wires handlers by convention (Spring, Django, dependency injection containers), has no static edge for the analyzer to find. The call exists; the source code doesn't say so directly.
  • Lazy-loaded and conditionally imported modules. A module imported behind a feature flag, an environment check, or a plugin loader is syntactically present and semantically maybe-never-runs. Static analysis sees the import; it can't see the runtime condition.

None of this makes static reachability wrong. It makes it a proof of possibility, computed once, against source. It cannot tell you whether that possible path is the one your production traffic actually walks, today, on this build, with this config, on this compute model. That's a different question, and it needs a different kind of evidence, gathered from the running system itself.

Runtime reachability without depending on the kernel

If eBPF's ceiling is kernel access, the fix isn't to accept a gap on Fargate-style compute, it's to stop making kernel access a requirement in the first place. That means observing execution from a layer that doesn't need host privilege to work, and reserving anything that does need deeper access for where it's actually available.

Think of it in two layers, doing different jobs:

  1. An out-of-process layer that reads a process's own memory mappings, the list of files the process has actually loaded into memory, and matches each one against that container's package metadata. It doesn't touch application code and doesn't require a kernel hook, so it runs the same way on a bare host, inside Kubernetes, on ECS-EC2, and on ECS Fargate, provided it can see the process's memory map at all, which needs only ordinary process-inspection access, not kernel privilege. This is the layer that gives you library-level reachability everywhere your workload actually runs, including the compute model where kernel-level tooling structurally cannot follow.
  2. An in-process layer, scoped to interpreted languages, that hooks into how the language runtime itself loads modules and resolves function calls, giving finer precision than 'this library loaded.' It watches for a specific, pre-selected set of vulnerable functions, and once one of them is called for the first time, it records that observation and then removes its own hook, so steady-state overhead stays near zero instead of taxing every call on the hot path for the life of the process.

That second layer is deliberately scoped. Function-level precision works for interpreted-language packages, where the language runtime exposes enough structure to observe a specific call cleanly, in Python and Node, for instance. It's not claimed for compiled or native code, where that structure doesn't exist in the same way. For compiled dependencies, the out-of-process layer still gives you library-level reachability: this shared library is loaded and mapped into this process, just not a function-level trace inside it.

Both layers report through one common identity model. The out-of-process layer, because it inspects the operating system directly, can independently verify which container and process actually made an observation, rather than trusting a self-report from inside the application. On compute like Fargate, where a container can't always describe its own identity that way, the platform's own container metadata fills that gap. Either way, an observation is tied to a real container and process, not inferred.

Key takeaway

The point isn't that eBPF is wrong. It's that a runtime reachability engine which only works where the kernel is reachable will always have a gap shaped exactly like your serverless compute. A layered approach that doesn't require kernel access closes that gap without needing eBPF to be available everywhere it would otherwise be useful.

A tool that can only see the hosts it's allowed to touch will only ever be as complete as the hosts it's allowed on.

Two comparisons: what each approach proves, and where each one runs

DimensionStatic reachabilityRuntime reachability
Detection pointSource code / call graph, before deploymentA running process, observed in production
What it provesA call path from entry point to vulnerable function is theoretically possibleThat call path was actually executed, by this process, at this time
Blind spotsDynamic dispatch, reflection, lazy-loaded modules, DI-wired handlersCode that hasn't run yet, including rare or attacker-only paths never exercised in normal traffic
Performance overheadNone at runtime; cost is analysis time in CI/CDNear zero when instrumentation is scoped to known-vulnerable functions and removes itself after first use
False positive rateHigher; conservative call graphs flag paths that exist syntactically but never executeVery low for what it reports; a captured execution event is direct evidence, not inference
When it can runPre-deployment, on every commit or buildOnly once the workload is running, so it can't gate a merge the way static analysis can
Static Reachability vs. Runtime Reachability
DimensioneBPF-only runtime toolingLayered runtime reachability
Bare hostWorks, full kernel access availableWorks
Kubernetes (self-managed nodes)Works, node-level kernel access availableWorks
ECS on EC2Works, host-level kernel access availableWorks
ECS Fargate / serverless containersCannot run: no privileged access, no access to the underlying hostWorks: out-of-process layer needs no kernel or host privilege
Coupling to kernel internalsKprobes tie to kernel-version-specific function names and layoutsOut-of-process layer avoids that coupling entirely
Function-level precisionPossible via uprobes, where host access allows itAvailable for interpreted-language packages; library-level everywhere else
Steady-state overheadOngoing, bounded by hooked-event volumeNear-zero once first-observation instrumentation self-removes
eBPF-Only Runtime Tooling vs. a Layered (Kernel-Independent) Approach

Where runtime reachability still falls short

This limitation holds regardless of mechanism, eBPF or otherwise. Absence of a runtime signal is not absence of risk. A vulnerable function that hasn't fired yet during the observation window looks identical, from the trace data, to a vulnerable function that can never fire. Those are very different security postures, and no runtime observation technique can tell them apart from silence alone. An attacker-only code path, one that only executes when someone deliberately crafts the input to trigger it, is exactly the kind of thing that produces zero runtime signal right up until the day it's exploited.

This is why runtime reachability works best as a second, corroborating pass over what static reachability already flagged as theoretically possible, not as a replacement for it. Static analysis keeps the wide net; runtime observation tells you which parts of that net have live evidence behind them, right now, in this environment, on this compute model. A finding with both a static path and a confirmed runtime hit is about as close to 'stop what you're doing and fix this' as vulnerability prioritization gets. A finding with a static path and no runtime hit yet is still worth tracking, just not with the same urgency.

Frequently asked questions

What is eBPF reachability analysis?

+
eBPF reachability analysis uses eBPF kernel hooks, kprobes, tracepoints, and uprobes, to observe whether a vulnerable code path actually executes in a running application. Instead of inferring a call path from source code, it records real kernel-level events tied to a specific process, confirming execution rather than possibility, on any host where the kernel is reachable.

Does runtime reachability require eBPF?

+
No. eBPF is one way to observe execution, and a good one where you have kernel access, but it's not the only mechanism. An out-of-process layer that inspects a process's memory mappings and loaded packages from outside the application can confirm reachability without any kernel hook, which matters on compute like AWS Fargate where kernel access doesn't exist.

Why can't eBPF-based tools monitor AWS Fargate workloads?

+
Fargate tasks run on infrastructure AWS controls entirely: no privileged containers, restricted Linux capabilities like CAP_SYS_ADMIN, and per AWS's own documentation, no access to the underlying host for customers or AWS operators. eBPF needs kernel access to attach a program, and that access doesn't exist on Fargate by design.

How does runtime reachability work without eBPF?

+
An out-of-process layer reads a process's own memory mappings and matches loaded files against container package metadata, which needs no kernel or host privilege and works the same on bare host, Kubernetes, ECS-EC2, or Fargate. A second, in-process layer adds function-level precision for interpreted languages by hooking the language runtime itself, then removing its own instrumentation after first observation.

What is the difference between a kprobe and a tracepoint in eBPF?

+
A tracepoint is a hook point the kernel maintainers deliberately placed and keep stable across kernel versions, but there are relatively few of them. A kprobe can attach to almost any kernel function dynamically, giving far broader coverage, at the cost of being tied to internal kernel implementation details that can shift between versions.

Can runtime reachability prove a vulnerability is unreachable?

+
No, with or without eBPF. Runtime tools only report what they observe executing during the window they're watching. A path that hasn't fired yet, including rare attacker-only paths, produces no signal even though it can still be exploited. That's why runtime evidence should corroborate static analysis, not replace it.

See your full attack chain.
Code, build, runtime. One platform.