How bpftime Implements Event Attachment Mechanisms: A Modular Plugin Architecture

bpftime implements event attachment through a modular plugin architecture centered around abstract base classes, polymorphic private data containers, and a central manager that maps attach types to concrete implementations like Frida uprobes, syscall tracing, and custom triggers.

The bpftime project (eunomia-bpf/bpftime) is a userspace eBPF runtime designed to execute eBPF programs outside the kernel context. Its bpftime event attachment mechanisms employ a highly extensible plugin system that enables uniform handling of diverse event sources—from uprobes and syscall traces to CUDA GPU kernels—without requiring modifications to the core runtime libraries.

Core Architecture Components

The Base Attach Implementation Interface

At the heart of the system lies attach::base_attach_impl, defined in attach/base_attach_impl/base_attach_impl.hpp. This abstract base class establishes the strict contract that every attach implementation must fulfill, including pure virtual methods for creation, detachment, and custom helper registration. By inheriting from this interface, disparate event sources such as kernel probes and user-space instrumentation can be managed through a unified API.

Polymorphic Private Data Containers

The attach::attach_private_data class, found in attach/base_attach_impl/attach_private_data.hpp, serves as a type-erased container for attach-specific initialization data. Each concrete implementation provides a factory lambda that constructs specialized private data from string descriptions, enabling runtime configuration of parameters like target function addresses or syscall numbers. This polymorphism allows bpf_attach_ctx to handle type-specific data without knowing the underlying implementation details.

The Central Attachment Context Manager

The bpf_attach_ctx class in runtime/include/bpf_attach_ctx.hpp orchestrates all attachment activity. It maintains registries mapping attach-type IDs (such as ATTACH_UPROBE or ATTACH_SYSCALL_TRACE) to implementation objects, and it stores instantiated links between compiled eBPF programs and their event sources. The register_attach_impl method enables dynamic plugin registration at startup, while create_attach_with_ebpf_callback establishes the connection between an eBPF program's execution callback and the event source.

The Event Attachment Lifecycle

Registration

During system initialization, bpf_attach_ctx::register_attach_impl is invoked for each available implementation. This method binds a list of attach-type identifiers to a std::unique_ptr owning the concrete implementation, and associates a factory function for building attach_private_data objects from configuration strings. This registration phase populates the internal maps that the context manager uses to route attachment requests.

Handler Instantiation

When creating a new attachment, the runtime calls get_attach_impl_by_attach_type to retrieve the appropriate implementation. It then constructs the private data using the registered factory and invokes impl->create_attach_with_ebpf_callback, passing the compiled eBPF program's ebpf_run_callback. This callback encapsulates the JIT-compiled or interpreted eBPF bytecode ready for execution.

Event Triggering

Concrete implementations receive events from their respective sources—whether Frida's GumInterceptor for uprobes, a syscall trampoline for system call tracing, or manual calls to simple_attach_impl::trigger. They forward event arguments to the stored ebpf_run_callback, which executes the eBPF program. The callback's return value can be consumed by the attach implementation to perform actions such as bpftime_set_retval or bpftime_override_return.

Detachment

Links are destroyed through bpf_attach_ctx::destroy_instantiated_attach_link, which delegates to the implementation's detach_by_id method. For Frida-based attaches, this removes the interceptor and restores original function prologues; for syscall tracing, it unregisters the callback from the per-syscall dispatch table. This mechanism ensures clean removal without affecting other active instrumentation.

Built-in Implementation Types

Simple Attach for Custom Triggers

The simple attach implementation in attach/simple_attach_impl/simple_attach_impl.hpp provides a lightweight wrapper for user-defined triggers. It accepts string-based trigger arguments and callbacks, making it ideal for runtime-generated events, custom performance monitoring, or "hand-crafted" events inside the runtime that do not require external instrumentation frameworks.

Syscall-Trace Attach

Implemented in attach/syscall_trace_attach_impl/include/syscall_trace_attach_impl.hpp, this module hooks process syscall entry and exit points by replacing the original syscall dispatcher with a trampoline. It maintains per-syscall callback sets via structures like syscall_trace_attach_impl and forwards CPU register states and arguments to attached eBPF programs through its dispatch_syscall method.

Frida Uprobe Attach

The Frida-based implementation in attach/frida_uprobe_attach_impl/include/frida_uprobe_attach_impl.hpp leverages Frida's GumInterceptor to instrument userspace functions. It supports four distinct attach types: uprobe, uretprobe, override, and replace. The implementation can invoke plain C++ lambdas or forward calls to eBPF programs via attach_at_with_ebpf_callback and call_attach_specific_function.

CUDA GPU Attach

For heterogeneous computing environments, the optional CUDA attach implementation in attach/nv_attach_impl/nv_attach_impl.hpp enables eBPF programs to trigger from GPU kernels. It establishes a shared-memory IPC channel between the CUDA device code and the bpftime runtime, extending observability into GPU-accelerated workloads without kernel driver modifications.

Code Examples

Registering a Simple Attach Implementation

#include "runtime/include/bpf_attach_ctx.hpp"
#include "attach/simple_attach_impl/simple_attach_impl.hpp"

using namespace bpftime;

// Create the global attach context
bpf_attach_ctx ctx;

// Register a simple attach that reacts to a string trigger
ctx.register_attach_impl(
    { ATTACH_SIMPLE },                                   // ATTACH_SIMPLE is a user‑defined integer
    std::make_unique<simple_attach::simple_attach_impl>(
        // callback receives (attach_arg, trigger_arg, ebpf_cb)
        [](const std::string& attach_arg,
           const std::string& trigger_arg,
           const attach::ebpf_run_callback& ebpf_cb) -> int {
            // Forward the trigger argument to the eBPF program
            uint64_t ret = 0;
            int rc = ebpf_cb(nullptr, 0, &ret);
            // Do something with ret …
            return rc;
        },
        ATTACH_SIMPLE),
    // Factory that builds private data from a string (here just stores the string)
    [](const std::string_view& sv, int& err) -> std::unique_ptr<attach::attach_private_data> {
        struct simple_attach::simple_attach_private_data : attach::attach_private_data {
            std::string data;
            int initialize_from_string(const std::string_view& sv) override {
                data = std::string(sv);
                return 0;
            }
        };
        err = 0;
        return std::make_unique<simple_attach_private_data>();
    });

Source reference: This registration pattern is defined in bpf_attach_ctx::register_attach_impl within runtime/include/bpf_attach_ctx.hpp.

Attaching a Uprobe with Frida

#include "runtime/include/bpf_attach_ctx.hpp"
#include "attach/frida_uprobe_attach_impl/include/frida_uprobe_attach_impl.hpp"

bpftime::bpf_attach_ctx ctx;

// Assume the eBPF program has already been compiled and loaded,
// producing an `ebpf_run_callback` named `prog_cb`.
bpftime::attach::ebpf_run_callback prog_cb = /* ... */;

// Register the Frida implementation (done once at start‑up)
ctx.register_attach_impl(
    { bpftime::attach::ATTACH_UPROBE, bpftime::attach::ATTACH_URETPROBE },
    std::make_unique<bpftime::attach::frida_attach_impl>(),
    [](const std::string_view&, int& err) -> std::unique_ptr<bpftime::attach::attach_private_data> {
        // Frida doesn’t need extra data for the generic case
        err = 0;
        return nullptr;
    });

// Later, when creating the actual link:
int link_id = ctx.create_attach_with_ebpf_callback(
    prog_cb,
    /* private data for the specific function */ 
    []() -> std::unique_ptr<bpftime::attach::attach_private_data> {
        struct frida_uprobe_private_data : bpftime::attach::attach_private_data {
            void* func_addr = nullptr;
            int initialize_from_string(const std::string_view& sv) override {
                std::stringstream ss(std::string(sv));
                ss >> func_addr;          // simple example: "0x7f1234"
                return 0;
            }
        };
        return std::make_unique<frida_uprobe_private_data>();
    }(),
    bpftime::attach::ATTACH_UPROBE);

Source reference: The Frida implementation details are found in attach/frida_uprobe_attach_impl/include/frida_uprobe_attach_impl.hpp, specifically within attach_at_with_ebpf_callback and call_attach_specific_function.

Tracing System Calls

#include "runtime/include/bpf_attach_ctx.hpp"
#include "attach/syscall_trace_attach_impl/include/syscall_trace_attach_impl.hpp"

bpftime::bpf_attach_ctx ctx;

// Register the syscall‑trace impl (usually done by the runtime)
ctx.register_attach_impl(
    { bpftime::attach::ATTACH_SYSCALL_TRACE },
    std::make_unique<bpftime::attach::syscall_trace_attach_impl>(),
    [](const std::string_view& sv, int& err) -> std::unique_ptr<bpftime::attach::attach_private_data> {
        struct syscall_private_data : bpftime::attach::attach_private_data {
            int sys_nr = -1;
            bool is_enter = true;
            int initialize_from_string(const std::string_view& sv) override {
                // format: "enter:60" or "exit:60"
                std::string s(sv);
                is_enter = s.rfind("enter:", 0) == 0;
                sys_nr = std::stoi(s.substr(is_enter ? 6 : 5));
                return 0;
            }
        };
        err = 0;
        return std::make_unique<syscall_private_data>();
    });

// Create a link that prints arguments of `openat` (syscall 257) on entry
bpftime::attach::ebpf_run_callback ebpf_cb = /* compiled ebpf program */;
int link_id = ctx.create_attach_with_ebpf_callback(
    ebpf_cb,
    /* private data built from "enter:257" */
    [](){ 
        auto d = std::make_unique<bpftime::attach::syscall_private_data>();
        d->initialize_from_string("enter:257");
        return d;
    }(),
    bpftime::attach::ATTACH_SYSCALL_TRACE);

Source reference: The syscall dispatch and attachment logic resides in attach/syscall_trace_attach_impl/include/syscall_trace_attach_impl.hpp.

Extending bpftime with Custom Attach Types

Adding new event sources to bpftime requires only three steps without modifying the core runtime. First, create a subclass of base_attach_impl that overrides the pure virtual methods for creation and detachment. Second, derive from attach_private_data to store your attachment-specific configuration. Third, register your implementation with bpf_attach_ctx::register_attach_impl during startup. This design enables third-party developers to integrate proprietary instrumentation methods—such as hardware PMU counters or custom hypercall interfaces—while leveraging bpftime's existing eBPF program loading and memory management infrastructure.

Summary

  • bpftime uses a plugin architecture centered on base_attach_impl, attach_private_data, and bpf_attach_ctx to support multiple event types uniformly.
  • The attachment lifecycle involves registration via register_attach_impl, instantiation through create_attach_with_ebpf_callback, event forwarding to ebpf_run_callback, and cleanup via detach_by_id.
  • Built-in implementations cover user-space function tracing via Frida (frida_uprobe_attach_impl.hpp), syscall interception via trampolines (syscall_trace_attach_impl.hpp), lightweight string triggers (simple_attach_impl.hpp), and optional CUDA GPU events (nv_attach_impl.hpp).
  • Custom attach types can be added without rebuilding the core runtime by implementing the abstract base classes and registering them with the central context manager.

Frequently Asked Questions

What is the role of bpf_attach_ctx in bpftime?

bpf_attach_ctx acts as the central coordinator for all event attachments in the bpftime runtime. It maintains registries that map attach-type identifiers to concrete implementation objects, manages the lifecycle of instantiated links between eBPF programs and event sources, and provides the register_attach_impl API that enables modular extension of the system.

How does bpftime support different attachment types like uprobes and syscalls?

bpftime uses a polymorphic plugin system where each attachment type implements the base_attach_impl interface and provides a corresponding attach_private_data factory. The runtime registers these implementations at startup with register_attach_impl, allowing the same bpf_attach_ctx API to handle Frida-based uprobe instrumentation, syscall tracing via trampolines, and custom triggers uniformly through virtual dispatch.

Can I implement custom event sources in bpftime?

Yes. You can create custom attach types by subclassing attach::base_attach_impl to implement attachment logic and attach::attach_private_data for configuration storage. After implementing the required methods such as create_attach_with_ebpf_callback and detach_by_id, register your module with bpf_attach_ctx::register_attach_impl. This requires no changes to the core bpftime runtime, making the system highly extensible for proprietary instrumentation needs.

How does bpftime handle the eBPF program callback during event triggering?

When an event fires, the concrete attach implementation invokes the ebpf_run_callback function pointer stored during link creation. This callback executes the compiled eBPF program with the event context and arguments, and returns a status code that the attach implementation can use to perform actions like modifying return values via bpftime_override_return or logging data to maps.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →