From dc435a4242e0578f95c835a3630c9bc35fde503c Mon Sep 17 00:00:00 2001 From: Gary Guo Date: Fri, 1 Mar 2024 17:28:51 +0000 Subject: [PATCH] Add eBPF program for controlling device access 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. --- .github/workflows/ci.yml | 9 +- .gitignore | 3 +- Cargo.lock | 29 ++++++ Cargo.toml | 7 ++ build.rs | 35 +++++++ cgroup_device_filter/.cargo/config.toml | 5 + cgroup_device_filter/Cargo.lock | 115 ++++++++++++++++++++++ cgroup_device_filter/Cargo.toml | 7 ++ cgroup_device_filter/rust-toolchain.toml | 2 + cgroup_device_filter/src/main.rs | 118 +++++++++++++++++++++++ 10 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 build.rs create mode 100644 cgroup_device_filter/.cargo/config.toml create mode 100644 cgroup_device_filter/Cargo.lock create mode 100644 cgroup_device_filter/Cargo.toml create mode 100644 cgroup_device_filter/rust-toolchain.toml create mode 100644 cgroup_device_filter/src/main.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8454d46..ed79cdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,11 +9,18 @@ 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 + + - name: Install bpf-linker + run: | + cargo install bpf-linker + - name: Build run: cargo build --release diff --git a/.gitignore b/.gitignore index 27db599..21ad7f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -/ott \ No newline at end of file +/cgroup_device_filter/target +/ott diff --git a/Cargo.lock b/Cargo.lock index 8c9c063..d7e7ac2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,6 +339,7 @@ dependencies = [ "udev", "unescape", "usb-ids", + "walkdir", ] [[package]] @@ -1063,6 +1064,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1476,6 +1486,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -1561,6 +1581,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 84cec1b..455b114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..3c51c9e --- /dev/null +++ b/build.rs @@ -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(()) +} diff --git a/cgroup_device_filter/.cargo/config.toml b/cgroup_device_filter/.cargo/config.toml new file mode 100644 index 0000000..d6b50e7 --- /dev/null +++ b/cgroup_device_filter/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "bpfel-unknown-none" + +[unstable] +build-std = ["core"] diff --git a/cgroup_device_filter/Cargo.lock b/cgroup_device_filter/Cargo.lock new file mode 100644 index 0000000..b3dbe47 --- /dev/null +++ b/cgroup_device_filter/Cargo.lock @@ -0,0 +1,115 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aya-bpf" +version = "0.1.0" +source = "git+https://github.com/aya-rs/aya.git#b6a84b658ae00f23d0f1721c30d11f2e57f99eab" +dependencies = [ + "aya-bpf-bindings", + "aya-bpf-cty", + "aya-bpf-macros", + "rustversion", +] + +[[package]] +name = "aya-bpf-bindings" +version = "0.1.0" +source = "git+https://github.com/aya-rs/aya.git#b6a84b658ae00f23d0f1721c30d11f2e57f99eab" +dependencies = [ + "aya-bpf-cty", +] + +[[package]] +name = "aya-bpf-cty" +version = "0.2.1" +source = "git+https://github.com/aya-rs/aya.git#b6a84b658ae00f23d0f1721c30d11f2e57f99eab" + +[[package]] +name = "aya-bpf-macros" +version = "0.1.0" +source = "git+https://github.com/aya-rs/aya.git#b6a84b658ae00f23d0f1721c30d11f2e57f99eab" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cgroup_device_filter" +version = "0.1.0" +dependencies = [ + "aya-bpf", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/cgroup_device_filter/Cargo.toml b/cgroup_device_filter/Cargo.toml new file mode 100644 index 0000000..13b4603 --- /dev/null +++ b/cgroup_device_filter/Cargo.toml @@ -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" } diff --git a/cgroup_device_filter/rust-toolchain.toml b/cgroup_device_filter/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/cgroup_device_filter/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/cgroup_device_filter/src/main.rs b/cgroup_device_filter/src/main.rs new file mode 100644 index 0000000..00b2965 --- /dev/null +++ b/cgroup_device_filter/src/main.rs @@ -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 = 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 {} +}