use super::uevent::Uevent;
const SCAN_LIMIT: u32 = 320;
const SCAN_STEP_INSNS: usize = 6;
const SUBSYSTEM_KEY_BIAS: u32 = 17;
const SOCK_FILTER_SIZE: usize = 8;
const BPF_LD: u16 = 0x00;
const BPF_LDX: u16 = 0x01;
const BPF_ST: u16 = 0x02;
const BPF_ALU: u16 = 0x04;
const BPF_JMP: u16 = 0x05;
const BPF_RET: u16 = 0x06;
const BPF_MISC: u16 = 0x07;
const BPF_W: u16 = 0x00;
const BPF_H: u16 = 0x08;
const BPF_B: u16 = 0x10;
const BPF_IMM: u16 = 0x00;
const BPF_ABS: u16 = 0x20;
const BPF_IND: u16 = 0x40;
const BPF_MEM: u16 = 0x60;
const BPF_LEN: u16 = 0x80;
const BPF_ADD: u16 = 0x00;
const BPF_JA: u16 = 0x00;
const BPF_JEQ: u16 = 0x10;
const BPF_JGE: u16 = 0x30;
const BPF_K: u16 = 0x00;
const BPF_X: u16 = 0x08;
const BPF_TAX: u16 = 0x00;
const BPF_TXA: u16 = 0x80;
const M_SUBSYS_BASE: u32 = 0;
const ACCEPT: u32 = u32::MAX;
const DROP: u32 = 0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Insn {
code: u16,
jt: u8,
jf: u8,
k: u32,
}
impl Insn {
const fn new(code: u16, jt: u8, jf: u8, k: u32) -> Self {
Self { code, jt, jf, k }
}
fn to_bytes(self) -> [u8; SOCK_FILTER_SIZE] {
let mut out = [0u8; SOCK_FILTER_SIZE];
out[0..2].copy_from_slice(&self.code.to_ne_bytes());
out[2] = self.jt;
out[3] = self.jf;
out[4..8].copy_from_slice(&self.k.to_ne_bytes());
out
}
}
#[derive(Debug, Clone)]
pub struct CompiledUeventFilter {
program: Vec<u8>,
exact: bool,
}
impl CompiledUeventFilter {
pub fn program(&self) -> &[u8] {
&self.program
}
pub fn len(&self) -> usize {
self.program.len() / SOCK_FILTER_SIZE
}
pub fn is_empty(&self) -> bool {
self.program.is_empty()
}
pub fn is_exact(&self) -> bool {
self.exact
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UeventFilter {
actions: Vec<String>,
subsystems: Vec<String>,
devtypes: Vec<String>,
env: Vec<(String, String)>,
}
impl UeventFilter {
pub fn new() -> Self {
Self::default()
}
pub fn action(mut self, action: impl Into<String>) -> Self {
self.actions.push(action.into());
self
}
pub fn subsystem(mut self, subsystem: impl Into<String>) -> Self {
self.subsystems.push(subsystem.into());
self
}
pub fn devtype(mut self, devtype: impl Into<String>) -> Self {
self.devtypes.push(devtype.into());
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.push((key.into(), value.into()));
self
}
pub fn build(self) -> Self {
self
}
pub fn is_unconstrained(&self) -> bool {
self.actions.is_empty()
&& self.subsystems.is_empty()
&& self.devtypes.is_empty()
&& self.env.is_empty()
}
pub fn matches(&self, event: &Uevent) -> bool {
if !self.actions.is_empty() && !self.actions.contains(&event.action) {
return false;
}
if !self.subsystems.is_empty() && !self.subsystems.contains(&event.subsystem) {
return false;
}
if !self.devtypes.is_empty() {
let Some(devtype) = event.devtype() else {
return false;
};
if !self.devtypes.iter().any(|d| d == devtype) {
return false;
}
}
for (key, want) in &self.env {
if event.env.get(key).map(String::as_str) != Some(want.as_str()) {
return false;
}
}
true
}
pub fn compile(&self) -> CompiledUeventFilter {
let action_literals: Vec<Vec<u8>> = self
.actions
.iter()
.map(|a| {
let mut l = a.clone().into_bytes();
l.push(b'@');
l
})
.collect();
let subsystem_literals: Vec<Vec<u8>> = self
.subsystems
.iter()
.map(|s| {
let mut l = b"SUBSYSTEM=".to_vec();
l.extend_from_slice(s.as_bytes());
l.push(0);
l
})
.collect();
let lower_actions = !action_literals.is_empty()
&& action_literals
.iter()
.all(|l| literal_is_encodable(l, false));
let lower_subsystems = !subsystem_literals.is_empty()
&& subsystem_literals
.iter()
.all(|l| literal_is_encodable(l, true));
let user_side = !self.devtypes.is_empty()
|| !self.env.is_empty()
|| (!action_literals.is_empty() && !lower_actions)
|| (!subsystem_literals.is_empty() && !lower_subsystems);
if !lower_actions && !lower_subsystems {
return CompiledUeventFilter {
program: Vec::new(),
exact: !user_side,
};
}
let mut prog: Vec<Insn> = Vec::new();
if lower_actions {
emit_action_gate(&mut prog, &action_literals);
}
if lower_subsystems {
emit_subsystem_gate(&mut prog, &subsystem_literals);
}
prog.push(ret(ACCEPT));
let mut program = Vec::with_capacity(prog.len() * SOCK_FILTER_SIZE);
for insn in &prog {
program.extend_from_slice(&insn.to_bytes());
}
CompiledUeventFilter {
program,
exact: !user_side,
}
}
}
const fn ret(k: u32) -> Insn {
Insn::new(BPF_RET | BPF_K, 0, 0, k)
}
const fn ja(k: u32) -> Insn {
Insn::new(BPF_JMP | BPF_JA, 0, 0, k)
}
const fn ld_len() -> Insn {
Insn::new(BPF_LD | BPF_W | BPF_LEN, 0, 0, 0)
}
const fn ldx_imm(k: u32) -> Insn {
Insn::new(BPF_LDX | BPF_W | BPF_IMM, 0, 0, k)
}
const fn ld_mem(slot: u32) -> Insn {
Insn::new(BPF_LD | BPF_W | BPF_MEM, 0, 0, slot)
}
const fn ldx_mem(slot: u32) -> Insn {
Insn::new(BPF_LDX | BPF_W | BPF_MEM, 0, 0, slot)
}
const fn st_mem(slot: u32) -> Insn {
Insn::new(BPF_ST, 0, 0, slot)
}
const fn tax() -> Insn {
Insn::new(BPF_MISC | BPF_TAX, 0, 0, 0)
}
const fn txa() -> Insn {
Insn::new(BPF_MISC | BPF_TXA, 0, 0, 0)
}
const fn add_imm(k: u32) -> Insn {
Insn::new(BPF_ALU | BPF_ADD | BPF_K, 0, 0, k)
}
const fn add_x() -> Insn {
Insn::new(BPF_ALU | BPF_ADD | BPF_X, 0, 0, 0)
}
const fn jge_imm(k: u32, jt: u8, jf: u8) -> Insn {
Insn::new(BPF_JMP | BPF_JGE | BPF_K, jt, jf, k)
}
const fn jge_x(jt: u8, jf: u8) -> Insn {
Insn::new(BPF_JMP | BPF_JGE | BPF_X, jt, jf, 0)
}
const fn jeq_imm(k: u32, jt: u8, jf: u8) -> Insn {
Insn::new(BPF_JMP | BPF_JEQ | BPF_K, jt, jf, k)
}
const fn ld_abs(width: u16, offset: u32) -> Insn {
Insn::new(BPF_LD | width | BPF_ABS, 0, 0, offset)
}
const fn ld_ind(width: u16, offset: u32) -> Insn {
Insn::new(BPF_LD | width | BPF_IND, 0, 0, offset)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Addressing {
Absolute(u32),
Indirect { slot: u32 },
}
const MAX_LITERAL: usize = 100;
fn chunks(literal: &[u8]) -> Vec<(u16, u32, u32)> {
let mut out = Vec::new();
let mut i = 0usize;
while i < literal.len() {
let remaining = literal.len() - i;
if remaining >= 4 {
let v = u32::from_be_bytes([literal[i], literal[i + 1], literal[i + 2], literal[i + 3]]);
out.push((BPF_W, i as u32, v));
i += 4;
} else if remaining >= 2 {
let v = u16::from_be_bytes([literal[i], literal[i + 1]]) as u32;
out.push((BPF_H, i as u32, v));
i += 2;
} else {
out.push((BPF_B, i as u32, literal[i] as u32));
i += 1;
}
}
out
}
fn emit_literal_alternative(
prog: &mut Vec<Insn>,
literal: &[u8],
addressing: Addressing,
pending_success: &mut Vec<usize>,
) {
let chunks = chunks(literal);
let block_len = chunks.len() * 2 + 1;
match addressing {
Addressing::Absolute(base) => {
prog.push(ld_len());
prog.push(jge_imm(base + literal.len() as u32, 0, block_len as u8));
}
Addressing::Indirect { slot } => {
prog.push(ld_mem(slot));
prog.push(add_imm(literal.len() as u32));
prog.push(tax());
prog.push(ld_len());
prog.push(jge_x(0, (block_len + 1) as u8));
prog.push(ldx_mem(slot));
}
}
let base_off = match addressing {
Addressing::Absolute(base) => base,
Addressing::Indirect { .. } => 0,
};
for (i, (width, offset, value)) in chunks.iter().copied().enumerate() {
prog.push(match addressing {
Addressing::Absolute(_) => ld_abs(width, base_off + offset),
Addressing::Indirect { .. } => ld_ind(width, offset),
});
let remaining_after = block_len - (i * 2 + 2);
prog.push(jeq_imm(value, 0, remaining_after as u8));
}
pending_success.push(prog.len());
prog.push(ja(0));
}
fn literal_is_encodable(literal: &[u8], indirect: bool) -> bool {
if literal.is_empty() || literal.len() > MAX_LITERAL {
return false;
}
let block_len = chunks(literal).len() * 2 + 1;
let widest = if indirect { block_len + 1 } else { block_len };
widest <= u8::MAX as usize
}
fn patch_success_jumps(prog: &mut [Insn], pending: &[usize], target: usize) {
for &idx in pending {
prog[idx].k = (target - idx - 1) as u32;
}
}
fn emit_action_gate(prog: &mut Vec<Insn>, literals: &[Vec<u8>]) {
let mut pending = Vec::new();
for literal in literals {
emit_literal_alternative(prog, literal, Addressing::Absolute(0), &mut pending);
}
prog.push(ret(DROP));
let exit = prog.len();
patch_success_jumps(prog, &pending, exit);
}
fn emit_subsystem_gate(prog: &mut Vec<Insn>, literals: &[Vec<u8>]) {
prog.push(ldx_imm(0));
let scan_start = prog.len();
for _ in 0..SCAN_LIMIT {
prog.push(ld_ind(BPF_B, 0)); prog.push(jeq_imm(0, 0, 1)); prog.push(ja(0)); prog.push(txa());
prog.push(add_imm(1));
prog.push(tax());
}
prog.push(ret(ACCEPT));
let found = prog.len();
for step in 0..SCAN_LIMIT as usize {
let ja_idx = scan_start + step * SCAN_STEP_INSNS + 2;
prog[ja_idx].k = (found - ja_idx - 1) as u32;
}
prog.push(txa()); prog.push(add_x()); prog.push(add_imm(SUBSYSTEM_KEY_BIAS)); prog.push(st_mem(M_SUBSYS_BASE)); prog.push(tax());
let mut pending = Vec::new();
for literal in literals {
emit_literal_alternative(
prog,
literal,
Addressing::Indirect {
slot: M_SUBSYS_BASE,
},
&mut pending,
);
}
prog.push(ret(DROP));
let exit = prog.len();
patch_success_jumps(prog, &pending, exit);
}
#[cfg(test)]
mod tests {
use super::*;
fn frame(action: &str, devpath: &str, subsystem: &str, extra: &[&str]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(format!("{action}@{devpath}").as_bytes());
out.push(0);
for kv in [
format!("ACTION={action}"),
format!("DEVPATH={devpath}"),
format!("SUBSYSTEM={subsystem}"),
]
.iter()
.chain(extra.iter().map(|s| s.to_string()).collect::<Vec<_>>().iter())
{
out.extend_from_slice(kv.as_bytes());
out.push(0);
}
out
}
#[test]
fn subsystem_key_sits_at_twice_the_header_nul_plus_17() {
for (action, devpath, subsystem) in [
("add", "/devices/pci0000:00/0000:00:14.0/usb1/1-1", "usb"),
("remove", "/devices/virtual/block/loop0", "block"),
("bind", "/devices/virtual/net/veth0", "net"),
("change", "/d", "x"),
] {
let f = frame(action, devpath, subsystem, &[]);
let h = f.iter().position(|&b| b == 0).unwrap() as u32;
let base = (2 * h + SUBSYSTEM_KEY_BIAS) as usize;
assert_eq!(
&f[base..base + 10],
b"SUBSYSTEM=",
"action={action} devpath={devpath}"
);
}
}
fn run(program: &[u8], frame: &[u8]) -> u32 {
let insns: Vec<Insn> = program
.as_chunks::<SOCK_FILTER_SIZE>()
.0
.iter()
.map(|c| Insn {
code: u16::from_ne_bytes([c[0], c[1]]),
jt: c[2],
jf: c[3],
k: u32::from_ne_bytes([c[4], c[5], c[6], c[7]]),
})
.collect();
let load = |off: u32, width: u16| -> Option<u32> {
let off = off as usize;
let n = match width {
BPF_W => 4,
BPF_H => 2,
_ => 1,
};
if off.checked_add(n)? > frame.len() {
return None;
}
Some(match n {
4 => u32::from_be_bytes([
frame[off],
frame[off + 1],
frame[off + 2],
frame[off + 3],
]),
2 => u16::from_be_bytes([frame[off], frame[off + 1]]) as u32,
_ => frame[off] as u32,
})
};
let (mut a, mut x) = (0u32, 0u32);
let mut mem = [0u32; 16];
let mut pc = 0usize;
let mut steps = 0usize;
loop {
steps += 1;
assert!(steps < 1_000_000, "interpreter did not terminate");
let i = insns[pc];
pc += 1;
let class = i.code & 0x07;
match class {
BPF_RET => return i.k,
BPF_LD => {
let mode = i.code & 0xe0;
let width = i.code & 0x18;
let v = match mode {
BPF_LEN => Some(frame.len() as u32),
BPF_IMM => Some(i.k),
BPF_MEM => Some(mem[i.k as usize]),
BPF_ABS => load(i.k, width),
BPF_IND => load(x.wrapping_add(i.k), width),
_ => unreachable!("unsupported ld mode {mode:#x}"),
};
match v {
Some(v) => a = v,
None => return DROP,
}
}
BPF_LDX => {
x = match i.code & 0xe0 {
BPF_IMM => i.k,
BPF_MEM => mem[i.k as usize],
mode => unreachable!("unsupported ldx mode {mode:#x}"),
}
}
BPF_ST => mem[i.k as usize] = a,
BPF_ALU => {
let operand = if i.code & BPF_X != 0 { x } else { i.k };
match i.code & 0xf0 {
BPF_ADD => a = a.wrapping_add(operand),
op => unreachable!("unsupported alu op {op:#x}"),
}
}
BPF_JMP => {
let op = i.code & 0xf0;
if op == BPF_JA {
pc += i.k as usize;
continue;
}
let operand = if i.code & BPF_X != 0 { x } else { i.k };
let taken = match op {
BPF_JEQ => a == operand,
BPF_JGE => a >= operand,
_ => unreachable!("unsupported jmp op {op:#x}"),
};
pc += if taken { i.jt as usize } else { i.jf as usize };
}
BPF_MISC => {
if i.code & 0xf8 == BPF_TXA {
a = x;
} else {
x = a;
}
}
_ => unreachable!("unsupported class {class:#x}"),
}
}
}
fn accepts(filter: &UeventFilter, f: &[u8]) -> bool {
run(filter.compile().program(), f) != DROP
}
#[test]
#[cfg(target_endian = "little")]
fn program_bytes_match_the_sock_filter_layout() {
let program = UeventFilter::new().action("add").build().compile();
#[rustfmt::skip]
let expected: [u8; 7 * SOCK_FILTER_SIZE] = [
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x35, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x15, 0x00, 0x00, 0x01, 0x40, 0x64, 0x64, 0x61,
0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
];
assert_eq!(program.len(), 7);
assert_eq!(program.program(), &expected[..]);
}
#[test]
fn chunking_covers_every_remainder_without_overreading() {
assert_eq!(chunks(b"add@"), vec![(BPF_W, 0, 0x6164_6440)]);
assert_eq!(chunks(b"a"), vec![(BPF_B, 0, 0x61)]);
assert_eq!(chunks(b"ab"), vec![(BPF_H, 0, 0x6162)]);
assert_eq!(chunks(b"abc"), vec![(BPF_H, 0, 0x6162), (BPF_B, 2, 0x63)]);
assert_eq!(
chunks(b"abcdef"),
vec![(BPF_W, 0, 0x6162_6364), (BPF_H, 4, 0x6566)]
);
for len in 1..40usize {
let literal = vec![b'x'; len];
let last = *chunks(&literal).last().unwrap();
let width = match last.0 {
BPF_W => 4,
BPF_H => 2,
_ => 1,
};
assert_eq!(last.1 as usize + width, len, "literal of {len} bytes");
}
}
#[test]
fn is_unconstrained_tracks_every_criterion() {
assert!(UeventFilter::new().is_unconstrained());
assert!(!UeventFilter::new().action("add").is_unconstrained());
assert!(!UeventFilter::new().subsystem("net").is_unconstrained());
assert!(!UeventFilter::new().devtype("disk").is_unconstrained());
assert!(!UeventFilter::new().env("IFINDEX", "3").is_unconstrained());
}
#[test]
fn an_unconstrained_filter_matches_every_event() {
let filter = UeventFilter::new().build();
for action in ["add", "remove", "bind"] {
for subsystem in ["net", "usb"] {
let event =
Uevent::parse(&frame(action, "/devices/x", subsystem, &[])).unwrap();
assert!(filter.matches(&event));
}
}
}
#[test]
fn unconstrained_filter_compiles_to_nothing() {
let compiled = UeventFilter::new().build().compile();
assert!(compiled.is_empty());
assert!(compiled.is_exact());
}
#[test]
fn userspace_only_filter_compiles_to_nothing_and_is_inexact() {
let compiled = UeventFilter::new().devtype("disk").build().compile();
assert!(compiled.is_empty());
assert!(!compiled.is_exact());
}
#[test]
fn program_is_a_whole_number_of_instructions() {
let compiled = UeventFilter::new().subsystem("net").action("add").compile();
assert_eq!(compiled.program().len() % SOCK_FILTER_SIZE, 0);
assert_eq!(compiled.len(), compiled.program().len() / SOCK_FILTER_SIZE);
}
#[test]
fn instruction_count_stays_under_bpf_maxinsns() {
const BPF_MAXINSNS: usize = 4096;
let compiled = UeventFilter::new()
.subsystem("net")
.subsystem("block")
.subsystem("power_supply")
.action("add")
.action("remove")
.action("change")
.compile();
assert!(
compiled.len() < BPF_MAXINSNS,
"{} instructions",
compiled.len()
);
}
#[test]
fn action_gate_accepts_only_named_actions() {
let filter = UeventFilter::new().action("add").action("remove").build();
assert!(accepts(&filter, &frame("add", "/devices/virtual/net/veth0", "net", &[])));
assert!(accepts(&filter, &frame("remove", "/devices/virtual/net/veth0", "net", &[])));
assert!(!accepts(&filter, &frame("change", "/devices/virtual/net/veth0", "net", &[])));
assert!(!accepts(&filter, &frame("bind", "/devices/virtual/net/veth0", "net", &[])));
}
#[test]
fn action_gate_does_not_alias_on_prefixes() {
let filter = UeventFilter::new().action("bind").build();
assert!(accepts(&filter, &frame("bind", "/devices/x", "net", &[])));
assert!(!accepts(&filter, &frame("unbind", "/devices/x", "net", &[])));
}
#[test]
fn subsystem_gate_accepts_only_named_subsystems() {
let filter = UeventFilter::new().subsystem("net").build();
assert!(accepts(&filter, &frame("add", "/devices/virtual/net/veth0", "net", &[])));
assert!(!accepts(&filter, &frame("add", "/devices/virtual/block/loop0", "block", &[])));
assert!(!accepts(
&filter,
&frame("add", "/devices/pci0000:00/0000:00:14.0/usb1/1-1", "usb", &[])
));
}
#[test]
fn subsystem_gate_requires_the_whole_value() {
let filter = UeventFilter::new().subsystem("net").build();
assert!(!accepts(&filter, &frame("add", "/devices/x", "net_bogus", &[])));
let filter = UeventFilter::new().subsystem("net_bogus").build();
assert!(!accepts(&filter, &frame("add", "/devices/x", "net", &[])));
assert!(accepts(&filter, &frame("add", "/devices/x", "net_bogus", &[])));
}
#[test]
fn subsystem_gate_handles_every_literal_tail_length() {
for value in ["a", "ab", "abc", "abcd", "abcde", "abcdef"] {
let filter = UeventFilter::new().subsystem(value).build();
assert!(
accepts(&filter, &frame("add", "/devices/x", value, &[])),
"value={value}"
);
assert!(
!accepts(&filter, &frame("add", "/devices/x", "other", &[])),
"value={value}"
);
}
}
#[test]
fn combined_gates_are_anded() {
let filter = UeventFilter::new().action("add").subsystem("net").build();
assert!(accepts(&filter, &frame("add", "/devices/x", "net", &[])));
assert!(!accepts(&filter, &frame("remove", "/devices/x", "net", &[])));
assert!(!accepts(&filter, &frame("add", "/devices/x", "block", &[])));
}
#[test]
fn overlong_devpath_falls_through_to_accept() {
let long = format!("/devices/{}", "x".repeat(SCAN_LIMIT as usize + 64));
let filter = UeventFilter::new().subsystem("net").build();
assert!(accepts(&filter, &frame("add", &long, "block", &[])));
}
#[test]
fn truncation_never_drops_a_frame_that_matches() {
let filter = UeventFilter::new()
.action("remove")
.action("add")
.subsystem("net")
.build();
let full = frame("remove", "/devices/virtual/net/veth0", "net", &["SEQNUM=9"]);
let boundaries = (0..=full.len())
.filter(|&n| n == 0 || n == full.len() || full[n - 1] == 0);
for len in boundaries {
let truncated = &full[..len];
let Some(event) = Uevent::parse(truncated) else {
continue;
};
if filter.matches(&event) {
assert!(
accepts(&filter, truncated),
"dropped a matching {len}-byte truncation"
);
}
}
}
#[test]
fn unterminated_subsystem_value_is_dropped() {
let filter = UeventFilter::new().subsystem("net").build();
let full = frame("remove", "/devices/virtual/net/veth0", "net", &[]);
let cut = &full[..full.len() - 1];
let event = Uevent::parse(cut).unwrap();
assert_eq!(event.subsystem, "net");
assert!(filter.matches(&event));
assert!(!accepts(&filter, cut));
}
#[test]
fn unencodable_literal_falls_back_to_userspace() {
let huge = "s".repeat(MAX_LITERAL + 1);
let compiled = UeventFilter::new().subsystem(&huge).build().compile();
assert!(compiled.is_empty());
assert!(!compiled.is_exact());
let compiled = UeventFilter::new()
.action("add")
.subsystem(&huge)
.build()
.compile();
assert!(!compiled.is_empty());
assert!(!compiled.is_exact());
}
#[test]
fn a_long_alternative_does_not_shadow_a_short_one() {
let filter = UeventFilter::new()
.subsystem("power_supply")
.subsystem("net")
.build();
assert!(accepts(&filter, &frame("add", "/d", "net", &[])));
assert!(accepts(&filter, &frame("add", "/d", "power_supply", &[])));
assert!(!accepts(&filter, &frame("add", "/d", "block", &[])));
let filter = UeventFilter::new()
.subsystem("net")
.subsystem("power_supply")
.build();
assert!(accepts(&filter, &frame("add", "/d", "net", &[])));
assert!(accepts(&filter, &frame("add", "/d", "power_supply", &[])));
assert!(!accepts(&filter, &frame("add", "/d", "block", &[])));
}
#[test]
fn matches_is_the_authority_for_userspace_criteria() {
let filter = UeventFilter::new()
.subsystem("block")
.devtype("partition")
.build();
let disk = Uevent::parse(&frame(
"add",
"/devices/virtual/block/loop0",
"block",
&["DEVTYPE=disk"],
))
.unwrap();
let part = Uevent::parse(&frame(
"add",
"/devices/virtual/block/loop0p1",
"block",
&["DEVTYPE=partition"],
))
.unwrap();
assert!(accepts(&filter, &frame("add", "/devices/virtual/block/loop0", "block", &["DEVTYPE=disk"])));
assert!(!filter.matches(&disk));
assert!(filter.matches(&part));
}
#[test]
fn matches_ands_distinct_criteria_and_ors_repeats() {
let filter = UeventFilter::new()
.action("add")
.action("change")
.subsystem("net")
.env("IFINDEX", "3")
.build();
let ev = |action: &str, sub: &str, ifindex: &str| {
Uevent::parse(&frame(
action,
"/devices/virtual/net/veth0",
sub,
&[&format!("IFINDEX={ifindex}")],
))
.unwrap()
};
assert!(filter.matches(&ev("add", "net", "3")));
assert!(filter.matches(&ev("change", "net", "3")));
assert!(!filter.matches(&ev("remove", "net", "3")));
assert!(!filter.matches(&ev("add", "block", "3")));
assert!(!filter.matches(&ev("add", "net", "4")));
}
#[test]
fn every_kernel_accepted_frame_that_matches_is_accepted() {
let filter = UeventFilter::new()
.action("add")
.action("remove")
.subsystem("net")
.build();
for action in ["add", "remove", "change", "bind", "unbind"] {
for subsystem in ["net", "block", "usb", "n", "network"] {
for devpath in ["/d", "/devices/virtual/net/veth0", "/devices/pci0000:00/x/y/z"] {
let f = frame(action, devpath, subsystem, &["SEQNUM=1"]);
let event = Uevent::parse(&f).unwrap();
if filter.matches(&event) {
assert!(
accepts(&filter, &f),
"dropped a matching frame: {action}@{devpath} {subsystem}"
);
}
}
}
}
}
}