use oci_spec::runtime::*;
use rbpf::disassembler::disassemble;
use rbpf::insn_builder::{Arch as RbpfArch, *};
pub struct Program {
prog: BpfCode,
}
#[derive(thiserror::Error, Debug)]
pub enum ProgramError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid access: {0}")]
InvalidAccess(char),
#[error("{0} device not supported")]
DeviceNotSupported(&'static str),
#[error("wildcard device type should be removed when cleaning rules")]
WildcardDevice,
}
impl Program {
pub fn from_rules(
rules: &[LinuxDeviceCgroup],
default_allow: bool,
) -> Result<Self, ProgramError> {
let mut prog = Program {
prog: BpfCode::new(),
};
prog.init();
for rule in rules.iter().rev() {
prog.add_rule(rule)?;
}
prog.finalize(default_allow);
Ok(prog)
}
pub fn bytecodes(&self) -> &[u8] {
self.prog.into_bytes()
}
fn finalize(&mut self, default_allow: bool) {
self.prog
.mov(Source::Imm, RbpfArch::X32)
.set_dst(0)
.set_imm(default_allow as i32)
.push();
self.prog.exit().push();
}
fn init(&mut self) {
self.prog
.load_x(MemSize::Word)
.set_src(1)
.set_off(0)
.set_dst(2)
.push();
self.prog
.bit_and(Source::Imm, RbpfArch::X32)
.set_dst(2)
.set_imm(0xFFFF)
.push();
self.prog
.load_x(MemSize::Word)
.set_src(1)
.set_off(0)
.set_dst(3)
.push();
self.prog
.right_shift(Source::Imm, RbpfArch::X32)
.set_imm(16)
.set_dst(3)
.push();
self.prog
.load_x(MemSize::Word)
.set_src(1)
.set_off(4)
.set_dst(4)
.push();
self.prog
.load_x(MemSize::Word)
.set_src(1)
.set_off(8)
.set_dst(5)
.push();
}
fn add_rule(&mut self, rule: &LinuxDeviceCgroup) -> Result<(), ProgramError> {
let dev_type = bpf_dev_type(rule.typ().unwrap_or_default())?;
let access = bpf_access(rule.access().clone().unwrap_or_default())?;
let has_access = access
!= (libbpf_sys::BPF_DEVCG_ACC_READ
| libbpf_sys::BPF_DEVCG_ACC_WRITE
| libbpf_sys::BPF_DEVCG_ACC_MKNOD);
let has_major = rule.major().is_some() && rule.major().unwrap() >= 0;
let has_minor = rule.minor().is_some() && rule.minor().unwrap() >= 0;
let mut instruction_count = 1; if has_access {
instruction_count += 3;
}
if has_major {
instruction_count += 1;
}
if has_minor {
instruction_count += 1;
}
instruction_count += 2;
let mut next_rule_offset = instruction_count - 1;
self.prog
.jump_conditional(Cond::NotEquals, Source::Imm)
.set_dst(2)
.set_imm(dev_type as i32)
.set_off(next_rule_offset)
.push();
if has_access {
next_rule_offset -= 3;
self.prog
.mov(Source::Reg, RbpfArch::X32)
.set_dst(1)
.set_src(3)
.push();
self.prog
.bit_and(Source::Imm, RbpfArch::X32)
.set_dst(1)
.set_imm(access as i32)
.push();
self.prog
.jump_conditional(Cond::NotEquals, Source::Reg)
.set_dst(1)
.set_src(3)
.set_off(next_rule_offset)
.push();
}
if has_major {
next_rule_offset -= 1;
self.prog
.jump_conditional(Cond::NotEquals, Source::Imm)
.set_dst(4)
.set_imm(rule.major().unwrap() as i32)
.set_off(next_rule_offset)
.push();
}
if has_minor {
next_rule_offset -= 1;
self.prog
.jump_conditional(Cond::NotEquals, Source::Imm)
.set_dst(5)
.set_imm(rule.minor().unwrap() as i32)
.set_off(next_rule_offset)
.push();
}
self.prog
.mov(Source::Imm, RbpfArch::X32)
.set_dst(0)
.set_imm(rule.allow() as i32)
.push();
self.prog.exit().push();
Ok(())
}
pub fn dump(&self) {
disassemble(self.prog.into_bytes());
}
pub fn execute(
&self,
typ: LinuxDeviceType,
major: u32,
minor: u32,
access: String,
) -> Result<u64, ProgramError> {
let mut mem = bpf_cgroup_dev_ctx(typ, major, minor, access)?;
let vm = rbpf::EbpfVmRaw::new(Some(self.prog.into_bytes()))?;
let result = vm.execute_program(&mut mem[..])?;
Ok(result)
}
}
fn bpf_dev_type(typ: LinuxDeviceType) -> Result<u32, ProgramError> {
let dev_type: u32 = match typ {
LinuxDeviceType::C => libbpf_sys::BPF_DEVCG_DEV_CHAR,
LinuxDeviceType::U => return Err(ProgramError::DeviceNotSupported("unbuffered char")),
LinuxDeviceType::B => libbpf_sys::BPF_DEVCG_DEV_BLOCK,
LinuxDeviceType::P => return Err(ProgramError::DeviceNotSupported("pipe device")),
LinuxDeviceType::A => return Err(ProgramError::WildcardDevice),
};
Ok(dev_type)
}
fn bpf_access(access: String) -> Result<u32, ProgramError> {
let mut v = 0_u32;
for c in access.chars() {
let cur_access = match c {
'r' => libbpf_sys::BPF_DEVCG_ACC_READ,
'w' => libbpf_sys::BPF_DEVCG_ACC_WRITE,
'm' => libbpf_sys::BPF_DEVCG_ACC_MKNOD,
_ => return Err(ProgramError::InvalidAccess(c)),
};
v |= cur_access;
}
Ok(v)
}
fn bpf_cgroup_dev_ctx(
typ: LinuxDeviceType,
major: u32,
minor: u32,
access: String,
) -> Result<Vec<u8>, ProgramError> {
let mut mem = Vec::with_capacity(12);
let mut type_access = 0_u32;
if let Ok(t) = bpf_dev_type(typ) {
type_access = t & 0xFFFF;
}
type_access |= bpf_access(access)? << 16;
mem.extend_from_slice(&type_access.to_ne_bytes());
mem.extend_from_slice(&major.to_ne_bytes());
mem.extend_from_slice(&minor.to_ne_bytes());
Ok(mem)
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use oci_spec::runtime::LinuxDeviceCgroupBuilder;
use super::*;
fn build_bpf_program(rules: &Option<Vec<LinuxDeviceCgroup>>) -> Result<Program> {
let mut em = crate::v2::devices::emulator::Emulator::with_default_allow(false);
if let Some(rules) = rules {
em.add_rules(rules);
}
Ok(Program::from_rules(&em.rules, em.default_allow)?)
}
#[test]
fn test_devices_allow_single() {
let rules = vec![
LinuxDeviceCgroupBuilder::default()
.allow(true)
.typ(LinuxDeviceType::C)
.major(10)
.minor(20)
.access("r")
.build()
.unwrap(),
];
let prog = build_bpf_program(&Some(rules)).unwrap();
let ty_list = vec![
LinuxDeviceType::C,
LinuxDeviceType::U,
LinuxDeviceType::P,
LinuxDeviceType::B,
];
let major_list = vec![10_u32, 99_u32];
let minor_list = vec![20_u32, 00_u32];
let access_list = vec!["r", "w", "m"];
for ty in &ty_list {
for major in &major_list {
for minor in &minor_list {
for access in &access_list {
let ret = prog.execute(*ty, *major, *minor, access.to_string());
assert!(ret.is_ok());
println!("execute {ty:?} {major} {minor} {access} -> {ret:?}");
if *ty == LinuxDeviceType::C && *major == 10
&& *minor == 20
&& access.eq(&"r")
{
assert_eq!(ret.unwrap(), 1);
} else {
assert_eq!(ret.unwrap(), 0);
}
}
}
}
}
}
#[test]
fn test_devices_deny_all() {
let rules = vec![];
let prog = build_bpf_program(&Some(rules)).unwrap();
let ty_list = vec![
LinuxDeviceType::C,
LinuxDeviceType::U,
LinuxDeviceType::P,
LinuxDeviceType::B,
];
let major_list = vec![10_u32, 99_u32];
let minor_list = vec![20_u32, 00_u32];
let access_list = vec!["r", "w", "m"];
for ty in &ty_list {
for major in &major_list {
for minor in &minor_list {
for access in &access_list {
let ret = prog.execute(*ty, *major, *minor, access.to_string());
assert!(ret.is_ok());
assert_eq!(ret.unwrap(), 0);
}
}
}
}
}
#[test]
fn test_devices_allow_all() {
let rules = vec![
LinuxDeviceCgroupBuilder::default()
.allow(true)
.typ(LinuxDeviceType::A)
.build()
.unwrap(),
];
let prog = build_bpf_program(&Some(rules)).unwrap();
let ty_list = vec![
LinuxDeviceType::C,
LinuxDeviceType::U,
LinuxDeviceType::P,
LinuxDeviceType::B,
];
let major_list = vec![10_u32, 99_u32];
let minor_list = vec![20_u32, 00_u32];
let access_list = vec!["r", "w", "m"];
for ty in &ty_list {
for major in &major_list {
for minor in &minor_list {
for access in &access_list {
let ret = prog.execute(*ty, *major, *minor, access.to_string());
assert!(ret.is_ok());
println!("execute {ty:?} {major} {minor} {access} -> {ret:?}");
assert_eq!(ret.unwrap(), 1);
}
}
}
}
}
#[test]
fn test_devices_allow_wildcard() {
let rules = vec![
LinuxDeviceCgroupBuilder::default()
.allow(true)
.typ(LinuxDeviceType::C)
.minor(20)
.access("r")
.build()
.unwrap(),
];
let prog = build_bpf_program(&Some(rules)).unwrap();
let ty_list = vec![
LinuxDeviceType::C,
LinuxDeviceType::U,
LinuxDeviceType::P,
LinuxDeviceType::B,
];
let major_list = vec![10_u32, 99_u32];
let minor_list = vec![20_u32, 00_u32];
let access_list = vec!["r", "w", "m"];
for ty in &ty_list {
for major in &major_list {
for minor in &minor_list {
for access in &access_list {
let ret = prog.execute(*ty, *major, *minor, access.to_string());
assert!(ret.is_ok());
println!("execute {ty:?} {major} {minor} {access} -> {ret:?}");
if *ty == LinuxDeviceType::C && *minor == 20 && access.eq(&"r") {
assert_eq!(ret.unwrap(), 1);
} else {
assert_eq!(ret.unwrap(), 0);
}
}
}
}
}
}
#[test]
fn test_devices_allow_and_deny() {
let rules = vec![
LinuxDeviceCgroupBuilder::default()
.allow(true)
.typ(LinuxDeviceType::C)
.minor(20)
.access("rw")
.build()
.unwrap(),
LinuxDeviceCgroupBuilder::default()
.allow(false)
.typ(LinuxDeviceType::C)
.major(10)
.access("r")
.build()
.unwrap(),
];
let prog = build_bpf_program(&Some(rules)).unwrap();
let ty_list = vec![
LinuxDeviceType::C,
LinuxDeviceType::U,
LinuxDeviceType::P,
LinuxDeviceType::B,
];
let major_list = vec![10_u32, 99_u32];
let minor_list = vec![20_u32, 00_u32];
let access_list = vec!["r", "w", "m"];
for ty in &ty_list {
for major in &major_list {
for minor in &minor_list {
for access in &access_list {
let ret = prog.execute(*ty, *major, *minor, access.to_string());
assert!(ret.is_ok());
println!("execute {ty:?} {major} {minor} {access} -> {ret:?}");
if *ty == LinuxDeviceType::C && *major == 10 && access.eq(&"r") {
assert_eq!(ret.unwrap(), 0);
} else if *ty == LinuxDeviceType::C
&& *minor == 20
&& (access.eq(&"r") || access.eq(&"w"))
{
assert_eq!(ret.unwrap(), 1);
} else {
assert_eq!(ret.unwrap(), 0);
}
}
}
}
}
}
}