Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for My first Aya program
Joseph Ligier
Joseph Ligier

Posted on • Edited on

     

My first Aya program

I'm getting started with eBPF programming with Aya. The idea behind this series of articles is to get you started too.

In this section, we'll create our first Aya program. We'll also see that there are different types of eBPF programs.

FYI, this is the English version ofan article originally published in French.

Aya the bee 2


Example of Hello world Program

I assume you have an operating environment as defined inPart 1. You can also follow this step-by-step lab on creating an Aya program:

Killer koda screenshot

Types of eBPF programs

To generate an Aya program, we'll use cargo generate :

cargo generate https://github.com/aya-rs/aya-template
Enter fullscreen modeExit fullscreen mode

We're going to answer a few questions:

  • Project Name: the name of the project (I puttest-aya)
  • The second question is more interesting:

Which type of eBPF programs?

In fact, there are different types of eBPF programs. As we saw inPart 1, I wrote that there are 3 types of eBPF software:

  • Network
  • Observability and Tracing
  • Security

The choices presented to generate the eBPF program are, in fact, parts of the Linux kernel that can be modified or supervised by eBPF. So you need to think carefully about this before jumping headlong into programming.

As I wrote in Part 1, Aya is not yet fully mature compared with other solutions. Here's one reason why: although it may seem as if there are already a lot of program types available, there are still quite a few missing:

GitHub logo Meta: Add Support For All BPF Program Types#205

  • [x] BPF_PROG_TYPE_SOCKET_FILTER
  • [x] BPF_PROG_TYPE_KPROBE
  • [x] BPF_PROG_TYPE_SCHED_CLS
  • [ ] #206
  • [x] BPF_PROG_TYPE_TRACEPOINT
  • [x] BPF_PROG_TYPE_XDP
  • [x] #207
  • [x] BPF_PROG_TYPE_CGROUP_SKB
  • [x] #208
  • [ ] #209
  • [ ] #210
  • [ ] #211
  • [x] BPF_PROG_TYPE_SOCK_OPS
  • [x] BPF_PROG_TYPE_SK_SKB
  • [x] #212
  • [x] BPF_PROG_TYPE_SK_MSG
  • [x] BPF_PROG_TYPE_RAW_TRACEPOINT
  • [x] #213
  • [ ] #214
  • [x] BPF_PROG_TYPE_LIRC_MODE2
  • [ ] #215
  • [ ] #216
  • [x] #217
  • [ ] #218
  • [ ] #219
  • [x] BPF_PROG_TYPE_TRACING
  • [ ] #220
  • [x] BPF_PROG_TYPE_EXT
  • [x] BPF_PROG_TYPE_LSM
  • [x] #223
  • [ ] #221

What's more, as the Linux kernel continues to evolve, there will certainly be other types of eBPF programs available in the near future.

Time to choose

I tested a few types of eBPF program with Aya before writing this article. To study each type, you can quickly spend days: "I don't understand, it doesn't work" is certainly the phrase I'm uttering the most at the moment. There aren't many similarities between eBPF program types. If you've already played withXDP programs, you'll have other problems withKprobe programs, for example. For this article, we'll be looking atTracePoint eBPF programs.

TracePoint

Tracepoints are, as the name suggests, places in the Linux code that have been marked (seerecommendations). They are mainly used for tracing and debugging the Linux kernel.

code to add in Linux kernel for Tracepoint

All these points are accessible in the/sys/kernel/debug/tracing/available_events file:

A slice of this file

They are in the format:

[Category]:[Name]

Just in time, this is the supplementary question we ask:

Which tracepoint category?

We're going to choosesyscalls because everyone knows what a syscall is. Does everyone?

Syscalls

When a program is run, it asks the kernel to perform primary functions called syscalls (system calls).

Syscalls are interfaces between user and kernel space

To view the syscalls performed when a command is run, you can use the strace program. For example, you can use:

strace -c ls command output

Which syscall to use

tracepoint name choice

To find available tracepoint names, simply issue this command:

grep syscalls: /sys/kernel/debug/tracing/available_events
Enter fullscreen modeExit fullscreen mode

We can see that they are of the :sys_(enter|exit)_$name_of_syscall

  • enter : starting of syscall
  • exit : closing of syscall

We'll arbitrarily chooseexecve, which is the syscall that executes a program and the one that starts thesyscall. So we havesys_enter_execve.

