Full code for the example in this chapter is available here
What is LSM
LSM stands for Linux Security Modules which is a framework which allows developers to write security systems on top of the Linux kernel. It's also briefly described in the Linux kernel documentation.
LSM is used by kernel modules or (since kernel 5.7) by eBPF programs. The most popular modules that make use of LSM are AppArmor, SELinux, Smack and TOMOYO. eBPF LSM programs allow developers to implement the same functionality implemented by the modules just mentioned, using eBPF APIs.
The central concept behind LSM is LSM hooks. LSM hooks are exposed in key locations in the kernel, and eBPF programs can attach to them to implement custom security policies. Examples of operations that can be policied via hooks include:
- filesystem operations
- opening, creating, moving and removing files
- mounting and unmounting filesystems
- task/process operations
- allocating and freeing tasks, changing user and group identify for a task
- socket operations
- creating and binding sockets
- receiving and sending messages
Each of those actions has a corresponding LSM hook. All LSM hooks are listed in the lsm_hooks.h header inside the Linux kernel source code. Each hook takes a number of arguments, which provide context based on which programs can implement policy decisions and are listed in the lsm_hook_defs.h header.
For example, consider the
task_setnice hook, which has the following
The hook is triggered when a nice value is set for any process in the system. If you are not familiar with the concept of process niceness, check out this article. As you can see from the definition, this hook takes the following arguments:
pis the instance of
task_structwhich represents the process on which the nice value is set
niceis the nice value
By attaching to the hook, an eBPF program can decide whether to accept or reject the given nice value.
In addition to the arguments found in the hook definition, eBPF programs have
access to one extra argument -
ret - which is a return value of potential
previous eBPF LSM programs.
Ensure that BPF LSM is enabled
Before proceeding further and trying to write a BPF LSM program, please make sure that:
- Your kernel version is at least 5.7.
- BPF LSM is enabled.
The second point can be checked with:
The correct output should contain
bpf. If it doesn't, BPF LSM has to be
manually enabled by adding it to kernel config parameters. It can be achieved
by editing the GRUB config in
/etc/default/grub and adding the following to
the kernel parameters:
Then rebuilding the GRUB configuration with any of the commands listed below (each of them might be available or not in different Linux distributions):
And finally, rebooting the system.
Writing LSM BPF program
Let's try to create an LSM eBPF program which which is triggered by
task_setnice hook. The purpose of this program will be denying setting the
nice value lower than 0 (which means higher priority), for a particular process.
renice tool can be used to change niceness values:
With our eBPF program, we want to make it impossible to call
renice for a
pid with a negative
eBPF projects come with two parts: eBPF program(s) and the userspace program. To make our example simple, we can try to deny a change of a nice value of the userspace process which loads the eBPF program.
The first step is to create a new project:
$ cargo generate --name lsm-nice -d program_type=lsm -d lsm_hook=task_setnice https://github.com/aya-rs/aya-template
That command should create a new Aya project with an empty program attaching to
task_setnice hook. Let's go to its directory:
task_setnicehook is a pointer to a task_struct type. Therefore we need to generate a binding to
If you are not familiar with aya-tool, please refer to this section.
Now it's time to modify the
lsm-nice-ebpf project and write an actual program
there. The full program code should look like this:
- We include the autogenerated binding to
- Then we define a global variable
PID. We initialize the value to 0, but at runtime the userspace side will patch the value with the actual pid we're interested in.
- Finally we have the program and the logic what to do with nice values.
After that we also need to modify the userspace part. We don't need as much work as with the eBPF part, but we need to:
- Get the PID.
- Log it.
- Write it to the global variable in the eBPF object.
The final result should look like:
- Where we start with getting and logging a PID:
- And then we set the global variable:
After that, we can build and run our project with:
The output should contain our log line showing the PID of the userspace process, i.e.:
Now we can try to change the nice value for that process. Setting a positive value (lowering the priority) should still work:
But setting a negative value should not be allowed:
$ renice -10 -p 587184 renice: failed to set priority for 587184 (process ID): Operation not permitted
If doing that resulted in
Operation not permitted, congratulations, your LSM
eBPF program works!