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.

- 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:
- You don't recompile the kernel. eBPF programs are compiled and loaded into a running kernel while it's already running.
- 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.
- 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.
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.

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:
- 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.
- 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.
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.

Two comparisons: what each approach proves, and where each one runs
| Dimension | Static reachability | Runtime reachability |
|---|---|---|
| Detection point | Source code / call graph, before deployment | A running process, observed in production |
| What it proves | A call path from entry point to vulnerable function is theoretically possible | That call path was actually executed, by this process, at this time |
| Blind spots | Dynamic dispatch, reflection, lazy-loaded modules, DI-wired handlers | Code that hasn't run yet, including rare or attacker-only paths never exercised in normal traffic |
| Performance overhead | None at runtime; cost is analysis time in CI/CD | Near zero when instrumentation is scoped to known-vulnerable functions and removes itself after first use |
| False positive rate | Higher; conservative call graphs flag paths that exist syntactically but never execute | Very low for what it reports; a captured execution event is direct evidence, not inference |
| When it can run | Pre-deployment, on every commit or build | Only once the workload is running, so it can't gate a merge the way static analysis can |
| Dimension | eBPF-only runtime tooling | Layered runtime reachability |
|---|---|---|
| Bare host | Works, full kernel access available | Works |
| Kubernetes (self-managed nodes) | Works, node-level kernel access available | Works |
| ECS on EC2 | Works, host-level kernel access available | Works |
| ECS Fargate / serverless containers | Cannot run: no privileged access, no access to the underlying host | Works: out-of-process layer needs no kernel or host privilege |
| Coupling to kernel internals | Kprobes tie to kernel-version-specific function names and layouts | Out-of-process layer avoids that coupling entirely |
| Function-level precision | Possible via uprobes, where host access allows it | Available for interpreted-language packages; library-level everywhere else |
| Steady-state overhead | Ongoing, bounded by hooked-event volume | Near-zero once first-observation instrumentation self-removes |
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.