All questions and answers of the MCQ

Test

Now we're going to test the generated program directly:

cd test-ayaRUST_LOG=info cargo run
Enter fullscreen modeExit fullscreen mode

This will take a little while: the time it takes to download and compile all the dependencies. Now you can go and havea cup of coffee!

Once your program has been demarched, run commands on another terminal, e.g.:ls
On thecargo run terminal, you'll see the following message every time you type a command:

[INFO test_aya] tracepoint sys_enter_execve called
Enter fullscreen modeExit fullscreen mode

So every time the syscallexecve is called, this message will be displayed. We've just created the "Hello World" of the Tracepoint eBPF program (syscall:sys_enter_execve). So, for an eBPF program to start up, it needs an event (event-driven) that tells it to run: this is what we call ahook.

hook schema


Anatomy of a Hello world Aya program

Let's take a look at what thecargo generate command has generated. As we saw in part 1, there are two parts to an eBPF program: kernel space and user space.

Kernel space

The eBPF program source code can be found here:test-aya-ebpf/src/main.rs. It is compiled first.
Here are its contents:

#![no_std]#![no_main]useaya_ebpf::{macros::tracepoint,programs::TracePointContext};useaya_log_ebpf::info;#[tracepoint]pubfntest_aya(ctx:TracePointContext)->u32{matchtry_test_aya(ctx){Ok(ret)=>ret,Err(ret)=>ret,}}fntry_test_aya(ctx:TracePointContext)->Result<u32,u32>{info!(&ctx,"tracepoint sys_enter_execve called");Ok(0)}#[cfg(not(test))]#[panic_handler]fnpanic(_info:&core::panic::PanicInfo)->!{loop{}}
Enter fullscreen modeExit fullscreen mode

If you've been following part 1, there's one thing that should already shock you about Rust code: there is nomain() function. This is a prerequisite for writing an eBPF program. To overcome this problem, we use the notation:#![no_main]

Another more disturbing prerequisite is that the standard library (std) is forbidden. Only the core library and all libraries that use it are allowed. This means you can't use theprintln! macro. To tell Rust not to use the standard:#![no_std]

But then how are we going to display anything ifprintln! isn't possible? That's where Aya comes to the rescue:use aya_log_ebpf::info;
This library can be used to display messages if theRUST_LOG environment variable is set. It also includeswarn,error,debug andtrace (seedocumentation).
Thanks to this library, you can display :
info!(&ctx, "tracepoint sys_enter_execve called");

We're going to discard this part of the program:

#[cfg(not(test))]#[panic_handler]fnpanic(_info:&core::panic::PanicInfo)->!{loop{}}
Enter fullscreen modeExit fullscreen mode

Don't panic! We'll never change it. It's the code that makes the program work.
All that's left is to explain :

useaya_ebpf::{macros::tracepoint,programs::TracePointContext};#[tracepoint]pubfntest_aya(ctx:TracePointContext)->u32{matchtry_test_aya(ctx){Ok(ret)=>ret,Err(ret)=>ret,}}fntry_test_aya(ctx:TracePointContext)->Result<u32,u32>{Ok(0)}
Enter fullscreen modeExit fullscreen mode

Twoaya_ebpf libraries are used, including one for the tracepoint macro:

#[tracepoint]//herepubfntest_aya(ctx:TracePointContext)->u32{...}
Enter fullscreen modeExit fullscreen mode

In other words, we're going to create an eBPF tracepoint program with the functiontest_aya().

Thepub keyword is to say that the function is public; it's not really “important”, it's just to make the Aya framework work. If we remove these unwanted elements:

useaya_ebpf::programs::TracePointContext;fntest_aya(ctx:TracePointContext)->u32{matchtry_test_aya(ctx){Ok(ret)=>ret,Err(ret)=>ret,}}fntry_test_aya(ctx:TracePointContext)->Result<u32,u32>{Ok(0)}
Enter fullscreen modeExit fullscreen mode

We're back to a few things we've already seen in part 1.
Things to remember:

  • thetest_aya() function is attached to thetracepoint program.
  • All the work will be done in thetry_test_aya() function.
  • ctx: this is the context variable that will enable us to go beyond ahello world program.

As you can see, there's no mention ofsyscalls for the moment. I want this Tracepoint program to be attached to anexecve syscall. Good timing: let's take a look at the user-space code now.

User space

