Skip to content

Commit

Permalink
Add eBPF program for controlling device access
Browse files Browse the repository at this point in the history
This is a simple program which allows mknod, a standard list of devices
to be allowed inside the container, and a hashmap mapping a list of devices
to allwoed accesses. This allows runtime update on whether a device is
allowed inside a container.

It is automatically compiled with build.rs.
  • Loading branch information
nbdd0121 committed Mar 4, 2024
1 parent 7145210 commit 14e6a71
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 2 deletions.
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,20 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install dependency
- name: Install apt dependency
run: |
sudo apt-get update
sudo apt-get install -y libudev-dev
# Need to use nightly toolchain for eBPF
- uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src

- name: Install bpf-linker
run: |
cargo install bpf-linker
- name: Build
run: cargo build --release

Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
/ott
/cgroup_device_filter/target
/ott
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ bollard = "0.16"
futures = "0.3"
rustix = { version = "0.38", features = ["fs", "stdio", "termios"] }
bitflags = "2"

[build-dependencies]
anyhow = { version = "1", features = ["backtrace"] }
walkdir = "2"

[workspace]
exclude = ["cgroup_device_filter"]
35 changes: 35 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use anyhow::{Context, Result};

fn main() -> Result<()> {
// We need to rerun the build script if any files in the cgroup_device_filter change.
for entry in walkdir::WalkDir::new("cgroup_device_filter")
.into_iter()
.filter_entry(|entry| {
entry
.file_name()
.to_str()
.map(|s| s != "target")
.unwrap_or(true)
})
{
let entry = entry?;
if entry.file_type().is_file() {
println!(
"cargo:rerun-if-changed={}",
entry.path().to_str().context("file name not UTF-8")?
);
}
}

// Run cargo to compile the eBPF program.
let status = std::process::Command::new("cargo")
.current_dir("cgroup_device_filter")
.args(["build", "--release"])
.status()?;

if !status.success() {
anyhow::bail!("Failed to build eBPF program");
}

Ok(())
}
5 changes: 5 additions & 0 deletions cgroup_device_filter/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build]
target = "bpfel-unknown-none"

[unstable]
build-std = ["core"]
115 changes: 115 additions & 0 deletions cgroup_device_filter/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions cgroup_device_filter/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "cgroup_device_filter"
version = "0.1.0"
edition = "2021"

[dependencies]
aya-bpf = { git = "https://github.com/aya-rs/aya.git" }
2 changes: 2 additions & 0 deletions cgroup_device_filter/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"
118 changes: 118 additions & 0 deletions cgroup_device_filter/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#![no_std]
#![no_main]

use aya_bpf::bindings::{
BPF_DEVCG_ACC_MKNOD, BPF_DEVCG_DEV_BLOCK, BPF_DEVCG_DEV_CHAR, BPF_F_NO_PREALLOC,
};
use aya_bpf::macros::{cgroup_device, map};
use aya_bpf::maps::HashMap;
use aya_bpf::programs::DeviceContext;

#[repr(C)]
#[derive(Clone, Copy, PartialEq, Eq)]
struct Device {
/// Type of device. BPF_DEVCG_DEV_BLOCK or BPF_DEVCG_DEV_CHAR.
ty: u32,
major: u32,
minor: u32,
}

const DEV_NULL: Device = Device {
ty: BPF_DEVCG_DEV_CHAR,
major: 1,
minor: 3,
};

const DEV_ZERO: Device = Device {
ty: BPF_DEVCG_DEV_CHAR,
major: 1,
minor: 5,
};

const DEV_FULL: Device = Device {
ty: BPF_DEVCG_DEV_CHAR,
major: 1,
minor: 7,
};

const DEV_RANDOM: Device = Device {
ty: BPF_DEVCG_DEV_CHAR,
major: 1,
minor: 8,
};

const DEV_URANDOM: Device = Device {
ty: BPF_DEVCG_DEV_CHAR,
major: 1,
minor: 9,
};

const DEV_TTY: Device = Device {
ty: BPF_DEVCG_DEV_CHAR,
major: 5,
minor: 0,
};

const DEV_CONSOLE: Device = Device {
ty: BPF_DEVCG_DEV_CHAR,
major: 5,
minor: 1,
};

const DEV_PTMX: Device = Device {
ty: BPF_DEVCG_DEV_CHAR,
major: 5,
minor: 2,
};

#[map(name = "DEVICE_PERM")]
/// Hashmap storing a device -> permission mapping.
///
/// This is modified from user-space to change permission.
static DEVICE_PERM: HashMap<Device, u32> = HashMap::with_max_entries(256, BPF_F_NO_PREALLOC);

#[cgroup_device]
fn check_device(ctx: DeviceContext) -> i32 {
// SAFETY: This is a POD supplied by the kernel.
let ctx_dev = unsafe { *ctx.device };
let dev = Device {
// access_type's lower 16 bits are the device type, upper 16 bits are the access type.
ty: ctx_dev.access_type & 0xFFFF,
major: ctx_dev.major,
minor: ctx_dev.minor,
};
let access = ctx_dev.access_type >> 16;

// Always allow mknod, we restrict on access not on creation.
// This is consistent with eBPF genereated by Docker.
if matches!(dev.ty, BPF_DEVCG_DEV_BLOCK | BPF_DEVCG_DEV_CHAR) && access == BPF_DEVCG_ACC_MKNOD {
return 1;
}

// Allow default devices for containers
// https://github.com/opencontainers/runtime-spec/blob/main/config-linux.md
match dev {
DEV_NULL | DEV_ZERO | DEV_FULL | DEV_RANDOM | DEV_URANDOM => return 1,
DEV_TTY | DEV_CONSOLE | DEV_PTMX => return 1,
// Pseudo-PTY
Device {
ty: BPF_DEVCG_DEV_CHAR,
major: 136,
minor: _,
} => return 1,
_ => (),
}

// For extra devices, check the map.
// SAFETY: we have BPF_F_NO_PREALLOC enabled so the map is safe to access concurrently.
let device_perm = unsafe { DEVICE_PERM.get(&dev).copied() };
match device_perm {
Some(perm) => (perm & access == access) as i32,
None => 0,
}
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}

0 comments on commit 14e6a71

Please sign in to comment.