Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
RuntimeAugust 17, 202615 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. The name undersells what it does today. The original BPF, from 1992, was a small format for filtering network packets inside the kernel. It did this without constantly switching control back and forth to the application. eBPF took that same idea and opened it up. It's a small, safety-checked program that runs inside the kernel, triggered by events. Today it attaches to far more than packets. It can watch system calls, the start and end of specific functions, scheduling decisions, and events inside application code itself.

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

  1. You don't recompile the kernel. eBPF programs are compiled and loaded into a running kernel while it's already running.
  2. You don't modify the application. The hook attaches from outside the process. There's no library to link. No agent to inject. No extra piece sitting in the data path.
  3. You don't get to crash the kernel. Every program is checked for safety before it's allowed to load. That check is what makes running someone else's code inside the kernel something you can actually ship in production.

Kprobes: dynamic hooks into any kernel function

A kprobe is a dynamic hook. It lets you attach code to the start or end of almost any function inside the kernel, just by name. The kernel drops in a trap at that spot. When execution reaches it, control jumps into your code, runs, then returns to what it was doing. No recompiling. No reboot.

The tradeoff is stability. Kprobes hook internal kernel functions. Those functions are internal, which means the kernel team never promised they'd stay the same. A function's name or its inputs can change between kernel versions. A kprobe built for one kernel release can quietly stop matching on the next one, or worse, match something else. That's the cost of the flexibility. You can hook almost anything, but you're relying on how the kernel happens to be built right now, not on a stable promise.

Tracepoints: stable hooks, narrower coverage

Tracepoints are the other end of that tradeoff. The kernel team placed these hooks on purpose. They promised to keep them stable across releases. You get far fewer of them than you'd have kprobe targets. But the ones that exist won't move on you.

In practice, an eBPF-based agent uses both. It uses a tracepoint wherever one already covers the event it needs. It falls back to a kprobe for everything else, including the specific, deep hooks that no tracepoint was ever built to cover.

Note

Two related tools extend this same idea. Uprobes work like kprobes, but they target code inside an application instead of the kernel, useful for watching a library call without changing the application itself. LSM hooks tie into the kernel's security-decision points instead of just watching passively. Both still need one thing that kprobes need: a kernel to attach to.

The verifier: why this is safe to run in production

None of this matters if a bad eBPF program could crash the kernel. That's exactly what the verifier exists to stop. Before any program is allowed to load, the kernel checks every possible path through it in advance. It rejects the program unless it can prove three things. The program has to finish, not run forever. It can't touch memory outside what it's allowed to. And it can't read a value that was never set. This check is about safety, not about judging whether the program's logic makes sense. It guarantees the program won't crash or corrupt the kernel. It says nothing about whether the program is doing something useful. That distinction is exactly why eBPF-based agents can run in production, on the hosts where they're allowed to run at all.

That last part matters more than it sounds like it should. Everything above, kprobes, tracepoints, the verifier, only works if you actually have a kernel to load a program into, and permission to load it. That's true on a bare host. It's true on a Kubernetes node you or your cloud provider manages. It's true on ECS tasks running on EC2. It is not true everywhere.

A flowchart showing a flagged CVE moving through static reachability, then runtime reachability, then splitting into two paths: an eBPF kernel hook that works on bare host, Kubernetes, and ECS-EC2, and a kernel-independent layer that works everywhere including Fargate
The decision tree this article is really about: a flagged CVE goes through static reachability, then runtime reachability, then the runtime layer itself splits in two. An eBPF kernel hook covers bare host, Kubernetes, and ECS-EC2. A kernel-independent layer covers all of that plus Fargate, which eBPF alone cannot reach.

The constraint that breaks eBPF-only tooling

AWS Fargate runs each task on infrastructure that AWS controls completely, start to finish. That isolation is the whole point of the product. No privileged containers. No special access to the machine's core settings. Per AWS's own documentation, nobody gets access to the underlying host, not the customer, and not AWS's own staff working on your behalf. That's a deliberate security boundary, not an oversight. But it also means there's no kernel to attach a kprobe to. There's no host-level access to attach one from in the first place.