In this article, we won't be modifying the user space code. It can be found in this file:test-aya/src/main.rs.
I'll simplify its code for a better understanding:

useaya::programs::TracePoint;uselog::{debug,warn};usetokio::signal;#[tokio::main]asyncfnmain()->anyhow::Result<()>{letmutebpf=aya::Ebpf::load(aya::include_bytes_aligned!(concat!(env!("OUT_DIR"),"/test-aya")))?;//1ifletErr(e)=aya_log::EbpfLogger::init(&mutebpf){warn!("failed to initialize eBPF logger: {}",e);}//2letprogram:&mutTracePoint=ebpf.program_mut("test_aya").unwrap().try_into()?;//3program.load()?;//4program.attach("syscalls","sys_enter_execve")?;//5letctrl_c=signal::ctrl_c();println!("Waiting for Ctrl-C...");ctrl_c.await?;println!("Exiting...");Ok(())}
Enter fullscreen modeExit fullscreen mode

This Rust code is a little more traditional: you can use the standard library, as evidenced by theprintln! and themain() function.
I've commented out the important parts of the code with numbers:

  • 1: We'll load the previously compiled eBPF code into an ebpf variable
  • 2: We'll start displaying the eBPF logs (for example, theinfo! from the previous code) in the user space
  • 3: Convert the main function of the eBPF code intoTracepoint code
  • 4: Load the main program function (thetest_aya() function)
  • 5: Attach it to the tracepointsyscalls:sys_enter_execve

The rest of the code is for program operation, such as not quitting the program before the logs start.

Injects of eBPF program

If you're new to Rust, there are certainly some parts of the code that may seem obscure to you. That's a good thing! The next section is dedicated to Rust.


Rust, it's getting complicated!

Before modifying the code, I think it's worth reviewing the Rust language a little more theoretically. I didn't want to scare you too much in part 1 😃

Rust video game

Types

In Part 1, we looked at variable declaration and modification. But we didn't talk much about types. This is important because, for example, all functions must be filled in with the various types.
There are two types:

  • scalar types
  • compound types

Scalar types

Let's take a look at scalar types: for integers, for example, we can define very precisely how many bits they are encoded in:

integers types

It's often optional to declare the variable with its type, but to remove any ambiguity, we do it this way:

letmy_variable:i64=2055;
Enter fullscreen modeExit fullscreen mode

Withprintln!, you can display the contents in a different way, for example, you can convert to hexadecimal:

println!("{:x}",ma_variable);
Enter fullscreen modeExit fullscreen mode

That's the magic of the macro! There's also this kind of possibility withinfo! in Aya. For example, you can convert a number into anIP orMAC address: very useful for eBPF networking.

Let's finish with scalar types and talk aboutcasting, i.e. modifying the type. Let's take a look at this example:

