LSM
Source Code
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. Each hook takes a number of arguments, which provides context about the program and it's operation in order to implement policy decisions. The list of hooks with their arguments can be found in the lsm_hook_defs.h header.
For example, consider the task_setnice
hook, which has the following
definition:
```c
LSM_HOOK(int, 0, task_setnice, struct task_struct *p, int nice)
```
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:
p
is the instance oftask_struct
which represents the process on which the nice value is setnice
is 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:
```console
$ cat /sys/kernel/security/lsm
capability,lockdown,landlock,yama,apparmor,bpf
```
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:
```console
GRUB_CMDLINE_LINUX="lsm=[YOUR CURRENTLY ENABLED LSMs],bpf"
```
Then rebuilding the GRUB configuration with any of the commands listed below (each of them might be available or not in different Linux distributions):
```console
# update-grub2
```
```console
# grub2-mkconfig -o /boot/grub2/grub.cfg
```
```console
# grub-mkconfig -o /boot/grub/grub.cfg
```
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.
The renice
tool can be used to change niceness values:
```console
$ renice [value] -p [pid]
```
With our eBPF program, we want to make it impossible to call renice
for a
given pid
with a negative [value]
.
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:
```console
$ 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
the task_setnice
hook. Let's go to its directory:
```console
$ cd lsm-nice
```
One of the arguments passed to the task_setnice
hook is a pointer to a
task_struct type.
Therefore we need to generate a binding to task_struct
with aya-tool.
If you are not familiar with aya-tool, please refer to this section.
```console
$ aya-tool generate task_struct > lsm-nice-ebpf/src/vmlinux.rs
```
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:
```rust linenums="1" title="lsm-nice-ebpf/src/main.rs"
#![no_std]
#![no_main]
use aya_ebpf::{cty::c_int, macros::lsm, programs::LsmContext};
use aya_log_ebpf::info;
// (1)
#[allow(non_upper_case_globals)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
#[allow(dead_code)]
mod vmlinux;
use vmlinux::task_struct;
// (2)
/// PID of the process for which setting a negative nice value is denied.
#[no_mangle]
static PID: i32 = 0;
#[lsm(hook = "task_setnice")]
pub fn task_setnice(ctx: LsmContext) -> i32 {
match unsafe { try_task_setnice(ctx) } {
Ok(ret) => ret,
Err(ret) => ret,
}
}
// (3)
unsafe fn try_task_setnice(ctx: LsmContext) -> Result<i32, i32> {
let p: *const task_struct = ctx.arg(0);
let nice: c_int = ctx.arg(1);
let ret: c_int = ctx.arg(2);
let global_pid: c_int = core::ptr::read_volatile(&PID);
let pid: c_int = (*p).pid;
info!(&ctx,
"The PID supplied to this program is: {}, with nice value {} and return value {}. Monitoring for changes in PID: {}",
pid, nice, ret, global_pid);
if ret != 0 {
return Err(ret);
}
if pid == global_pid && nice < 0 {
return Err(-1);
}
Ok(0)
}
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
```
- We include the autogenerated binding to
task_struct
: - 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:
```rust linenums="1" title="lsm-nice/src/main.rs"
use std::process;
use aya::{include_bytes_aligned, programs::Lsm, BpfLoader, Btf};
use aya_log::BpfLogger;
use log::{info, warn};
use tokio::signal;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
env_logger::init();
// (1)
let pid = process::id() as i32;
info!("PID: {}", pid);
// 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 `Bpf::load_file` instead.
#[cfg(debug_assertions)]
let mut bpf = BpfLoader::new().set_global("PID", &pid, true).load(
include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/lsm-nice"
),
)?;
#[cfg(not(debug_assertions))]
let mut bpf = BpfLoader::new().set_global("PID", &pid, true).load(
include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/lsm-nice"
),
)?;
if let Err(e) = BpfLogger::init(&mut bpf) {
// This can happen if you remove all log statements from your eBPF program.
warn!("failed to initialize eBPF logger: {}", e);
}
let btf = Btf::from_sys_fs()?;
let program: &mut Lsm =
bpf.program_mut("task_setnice").unwrap().try_into()?;
program.load("task_setnice", &btf)?;
program.attach()?;
info!("Waiting for Ctrl-C...");
signal::ctrl_c().await?;
info!("Exiting...");
Ok(())
}
```
- 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:
```console
$ RUST_LOG=info cargo xtask run
```
The output should contain our log line showing the PID of the userspace process, i.e.:
```console
16:32:30 [INFO] lsm_nice: [lsm-nice/src/main.rs:22] PID: 573354
```
Now we can try to change the nice value for that process. Setting a positive value (lowering the priority) should still work:
```console
$ renice 10 -p 587184
587184 (process ID) old priority 0, new priority 10
```
But setting a negative value should not be allowed:
```console
$ 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!