Writing eBPF Tracepoint Program with Rust Aya: Tips and Example

TL;DR

This post shows an example eBPF Tracepoint program and shares tips on writing the eBPF Tracepoint programs with aya. ๐Ÿฆ€๐Ÿ

  1. Introduction
    1. Dataflow
      1. Kernel space
      2. User space
    2. Benefits of using aya
      1. One language: Rust
      2. Auto-generating Rust codes
  2. Run eBPF Tracepoint tracing program
    1. Prerequisites
    2. Clone
    3. Generate structs codes
    4. Build and Run
  3. Tips on writing eBPF codes
    1. Tip1: Deserializing with generated struct codes
    2. Tip2. Decode __data_loc
    3. Tip3. PerCpuArray eBPF Map as a Heap
    4. Tip4: Inspect eBPF programs/maps with bpftool
  4. Helpful references
    1. Aya Community’s discord
    2. How to write an eBPF/XDP load-balancer in Rust
    3. suidsnoop
    4. aya-examples
    5. Frequently asked questions about using tracepoint with ebpf/libbpf programs
  5. Wrap up

Introduction

Dataflow

We will run the eBPF program where tracingpoint’s data flows from kernel space to user space like below.

sched_process_exec data flow

Kernel space

The eBPF Program trace_sched_process_exec is invoked by tracepoint sched_process_exec. Then, trace_sched_process_exec parses the tracepoint’s data and sends it to the eBPF Map events.

User space

User space Program observer observes the eBPF Map. If observer detects new data in the map, it observer reads the data from the map.

Benefits of using aya

Before diving into codes, let’s take a look at the benefits of using aya for eBPF projects.

One language: Rust

The biggest benefit of using aya is that both kernel-space and user-space programs are written in Rust. This means the kernel-space and user-space programs can share codes!

In this example case, trace_sched_process_exec and sched_process_exec are written in Rust and compiled into binaries by rustc. And, a shared library is placed common folder.

The folder structure

Auto-generating Rust codes

The document says ‘Aya is an eBPF library built with a focus on operability and developer experience.‘ So, it has several tools that help developers. Aya generates code for user-space codes for loading the eBPF into the kernel and attaching it to events. Also, aya-tool generates rust struct codes.

Run eBPF Tracepoint tracing program

Let’s run my eBPF Tracepoint tracing program on your machine!

Prerequisites

(Optional) Set up environment on MacOS

If you’re using MacOS, you can quickly set it up with lima and my template.

  • Install lima
brew install lima
  • Download the template
wget https://raw.githubusercontent.com/yukinakanaka/aya-lab/main/lima-vm/aya-lab.yaml
  • Edit cpu and memory configuration in aya-lab.yaml. Default values are:
cpus: 4
memory: "8GiB"
  • Create a VM
limactl start lima-vm/aya-lab.yaml

Clone

You can get all the codes from my repo.

git clone https://github.com/yukinakanaka/aya-lab.git
cd aya-lab/tracepoint-sched-process-exec 

Generate structs codes

cargo xtask codegen

Build and Run

Build and execute the program!

cargo xtask build && sudo ./target/debug/observer

You can see logs of both the kernel-space program and the user-space program. These logs show information that is extracted from the tracepoint!

  • Example:
2024-07-06T13:33:30.024915Z  INFO observer: 54304 502 /usr/bin/sed 12
2024-07-06T13:33:30.024994Z  INFO trace_sched_process_exec: 54304 502 /usr/bin/sed 12    
2024-07-06T13:33:30.026210Z  INFO observer: 54305 502 /usr/bin/cat 12
2024-07-06T13:33:30.026252Z  INFO trace_sched_process_exec: 54305 502 /usr/bin/cat 12    

Tips on writing eBPF codes

I learned a lot by writing the above eBPF Tracepoint program. Let me share some tips on writing eBPF codes.

Tip1: Deserializing with generated struct codes

We must deserialize a context (ctx) to get kernel call parameters from the tracepoint events. Manually deserializing is a bit cumbersome and might be a cause of bugs. So, we should do it automatically as long as possible.

  • You can see generated struct codes of sched_process_exec’s parameters in ebpf/src/bindings.rs.
#[repr(C)]
#[derive(Debug)]
pub struct trace_event_raw_sched_process_exec {
    pub ent: trace_entry,
    pub __data_loc_filename: u32_,
    pub pid: pid_t,
    pub old_pid: pid_t,
    pub __data: __IncompleteArrayField<::aya_ebpf::cty::c_char>,
}

This file was generated by aya-tool.

aya-tool generate trace_event_raw_sched_process_exec

Also, aya-tool can be used in codes like xtask/src/codegen.rs. By doing that, we can run it via cargo.

cargo xtask codegen
let event: trace_event_raw_sched_process_exec =
        ctx.read_at::<trace_event_raw_sched_process_exec>(0)?;

Tip2. Decode __data_loc

I struggled to get the filename from __data_loc_filename. In Rust, its type is u32_. I could not understand that meaning. Therefore, I conducted the following search.

  • Checked the C/C++ expression:
cat /sys/kernel/tracing/events/sched/sched_process_exec/format

name: sched_process_exec
ID: 255
format:
        field:unsigned short common_type;       offset:0;       size:2; signed:0;
        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
        field:int common_pid;   offset:4;       size:4; signed:1;

        field:__data_loc char[] filename;       offset:8;       size:4; signed:0;
        field:pid_t pid;        offset:12;      size:4; signed:1;
        field:pid_t old_pid;    offset:16;      size:4; signed:1;

print fmt: "filename=%s pid=%d old_pid=%d", __get_str(filename), REC->pid, REC->old_pid

The filed type of filename is __data_loc char[] and its size is 4, which means 32bit.

Lower 16bit is the offset against the beginning of the event entry.
Higher 16bit is the length of the array.

Using the above information, I managed to decode __data_loc by the following.

let mut buf = [0u8; 32];

let offset = (event.__data_loc_filename & 0xFFFF) as usize;
let len = (event.__data_loc_filename >> 16 & 0xFFFF) as usize - 1; // -1 is for null-termination.
let bytes = bpf_probe_read_kernel_str_bytes(
        ctx.as_ptr().add(offset) as *const u8,
        &mut buf,
)?;
let filename = core::str::from_utf8_unchecked(bytes),

Tip3. PerCpuArray eBPF Map as a Heap

When I built eBPF programs, I faced the following error Building the eBPF program failed with:

cargo xtask build-ebpf

error: linking with `bpf-linker` failed: exit status: 2
  |
  = note: LC_ALL="C" PATH="/home/
...
  = note: error: LLVM issued diagnostic with error severity

It looks like an error related LLVM, but the error message didn’t tell the cause. So, I had no idea what was wrong with my code. I could not find a solution in Google and GitHub. But, I finally found the cause by searching Aya Community’s discord!

We cannot use big variables in stack space because eBPF programs are limited to 512 bytes of stack space! I used a big variable like let buf = [0: 512]. I can solve that problem using PerCpuArray eBPF Map as a heap. Here are snippets.

#[map(name = "data_heap")]
pub static mut DATA_HEAP: PerCpuArray<ProcessExecEventBuf> = PerCpuArray::with_max_entries(1, 0);

#[repr(C)]
pub struct ProcessExecEventBuf {
    pub p: ProcessExecEvent,
}

let buf:  &mut ProcessExecEventBuf = unsafe {
        let ptr = DATA_HEAP.get_ptr_mut(0).ok_or(0)?;
        &mut *ptr
};
buf.p.pid = event.pid;

Note: The above error message is from v0.9.2 bpf-linker. It may be improved in the future.

Tip4: Inspect eBPF programs/maps with bpftool

Sometimes I had errors while loading eBPF programs and using eBPF Map. Then, I needed to check programs and maps in the kernel. In that case, bpftool is a great tool.

  • Check what eBPF programs are loaded into the kernel
sudo bpftool prog list

You can see trace_sched_pro!

791: tracepoint  name trace_sched_pro  tag 8d60832b8d7402c8  gpl run_time_ns 50976378 run_cnt 986
        loaded_at 2024-07-04T21:17:24+0900  uid 0
        xlated 3744B  jited 3480B  memlock 4096B  map_ids 690,693,689,691,692
        pids observer(474946)
  • Check what eBPF maps in the kernel
sudo bpftool map list

You can see events, data_heap.

690: percpu_array  name data_heap  flags 0x0
        key 4B  value 528B  max_entries 1  memlock 8192B
        pids observer(474946)
...
692: perf_event_array  name events  flags 0x0
        key 4B  value 4B  max_entries 8  memlock 4096B
        pids observer(474946)

Helpful references

The eBPF and aya are relatively new technologies, and they are actively developed now. So, it may be difficult to find solutions when you face problems. Bellows are really helpful references, so check them!

Aya Community’s discord

Aya community is so active that you can find helpful information on the discord! You can join it from here.

Join the discussion on the discord

How to write an eBPF/XDP load-balancer in Rust

Kong’s blog explains how to write eBPF/XDP load-balancer using in Rust from scratch. You can learn how to set up a Rust project and generate Rust codes using aya-tools, etc. If you haven’t used Aya yet, I highly recommend that you go through this post!

suidsnoop

Uses Aya and eBPF LSM programs to implement audit logging and policy enforcement for suid binaries. I learned from it how to access a PerCpuArray eBPF Map from user space.

aya-examples

There are so many examples of aya. I learned from it how to access a PerCpuArray eBPF Map from kernel space.

Frequently asked questions about using tracepoint with ebpf/libbpf programs

This post doesn’t use aya. But, we can learn how to get kernel call parameters from the tracepoint events.

Wrap up

I showed my eBPF Tracepoint program and shared tips on writing the eBPF programs with aya. Also, I introduced some helpful references. I hope this post may help someone to write eBPF program with Aya! ๐Ÿฆ€๐Ÿ