Kprobes observe kernel boundaries, while uprobes observe userspace function entry points. To trace whether a vulnerable Java deserialization library receives attacker-controlled input, should you attach to the syscall or the library function? That choice determines what you see, how much noise you accept, and which evasion methods can blind you. These different primitives shape every detection capability built above them.
This post compares the two from the standpoint of building application security instrumentation. It does not examine tracepoints in depth, since that is a separate discussion of static kernel hook points. The focus is kprobes versus uprobes: when each is appropriate and what each leaves unseen.
How Kprobes Work
A kprobe connects an eBPF program to a kernel function by replacing the first byte of its compiled target instruction with a breakpoint, int3 on x86-64. When execution reaches that address, the kernel invokes your eBPF program, provides the CPU registers from that moment, and then continues with the original instruction. The important detail is that this occurs in kernel space, at a function boundary crossed by every syscall.
The key kernel functions for security instrumentation are those in the syscall table: execve, connect, open, read, write, mmap, ptrace, plus their 64-bit variants. Every process, regardless of language, runtime, or compilation method, crosses these boundaries to perform meaningful work. Kprobes at syscall entry points therefore provide universal coverage.
A kprobe context contains syscall arguments as they arrived from userspace. With execve, that means the filename and argv array. With connect, it includes the file descriptor and sockaddr structure containing the destination address and port. The application-layer payload is not available at this stage, but the kernel view of the process's intended action is authoritative.
How Uprobes Work
A uprobe connects to a function inside a userspace binary or shared library. Its mechanism resembles a kprobe: the target function's first instruction is replaced by a breakpoint, and the kernel runs your eBPF program when execution reaches it. The difference is that the target is an address in a particular process's virtual address space, not a kernel function.
Attaching a uprobe requires the binary or library path and the target function's offset within that file. You can obtain the offset from debug symbols, by inspecting the ELF symbol table, or by calculating it manually. Names in well-known libraries such as libc, OpenSSL, and common language runtimes are stable enough for dependable instrumentation. Application-specific code depends on exported symbols and binary stability.
A uprobe runs before the function executes in userspace. It exposes the function arguments as they appear there, often providing more detail than the kernel syscall view. For example, a TLS library hook can capture plaintext before encryption, something a kprobe on the write() syscall cannot observe.
Security Instrumentation Coverage Tradeoffs
Kprobes: broad but coarse
Kprobes at syscall entry let you observe every process on the system. Python scripts, Java applications, and statically compiled Go binaries all cross the same kernel boundary. That is the central security benefit of kprobes: changing the application layer cannot evade them.
The tradeoff is that syscall arguments represent an action at its lowest level. execve identifies the binary and its arguments, but not whether attacker-controlled input or a scheduled job initiated the execution. Distinguishing malicious execve from normal execve requires added context such as the parent process, timing, and file descriptor state. At high syscall rates, correlating that context increases filtering complexity.
Uprobes: precise but fragile
Uprobes instrument a higher abstraction level. If the question is whether a specific vulnerable dependency function received attacker-controlled arguments, a uprobe on that function answers it directly. You see the arguments before processing, in their application-layer form.
That precision depends on a stable attach point. Library updates, address space layout randomization (ASLR) offsets calculated per process, stripped binaries, or builds without frame pointers can cause attachment to fail or land at the wrong address. In application security, you must know the target library versions and resolve the function symbol.
Uprobes also cover a particular process or binary rather than the whole system. A payload launched through a binary absent from your uprobe list remains invisible. This creates a significant gap for fileless execution, where payloads may travel through unexpected interpreter paths.
Practical Decision Guide
For most application security detection scenarios, the choice depends on what you need to detect and how important application-layer context is.
Choose kprobes for system-wide coverage when you cannot enumerate every binary that could be abused, for syscall-level signals such as unexpected outbound connections, new process spawns, and privilege escalation attempts through setuid, or for language-agnostic behavioral baselines. Most of Oligo's detection rules for RCE, SSRF, and fileless execution use kprobe-attached programs at the relevant syscall entry points.
Choose uprobes when application-layer argument context is unavailable at the syscall level. Their primary security use is library-level vulnerability detection. If a specific CVE-matched dependency function, such as a deserialization function in a widely-used Java library, receives a particular argument pattern, a uprobe on that function provides the clearest direct view. Uprobes also help capture TLS plaintext for protocol security analysis because the data is visible before encryption at the userspace library boundary.
Hybrid Instrumentation
In practice, a complete runtime security agent uses both. Kprobes form the security perimeter: anything crossing the syscall boundary is visible regardless of application structure. Uprobes supply application-layer context at selected, high-value detection points where syscall arguments alone are insufficient.
The combined approach carries a maintenance burden. Uprobe attach points must be refreshed as target library versions change. Symbol resolution must also handle stripped binaries and JIT-compiled runtimes. Go's runtime, for example, has fairly stable function names in its symbol table, while Java's JIT-compiled bytecode is more difficult. This work is performed once for each library family and then maintained.
At Oligo, the architectural decision was to use kprobes as the primary detection layer and add uprobes selectively for vulnerability families where the application-layer view contributes signal unavailable from syscalls alone. When a new critical CVE affects a function in a widely-deployed library, placing a uprobe on that function is often the quickest route to high-confidence detection while the behavioral model for kprobe-level signals is refined.
What Neither Hook Type Covers
Kprobes and uprobes both attach at synchronous execution boundaries. Neither natively captures asynchronous events, signals delivered to processes, or kernel-internal state changes that bypass an instrumented function. Tracepoints are better suited to those cases because they attach to stable kernel subsystem events rather than function entry points. They form a separate instrumentation layer that complements both hook types.
Neither hook exposes application-layer payload content unless you deliberately capture it. A kprobe on connect() reveals the destination IP and port, not the HTTP request body sent over that connection. The importance of that limitation depends on the detection objective. connect() arguments are enough to identify an unexpected outbound connection, but SQL injection detection requires a higher-layer hook or network traffic analysis.