
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.
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:
Types of eBPF programs
To generate an Aya program, we'll use cargo generate :
cargo generate https://github.com/aya-rs/aya-template
We're going to answer a few questions:
Project Name
: the name of the project (I puttest-aya
)- The second question is more interesting:
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:
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.
All these points are accessible in the/sys/kernel/debug/tracing/available_events
file:
They are in the format:
[Category]:[Name]
Just in time, this is the supplementary question we ask:
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).
To view the syscalls performed when a command is run, you can use the strace program. For example, you can use:
Which syscall to use
To find available tracepoint names, simply issue this command:
grep syscalls: /sys/kernel/debug/tracing/available_events
We can see that they are of the :sys_(enter|exit)_$name_of_syscall
enter
: starting of syscallexit
: 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.
Test
Now we're going to test the generated program directly:
cd test-ayaRUST_LOG=info cargo run
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
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.
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{}}
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{}}
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)}
Twoaya_ebpf
libraries are used, including one for the tracepoint macro:
#[tracepoint]//herepubfntest_aya(ctx:TracePointContext)->u32{...}
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)}
We're back to a few things we've already seen in part 1.
Things to remember:
- the
test_aya()
function is attached to thetracepoint program. - All the work will be done in the
try_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(())}
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, the
info!
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 (the
test_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.
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 😃
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:
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;
Withprintln!
, you can display the contents in a different way, for example, you can convert to hexadecimal:
println!("{:x}",ma_variable);
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);}
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);}
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
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
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;
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
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);}
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);}
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")?;
TracePoint is actually a structure. Let's readthe 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]}}
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);}
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);}
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:
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.
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(())}
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.
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(())}
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(())}
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:
The piece of code that we discarded in the kernel space above :
#[cfg(not(test))]#[panic_handler]fnpanic(_info:&core::panic::PanicInfo)->!{loop{}}
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);}
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:
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);}
This will displayHello
twice.
We say thats2
borrowss1
'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);}
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());}
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);}
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.
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:
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);}
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)}
This is based on thectx variable, which is aTracePointContext structure.
To do this, we'll take a look atthe 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:
This shows the functions that can be used withTracePointContext:
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
.
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)}
I wasn't sure what to put in place ofT, so I arbitrarily set the type tou64
.
If you try to compile :
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)}
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)}
Nevertheless, compilation won't work because if it makes an error, theread_at
function returns ani64
and my final function returns au32
.
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)}
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}}
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{}}
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
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)}
And what doesbpf_probe_read
do?
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:
I have the feeling that this is this function that i need:
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)?};
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
The compilation works! Now we have a byte-coded file name. Now we need to figure out how to convert it to&str
.
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 :
So we add:let _filename = core::str::from_utf8(_filename_bytes);
Let's see if it compiles:
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).
The explanation is at the end:
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?
Let's add_unchecked
andunsafe
:
let_filename=unsafe{core::str::from_utf8_unchecked(_filename_bytes)};
It compiles!
By modifying the log line:
info!(&ctx,"tracepoint sys_enter_execve called Binary: {}",_filename);
I connect withssh
on the server where the program is installed, I see all the binaries that are executed :
The code works, and if you clean it up afterwards, you'll get a code similar to this one
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!
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse