eBPF - The Modern Approach to Runtime Monitoring and Security
eBPF is a revolutionary technology, originating from the Linux kernel, that can run sandboxed programs in a privileged context such as the operating system kernel. It is used to safely and efficiently extend the capabilities of the kernel without requiring changing the kernel source code or loading kernel modules/extensions.
It allows application developers to run eBPF programs and add additional capabilities to the operating system at runtime. The operating system then guarantees safety and execution efficiency as if natively compiled with the aid of a Just-In-Time (JIT) compiler and verification engine. This has led to a wave of eBPF-based projects spanning a wide array of use cases, including next-generation networking, observability, and security functionality.
Today, eBPF is used extensively to:
- Provide high-performance networking and security in modern data centers and cloud native environments
- Extract fine-grained security observability data at low overhead, help application developers trace applications
- Provide insights for performance troubleshooting
- Provide preventive application and container runtime security enforcement
The possibilities are endless, and eBPF innovation has only just begun.
The Advantages of eBPF
Performance
eBPF drastically improves processing by being JIT compiled and running directly in the kernel. The performance difference is drastic - eBPF performs up to 10x better than kernel extensions for the following reasons:
- eBPF is more modern and offers a flexible approach for extending the Linux kernel's functionality.
- eBPF allows you to run custom code in a restricted and safe manner within the kernel.
- eBPF programs are usually JIT-compiled, which means they can be highly optimized and executed quickly.
- eBPF is well-suited for various networking and security tasks, making it an attractive option for performance-critical applications in these domains.
- eBPF offers the ability to dynamically load and unload programs, which can be useful for fine-tuning performance without requiring kernel restarts.
On the other hand, Kernel extensions involve writing and loading code directly into the kernel. Optimal performance can be achieved with kernel extensions if well-optimized, but this comes with significant risks. Poorly written or unoptimized kernel extensions can lead to system instability and crashes.
Flexibility
In addition to its other features, eBPF also allows you to modify or add functionality and use cases to the kernel without having to restart or patch it.
eBPF programs offer the flexibility of dynamic loading and unloading into the kernel. Once attached to an event, these programs are triggered whenever that event occurs, regardless of the underlying cause. For example, if you attach a program to the system call responsible for file openings, it will be triggered every time any process attempts to open a file, including file-opening events from processes that were already running before the program was loaded. This capability provides a significant advantage over upgrading the kernel and requiring a system reboot to access new functionalities.
This attribute leads to one of the remarkable strengths of observability and security tools utilizing eBPF — they provide instantaneous visibility into all activities taking place on the machine. In containerized environments, this visibility encompasses not only the processes running within the containers but also those operating on the host machine. In short, eBPF-based tools offer comprehensive insights into the entire system's operation.
Security
eBPF is an incredibly powerful technology that now runs at the heart of many critical software infrastructure components. During its development, the safety of eBPF was the most crucial aspect discussed when it was considered for inclusion in the Linux kernel. eBPF safety is ensured through several layers, which we will explore below.
Required Privileges
Unless unprivileged eBPF is enabled, all processes that intend to load eBPF programs into the Linux kernel must be running in privileged mode (root) or require the capability CAP_BPF. This means that untrusted programs cannot load eBPF programs.
If unprivileged eBPF is enabled, unprivileged processes can load certain eBPF programs subject to a reduced functionality set and with limited access to the kernel.
Verifier
If a process is allowed to load an eBPF program, all programs still pass through the eBPF verifier. The eBPF verifier ensures the safety of the program itself. This means, for example:
- Programs are validated to ensure they always run to completion, e.g. an eBPF program may never block or sit in a loop forever. eBPF programs may contain so-called bounded loops, but the program is only accepted if the verifier can ensure that the loop contains an exit condition which is guaranteed to become true.
- Programs may not use any uninitialized variables or access memory out of bounds.
- Programs must fit within the system's size requirements. It is not possible to load arbitrarily large eBPF programs.
- Programs must have a finite complexity. The verifier will evaluate all possible execution paths and must be capable of completing the analysis within the limits of the configured upper complexity limit.
The verifier is meant as a safety tool, checking that programs are safe to run. It is not a security tool inspecting what the programs are doing.
Hardening
Upon successful completion of verification, the eBPF program runs through a hardening process according to whether the program is loaded from a privileged or unprivileged process. This step includes:
- Program execution protection: The kernel memory holding an eBPF program is protected and made read-only. If for any reason, whether it is a kernel bug or malicious manipulation, the eBPF program is attempted to be modified, the kernel will crash instead of allowing it to continue executing the corrupted/manipulated program.
- Mitigation against Spectre: Under speculation, CPUs may mispredict branches and leave observable side effects that could be extracted through a side channel. To name a few examples: eBPF programs mask memory access in order to redirect access under transient instructions to controlled areas, the verifier also follows program paths accessible only under speculative execution and the JIT compiler emits Retpolines in case tail calls cannot be converted to direct calls.
- Constant blinding: All constants in the code are blinded to prevent JIT spraying attacks. This prevents attackers from injecting executable code as constants which in the presence of another kernel bug, could allow an attacker to jump into the memory section of the eBPF program to execute code.
Abstracted Runtime Context
eBPF programs cannot access arbitrary kernel memory directly. Data and data structures that lie outside of the context of the program must be accessed via eBPF helpers. This guarantees consistent data access and makes any access subject to the privileges of the eBPF program. For example, only data structures relevant to the program's type can be read or (sometimes) modified, provided the verifier could ensure at load time that out-of-bound accesses will never happen; or an eBPF program running is only allowed to modify the data of certain data structures if the modification can be guaranteed to be safe. An eBPF program cannot randomly modify data structures in the kernel.