This isn't some rare edge case. Fargate, and services like it, are a normal, common place to run workloads. Teams choose it specifically because it removes host management from their plate. That popularity is exactly why eBPF-based security vendors have had to build a second, non-eBPF version of their product just to cover it. Datadog's own workload protection docs describe a fallback agent built specifically for, in their words, 'eBPF disabled environments, such as AWS Fargate.' That's a tell. Even a vendor that leads with eBPF everywhere else needs an entirely separate approach here.

So picture a fleet spread across a bare host, Kubernetes, ECS on EC2, and ECS on Fargate. Three of those four give you kernel access. The fourth, by design, does not. If your runtime reachability strategy depends on eBPF as its core mechanism, you inherit that same split. You get full visibility on most of the fleet. And you get a blind spot on exactly the one compute model your team picked because it needed the least babysitting.

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

eBPF's ceiling is kernel access. The fix isn't to just accept a gap on Fargate-style compute. The fix is to stop requiring kernel access in the first place. That means watching execution from a layer that doesn't need host privilege to work at all. Anything that does need deeper access gets reserved for the places that can actually grant it.

Think of it as two layers, each doing a different job:

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

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

Both layers report through one shared identity model. The out-of-process layer inspects the operating system directly, so it can independently confirm which container and process actually made an observation. It doesn't have to trust a self-report from inside the application. On compute like Fargate, a container can't always describe its own identity that way on its own. There, the platform's own container metadata fills the gap instead. Either way, every observation ties back to a real container and a real process. Nothing here is 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.

A Tuesday afternoon, one CVE, four compute models

Here's what this looks like when it's not abstract. This is a hypothetical, but it's the shape of a real week for most platform teams: a critical CVE lands in a widely used serialization library, the kind of dependency that shows up in half your services because something else pulled it in. Your SCA scanner lights up: 40 services across the fleet have it installed. Some run on Kubernetes, some on ECS-EC2, a chunk on Fargate because that's where the team put anything that didn't need special infrastructure.

Forty services. You have today, maybe tomorrow morning, before this becomes the kind of finding that shows up in a board update.

What static reachability narrows it to

Static analysis reads the code and asks whether any of those 40 services have an actual call path from an entry point to the vulnerable function. That cuts the list, say to 14. That's real work; you've just eliminated 26 services where the library is present but structurally unreachable. But 14 is still a shortlist, not an answer. Some of those 14 paths run through dynamic dispatch or a deserialization step wired up by a framework, exactly the pattern static analysis can prove is possible without being able to prove it's what actually happens at runtime. You still have to go service by service, and the clock hasn't stopped.

What runtime reachability adds

Runtime observation, running continuously across all four compute models, doesn't ask whether this could run, it reports what already did. Of those 14, it comes back with 5 confirmed: the vulnerable function was actually called, in production, recently. Two are on Kubernetes nodes. One is on an ECS-EC2 host. And one is on Fargate, caught by the layer that doesn't need kernel access, the exact confirmation an eBPF-only tool would never have produced for that service, not because it checked and found it safe, but because it was never able to look there at all. If your runtime strategy depended on eBPF alone, your Fargate services wouldn't show up on either side of this. Not confirmed, not cleared. Just missing, and easy to mistake for fine.

The decision this actually changes

The 5 confirmed hits get fixed today, no debate. Patched, or the call path removed, before end of day. The other 9 from the static shortlist get tracked and scheduled, real work, just not fire-drill work, because nothing has shown they're firing in production right now. That's the whole value: an engineer spends the afternoon on 5 services with evidence behind them instead of guessing their way through 14, and nobody has to quietly hope the Fargate fleet was included in the sweep.

A diagram showing 40 flagged services narrowing to 14 after static reachability, then splitting into 5 confirmed executing in production including 1 on Fargate, and 9 tracked at lower urgency
The same afternoon, start to finish: 40 flagged services, 14 left after static reachability narrows the list, then a split, 5 confirmed executing right now including 1 on Fargate, 9 tracked without urgency.

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.