fnaddition(x:u32,y:u32)->u32{x+y}fnmain(){leta:u16=10;letb:u16=20;letres=addition(a,b);//❌ ERROR: u16 != u32println!("{}",res);}
Enter fullscreen modeExit fullscreen mode

This program won't work because the arguments require u32 and not u16. It is of course possible to change the function directly, but you don't always have access to the function as you would in a library. How do I change the code? Use the keywordas:

fnaddition(x:u32,y:u32)->u32{x+y}fnmain(){leta:u16=10;letb:u16=20;letres=addition(aasu32,basu32);// come as you areprintln!("{}",res);}
Enter fullscreen modeExit fullscreen mode

Compound types

Now let's talk about compound types, for example how to represent integer arrays:

let_my_array:[i8;2]=[0,2];// An array of 2 entries of type i8
Enter fullscreen modeExit fullscreen mode

An array has a fixed number of entries: you can't add a number after the fact. There are also dynamic arrays in Rust, such asVec. However, eBPF programs (on the kernel side) cannot use them: they require arrays with a number of entries already defined before compilation. The strategy is therefore to evaluate the maximum number an array can have by filling it with 0.

let_zeros_array=[0u8;256];// An array of 256 entries of 0 of type u8
Enter fullscreen modeExit fullscreen mode

And what about strings?

The default string in Rust is an array (a slice, to be precise - I'll get a slap on the wrist if I don't) with a fixed number ofu8 entries (and thereforeUTF-8 encoding). It is not, as in other programming languages, an array of characters (char). The notation is&str.

Thus, it's not possible to concatenate in this way:

let_toto="aaaa";let_tutu="bbbb";let_toto=_toto+_tutu;
Enter fullscreen modeExit fullscreen mode

However, if we convert_toto toString, it works:

let_toto="aaaa";//_toto is of type &strlet_tutu="bbbb";//_tutu is of type &strlet_toto=_toto.to_owned()+_tutu;//_toto is of type String
Enter fullscreen modeExit fullscreen mode

String is the equivalent ofVec. This makes it possible to have arrays ofu8 dynamically.

Structures

Structures are a mixture of different types. The keyword isstruct. For example, to create a simple role-playing game :

structPerson{name:String,age:u32,}fnmain(){letperson=Person{name:String::from("Alice"),age:30,};println!("Name: {}, Age: {}",person.name,person.age);}
Enter fullscreen modeExit fullscreen mode

If you find initialization a little complicated, you can create methods with theimpl keyword:

structPerson{name:String,age:u32,}implPerson{fnnew(name:&str,age:u32)->Person{Person{name:String::from(name),age}}fnold(&mutself){//👴🏻self.age+=1;}fnchange_name(&mutself,name:&str){self.name=String::from(name);}}fnmain(){letmutperson=Person::new("Julia",40);println!("Before: Name: {}, Age: {}",person.name,person.age);person.age();person.changer_name("Julian");println!("After: Name: {}, Age: {}",person.name,person.age);}
Enter fullscreen modeExit fullscreen mode

As you can see, thanks to the structures, the programming language reads almost naturally. In a way, it's like an object language.
To give you a more realistic example, take a look at the code generated in the user area above:

letprogram:&mutTracePoint=ebpf.program_mut("test_aya").unwrap().try_into()?;program.load()?;program.attach("sys_enter_execve","syscalls")?;
Enter fullscreen modeExit fullscreen mode

TracePoint is actually a structure. Let's readthe documentation:

TracePoint Aya documentation

What can be deduced from the doc, the approximate code:

structTracePoint{[Idon'tknow]}implTracePoint{pubfnload(&mutself)->Result<(),ProgramError>{[seethesourcecode]}pubfnattach(&mutself,category:&str,name:&str)->Result<...,...>{[seethesourcecode]}}
Enter fullscreen modeExit fullscreen mode

Generic Functions

Previously, we had to cast to have the same type:

fnaddition(x:u32,y:u32)->u32{x+y}fnmain(){leta:u16=10;letb:u16=20;letres=addition(aasu32,basu32);//Hereprintln!("{}",res);}
Enter fullscreen modeExit fullscreen mode

Using generic function, we don't need to cast:

fnaddition<T>(x:T,y:T)->TwhereT:std::ops::Add<Output=T>,{x+y}fnmain(){leta:u16=10;letb:u16=20;letres=addition(a,b);println!("{}",res);}
Enter fullscreen modeExit fullscreen mode

This way, you won't have to think about which type you need to use the addition function.
The function is inevitably a little more complicated to write. We won't be writing any in this section. But for use via a library, it's pure bliss (I'm exaggerating a little bit).

Soon, we'll see a generic function:
Example of generic function

Result

We've been seeingResult<...,...> a lot since part 1. Let's talk about it in a little more detail.
Result is anenumeration. We haven't seen what it is, but to put it simply:Result lets you create functions that can result in a success or an error. This is elegant when used withmatch or? as we saw in Part 1.

Documentation of Result

As withgeneric functions,T andE can be of any type (within certain constraints).
As a reminder, here's a simple example of a function that returnsResult :

fnget_some_value(value:u32)->Result<u32,u32>{matchvalue>10{true=>Ok(value),false=>Err(0),}}constNUM:u32=55;fnmain()->Result<(),u32>{letv=get_some_value(NUM)?;println!("{}",v);Ok(())}
Enter fullscreen modeExit fullscreen mode

In this example, only integers greater than 10 are displayed.

Option

Option is anotherenumeration quite similar to Result: the function returns eitherSome(T) orNone. It's anoptional value.

Option documentation

To retrieve theT value, there's no equivalent to the question mark withResult. However, a trick is to convert the Option type withok_or(...) toResult and then add thequestion mark.
Here's an equivalent of the previous code with a function that returnsOption<T> :

fnget_some_value(value:u32)->Option<u32>{matchvalue>10{true=>Some(value),false=>None,}}constNUM:u32=55;fnmain()->Result<(),()>{letv=get_some_value(NUM).ok_or(())?;println!("{}",v);Ok(())}
Enter fullscreen modeExit fullscreen mode

Another solution to retrieve this value is to useunwrap() method:

fnget_some_value(value:u32)->Option<u32>{matchvalue>10{true=>Some(value),false=>None,}}constNUM:u32=55;fnmain()->Result<(),()>{letv=get_some_value(NUM).unwrap();//No need of "?" nowprintln!("{}",v);Ok(())}
Enter fullscreen modeExit fullscreen mode

But there's a bigBUT: it's not possible to useunwrap() in kernel space code. Why not? To cut a long story short:unwrap() can panic, and an eBPF program can't afford that:

Extract of Aya book

The piece of code that we discarded in the kernel space above :

#[cfg(not(test))]#[panic_handler]fnpanic(_info:&core::panic::PanicInfo)->!{loop{}}
Enter fullscreen modeExit fullscreen mode

It can be used to manage the case ofunwrap(), for example.

Memory Management

When a variable is created in a program, the compiler must take care of finding where in RAM to add the variable, as well as deleting it to avoid so-called memory leaks. For fixed variables such as integers or non-dynamic arrays, variables are stored instacks, so there are no problems with releasing them. Thus, because of the restrictions in kernel space programs, there are no clean-up problems in eBPF. Dynamic variables, on the other hand, are stored inheaps, and that's where the problems come in.

InC, we let the developer do this withmalloc andfree. If he forgets to free memory, it often goes unnoticed, but is a potential source of bugs.

In other languages (such asPython orGo), the garbage collector takes care of cleaning up automatically, without developer intervention. However, this is done at the expense of performance.

In Rust, we use the notion of ownership to solve the problem.

Let's take an example:

fnmain(){lets1=String::from("Hello");lets2=s1;println!("{}",s1);}
Enter fullscreen modeExit fullscreen mode
  • s1 has theString type, which is dynamic

This program cannot compile. Why?

  • s1 will reserve memory space in the heap:let s1 = String::from("Hello");
  • s2 will retrieve the ownership of this memory space:let s2 = s1;Thuss1 has lost ownership of this memory space:

Error of ownership

This means that no two (or more) variables can have the same memory space. Rust will automatically release a variable once it has left the scope.

Borrowing

The most elegant solution is to use areference (Keyword:&). These references are also known as safe pointers:

fnmain(){lets1=String::from("Hello");lets2=&s1;println!("{}",s1);println!("{}",s2);}
Enter fullscreen modeExit fullscreen mode

This will displayHello twice.
We say thats2borrowss1's value, buts1 retains its property.
Let's take one last example to show you that this isn't always obvious:

fnprint_length(s:String){println!("Length: {}",s.len());}fnmain(){lets=String::from("Hello");print_length(s);println!("{}",s);}
Enter fullscreen modeExit fullscreen mode

This code doesn't compile either. Why?
s will reserve a memory space in the heap:
let s = String::from("Hello");
Theprint_length() function will take the property of this memory space:
print_length(s);
At the end of this processing, Rust will delete this memory space:

fnprint_length(s:String){println!("Length: {}",s.len());}
Enter fullscreen modeExit fullscreen mode

So the display ofs crashes.

To solve this problem simply use this borrowing system:

fnprint_length(s:&String){// We add an "&"println!("Longueur: {}",s.len());}fnmain(){lets=String::from("Hello");print_length(&s);// We don't forget to also add itprintln!("{}",s);}
Enter fullscreen modeExit fullscreen mode

When you're not used to it, you often make the mistake. But the error messages are self-explanatory and therefore easy to correct.

Unsafe

Theunsafe keyword is very useful when working at low level. It's the sysadmin equivalent ofsudo. By default, Rust has protections, notably for reading memory. The unsafe keyword lets you tell Rust: "Don't worry, I know what I'm doing! If you setunsafe anywhere, Rust will remind you with a warning, or if you don't set unsafe anywhere, Rust will tell you to set it if that's what you really want to do.

Example of warning if you use safe and it is useless

Raw pointers

Let's finish this busy section with raw pointers. The notion is similar to the C pointer. It lets you know the address of the variable. There are two types of raw pointers:

  • If data of typeT cannot change :*const T
  • If data of typeT can change:*mut T

To convert to a raw pointer, use theas_ptr() function:

as_ptr() function documentation

Let's give an example:

fnmain(){leta="toto";letmemory_location=a.as_ptr()asusize;println!("toto is on the memory address: {:x}",memory_location);leta=12;letmemory_location=&aas*consti32asusize;println!("12 is on the memory address {:x}",memory_location);}
Enter fullscreen modeExit fullscreen mode

That's all for this Rust part. I hope it wasn't too complicated and dense to understand. If it was, don't worry: let us guide you through the rest, and we'll review each point for a concrete case.


Improving the program

What are we going to do?

We've already created a program that every time a binary is executed, it displays a few things in the logs. It would be nice to be able to see which binary is being executed. So we're going to create a little program that will log all the binaries that are executed on the machine. It might be handy to see this for security reasons.

Reminder

To do this, you'll need to modify the aya code in kernel spacetest-aya-ebpf/src/main.rs
This is the main part you'll need to modify:

fntry_test_aya(ctx:TracePointContext)->Result<u32,u32>{info!(&ctx,"tracepoint sys_enter_execve called");Ok(0)}
Enter fullscreen modeExit fullscreen mode

This is based on thectx variable, which is aTracePointContext structure.
To do this, we'll take a look atthe documentation:

ebpf Aya Documentation

The library is located at the program level:
use aya_ebpf::programs::TracePointContext;
This shows all the documentation for each type of eBPF program:

List of contexts for each program

This shows the functions that can be used withTracePointContext:

TracePointContext documentation

We're not interested in the first function, which is used to create aTracePointContext. It's probably used in the macro. That leaves us with the second function:read_at(). This function reads the tracepoint at a certain offset. How do we find this offset?

You need to look in the syscall tracepoint file. To find it, go to:/sys/kernel/debug/tracing/events/[category]/[name]. And the file name isformat.
So the file is here:/sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format.

Format file for sys_enter_execve tracepoint

We'll try to retrieve the filename with offset16:

fntry_test_aya(ctx:TracePointContext)->Result<u32,u32>{letfilename=ctx.read_at::<u64>(16);info!(&ctx,"tracepoint sys_enter_execve called");Ok(0)}
Enter fullscreen modeExit fullscreen mode

I wasn't sure what to put in place ofT, so I arbitrarily set the type tou64.
If you try to compile :

unsafe compilation error

It works better now withunsafe:

fntry_test_aya(ctx:TracePointContext)->Result<u32,u32>{letfilename=unsafe{ctx.read_at::<u64>(16)};info!(&ctx,"tracepoint sys_enter_execve called");Ok(0)}
Enter fullscreen modeExit fullscreen mode

Now let's try using this variable. The result of the read_at function isResult(u64, i64). The aim is to retrieveu64. Just use the question mark (?).
So we'll get :

fntry_test_aya(ctx:TracePointContext)->Result<u32,u32>{letfilename=unsafe{ctx.read_at::<u64>(16)?};info!(&ctx,"tracepoint sys_enter_execve called");Ok(0)}
Enter fullscreen modeExit fullscreen mode

Nevertheless, compilation won't work because if it makes an error, theread_at function returns ani64 and my final function returns au32.

read_at function documentation

We have to change:

fntry_test_aya(ctx:TracePointContext)->Result<u32,i64>{//u32 -> i64letfilename=unsafe{ctx.read_at::<u64>(16)?};info!(&ctx,"tracepoint sys_enter_execve called");Ok(0)}
Enter fullscreen modeExit fullscreen mode

Similarly, theTracepoint function must always return au32. We must therefore cast the type tou32 :

pubfntest_aya(ctx:TracePointContext)->u32{matchtry_test_aya(ctx){Ok(ret)=>ret,Err(ret)=>retasu32,//cast}}
Enter fullscreen modeExit fullscreen mode

With these type-matching modifications, it should compile. Let's have a look at the contents of filename. Here's the whole file now if you're lost :

#![no_std]#![no_main]useaya_ebpf::{macros::tracepoint,programs::TracePointContext};useaya_log_ebpf::info;#[tracepoint]pubfntest_aya(ctx:TracePointContext)->u32{matchtry_test_aya(ctx){Ok(ret)=>ret,Err(ret)=>retasu32,}}fntry_test_aya(ctx:TracePointContext)->Result<u32,i64>{letfilename=unsafe{ctx.read_at::<u64>(16)?};info!(&ctx,"tracepoint sys_enter_execve called {}",filename);// the modifOk(0)}#[cfg(not(test))]#[panic_handler]fnpanic(_info:&core::panic::PanicInfo)->!{loop{}}
Enter fullscreen modeExit fullscreen mode

If we run a cargo run again, here's what we see:

[INFO  test_aya] tracepoint sys_enter_execve called 94803283704040[INFO  test_aya] tracepoint sys_enter_execve called 94803283704112[INFO  test_aya] tracepoint sys_enter_execve called 94173001563176[INFO  test_aya] tracepoint sys_enter_execve called 824637710416[INFO  test_aya] tracepoint sys_enter_execve called 94865232898256[INFO  test_aya] tracepoint sys_enter_execve called 824638316624[INFO  test_aya] tracepoint sys_enter_execve called 94865232898256
Enter fullscreen modeExit fullscreen mode

These numbers are not file names. This was to be expected, given that the type isu64. What I notice is that when I run a command: I'm always number94865232898256 if it's the server it's the other numbers, if I change user I get a different number.
Well... that's not much help. Shall we give up? The documentation isn't complete, it doesn't say what it's really for.
I look at the source code of theread_at function:

pubunsafefnread_at<T>(&self,offset:usize)->Result<T,i64>{bpf_probe_read(self.ctx.add(offset)as*constT)}
Enter fullscreen modeExit fullscreen mode

And what doesbpf_probe_read do?

bpf_probe_read documentation

In this way, an address in memory is accessed. Now we need to think about how to successfully read this address.
I'll take a look at the helper functions:

helper functions documentation

I have the feeling that this is this function that i need:

bpf_probe_read_user_str_bytes documentation

Let's add these lines and see what happens:

letmutbuf=[0u8;16];//let's create a array of u8let_my_str_bytes=unsafe{aya_ebpf::helpers::bpf_probe_read_user_str_bytes(filename,&mutbuf)?};
Enter fullscreen modeExit fullscreen mode

Typing error compilation

So all we have to do is change theu64 type to*const u8 (a raw pointer) and we'll change the variable names to be consistent with the documentation:

letfilename_src_addr=unsafe{ctx.read_at::<*constu8>(16)?};letmutbuf=[0u8;16];let_filename_bytes:&[u8]=unsafe{aya_ebpf::helpers::bpf_probe_read_user_str_bytes(filename_src_addr,&mutbuf)?};info!(&ctx,"tracepoint sys_enter_execve called {}",filename_src_addrasu64);//We also cast
Enter fullscreen modeExit fullscreen mode

The compilation works! Now we have a byte-coded file name. Now we need to figure out how to convert it to&str.

Google search for that

That's what I want, but as I said earlier, eBPF doesn't accept the standard library (std).
Miracle! It also exists in thecore library :

from_utf8 documentation

So we add:
let _filename = core::str::from_utf8(_filename_bytes);
Let's see if it compiles:

Compilation work but problem with the verifier

So it compiled fine. But the eBPF verifier is not happy at all. The verifier is a kernel protection system that prevents an eBPF program from being launched if it considers it dangerous for the kernel. What we see in the screenshot is JIT code (Just In Time code).

verifier diagrams

The explanation is at the end:
Permission denied

In the documentation, there's another function that might suit us. Perhapsfrom_utf8 makes too many checks that are not compatible with the eBPF verifier?

alternative from_utf8 uncheck

Let's add_unchecked andunsafe:

let_filename=unsafe{core::str::from_utf8_unchecked(_filename_bytes)};
Enter fullscreen modeExit fullscreen mode

It compiles!
By modifying the log line:

info!(&ctx,"tracepoint sys_enter_execve called Binary: {}",_filename);
Enter fullscreen modeExit fullscreen mode

I connect withssh on the server where the program is installed, I see all the binaries that are executed :

It works: we can see binary but there is a problem

The code works, and if you clean it up afterwards, you'll get a code similar to this one

complete code

You can find the program inGitHub.


That's all for this part. I hope you enjoyed it. It was much more technical than the first part. In the next part, we'll be looking at a few things we've already mentioned: the eBPF map. This will, for example, solve the problem we have with the program: file names are truncated. With an eBPF map, we can solve the problem!

A very nice crab

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

CNCF Ambassador | Kubestronaut
  • Joined

More fromJoseph Ligier

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp