Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fentry / Fexit

Note

Full code for the example in this chapter is available on GitHub.

What are fentry and fexit programs?

Fentry and fexit programs attach to the entry and exit of a kernel function through a BPF trampoline. Compared to kprobes, they have lower overhead and, because they are BTF-aware, they let the verifier reason about kernel types directly — so kernel pointers can be dereferenced without calling bpf_probe_read_kernel. A fexit program also has access to the function’s return value in the same context, without needing a separate return-side program the way kprobes do with kretprobes.

Note

Fentry and fexit require a kernel built with BTF (CONFIG_DEBUG_INFO_BTF=y) and a kernel version of at least 5.5.

Example project

To illustrate fentry and fexit with Aya, let’s write a program which attaches to kernel_clone — the function the kernel uses to create new processes and threads — and prints the PID of the caller on entry and the PID of the newly created child on exit.

Design

For this demo program, we are going to rely on aya-log to print PIDs from the BPF program.

The kernel_clone function is also called for thread creation, so to keep the output focused on process creation we filter out callers that pass the CLONE_THREAD flag.

eBPF code

  • From the kernel_clone signature, we see that struct kernel_clone_args *args is the only function parameter. We access it from the FEntryContext / FExitContext handles via ctx.arg(0).
  • Because fentry/fexit programs run inside a BPF trampoline, the verifier treats the argument pointer as a typed kernel pointer and we can dereference it directly — no bpf_probe_read_kernel call is required.
  • In the fexit program, the return value of kernel_clone (the child PID) is exposed as the slot after the original arguments, so we read it with ctx.arg(1).
  • We check the flags field for CLONE_THREAD, and for process-creating calls we print the parent PID (and, on exit, the child PID) using the aya-log info! macro.

Here’s how the eBPF code looks like:

#![no_std]
#![no_main]
#![allow(linker_messages)]

use aya_ebpf::{
    helpers::bpf_get_current_pid_tgid,
    macros::{fentry, fexit},
    programs::{FEntryContext, FExitContext},
};
use aya_log_ebpf::info;

#[allow(
    clippy::all,
    dead_code,
    improper_ctypes_definitions,
    non_camel_case_types,
    non_snake_case,
    non_upper_case_globals,
    unnecessary_transmutes,
    unsafe_op_in_unsafe_fn,
)]
#[rustfmt::skip]
mod vmlinux;

use vmlinux::kernel_clone_args;

const CLONE_THREAD: u64 = 0x00010000;

#[fentry]
pub fn fentry_clone(ctx: FEntryContext) -> u32 {
    let args_ptr: *const kernel_clone_args = ctx.arg(0);
    let flags = unsafe { (*args_ptr).flags };

    if flags & CLONE_THREAD == 0 {
        let pid = (bpf_get_current_pid_tgid() >> 32) as u32;
        info!(&ctx, "Process creation is started by: {}", pid);
    }
    0
}

#[fexit]
pub fn fexit_clone(ctx: FExitContext) -> u32 {
    let args_ptr: *const kernel_clone_args = ctx.arg(0);
    let flags = unsafe { (*args_ptr).flags };
    let return_value: i32 = ctx.arg(1);

    if flags & CLONE_THREAD == 0 {
        let pid = (bpf_get_current_pid_tgid() >> 32) as u32;
        if return_value < 0 {
            info!(
                &ctx,
                "Process creation by {} failed. errno: {}", pid, -return_value
            );
        } else {
            info!(
                &ctx,
                "New process is created by: {} child id: {}", pid, return_value
            );
        }
    }
    0
}

#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

Userspace code

The purpose of the userspace code is to load the eBPF program and attach the fentry and fexit handlers to kernel_clone. Both program types need a Btf handle: Btf::from_sys_fs() reads the running kernel’s BTF from /sys/kernel/btf/vmlinux, aya uses it to translate the function name to its BTF type id, and the kernel uses that id to find the function’s address and signature, so it can build a trampoline and verify your argument accesses.

Here’s how the code looks like:

use aya::{
    Btf,
    programs::{FEntry, FExit},
};
use aya_log::EbpfLogger;
use log::{info, warn};
use tokio::signal;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    env_logger::init();

    // This will include your eBPF object file as raw bytes at compile-time and load it at
    // runtime. This approach is recommended for most real-world use cases. If you would
    // like to specify the eBPF program at runtime rather than at compile-time, you can
    // reach for `Ebpf::load_file` instead.
    let mut bpf = aya::Ebpf::load(aya::include_bytes_aligned!(concat!(
        env!("OUT_DIR"),
        "/fentry-fork"
    )))?;
    match EbpfLogger::init(&mut bpf) {
        Err(e) => {
            // This can happen if you remove all log statements from your eBPF program.
            warn!("failed to initialize eBPF logger: {e}");
        }
        Ok(logger) => {
            let mut logger = tokio::io::unix::AsyncFd::with_interest(
                logger,
                tokio::io::Interest::READABLE,
            )?;
            tokio::task::spawn(async move {
                loop {
                    let mut guard = logger.readable_mut().await.unwrap();
                    guard.get_inner_mut().flush();
                    guard.clear_ready();
                }
            });
        }
    }

    let btf = Btf::from_sys_fs()?;

    let program: &mut FEntry =
        bpf.program_mut("fentry_clone").unwrap().try_into()?;
    program.load("kernel_clone", &btf)?;
    program.attach()?;

    let program: &mut FExit =
        bpf.program_mut("fexit_clone").unwrap().try_into()?;
    program.load("kernel_clone", &btf)?;
    program.attach()?;

    let ctrl_c = signal::ctrl_c();
    info!("Waiting for Ctrl-C...");
    ctrl_c.await?;
    info!("Exiting...");

    Ok(())
}

Running the program

$ RUST_LOG=info cargo run
[2026-05-07T10:00:00Z INFO  fentry_fork] Waiting for Ctrl-C...
[2026-05-07T10:00:05Z INFO  fentry_fork] Process creation is started by: 12345
[2026-05-07T10:00:05Z INFO  fentry_fork] New process is created by: 12345 child id: 67890