pub const REGION_UNKNOWN: u8 = 0;
pub const REGION_STACK: u8 = 1;
pub const REGION_DATA: u8 = 2;
const NUM_REGS: usize = 11;
const EBPF_CLS_MASK: u8 = 0x07;
const EBPF_CLS_LD: u8 = 0x00;
const EBPF_CLS_LDX: u8 = 0x01;
const EBPF_CLS_ST: u8 = 0x02;
const EBPF_CLS_STX: u8 = 0x03;
const EBPF_CLS_ALU: u8 = 0x04;
const EBPF_CLS_JMP: u8 = 0x05;
const EBPF_CLS_JMP32: u8 = 0x06;
const EBPF_CLS_ALU64: u8 = 0x07;
const EBPF_SRC_REG: u8 = 0x08;
const EBPF_ALU_OP_MASK: u8 = 0xf0;
const EBPF_ALU_OP_ADD: u8 = 0x00;
const EBPF_ALU_OP_SUB: u8 = 0x10;
const EBPF_ALU_OP_MOV: u8 = 0xb0;
const EBPF_OP_LDDW: u8 = EBPF_CLS_LD | 0x18; const EBPF_OP_JA: u8 = EBPF_CLS_JMP; const EBPF_OP_JA32: u8 = EBPF_CLS_JMP32;
const EBPF_OP_CALL: u8 = EBPF_CLS_JMP | 0x80; const EBPF_OP_EXIT: u8 = EBPF_CLS_JMP | 0x90;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum RegKind {
Uninit,
Stack,
Data,
Scalar,
Unknown,
}
impl RegKind {
fn meet(self, other: RegKind) -> RegKind {
match (self, other) {
(a, b) if a == b => a,
(RegKind::Uninit, b) => b,
(a, RegKind::Uninit) => a,
_ => RegKind::Unknown,
}
}
}
const R10: usize = 10;
#[derive(Clone, PartialEq, Eq)]
struct State {
regs: [RegKind; NUM_REGS],
slots: std::collections::BTreeMap<i16, RegKind>,
}
impl State {
fn top() -> State {
State {
regs: [RegKind::Uninit; NUM_REGS],
slots: std::collections::BTreeMap::new(),
}
}
fn meet_from(&mut self, other: &State) -> bool {
let mut changed = false;
for r in 0..NUM_REGS {
let merged = self.regs[r].meet(other.regs[r]);
if merged != self.regs[r] {
self.regs[r] = merged;
changed = true;
}
}
for (&off, &k) in &other.slots {
let cur = self.slots.get(&off).copied().unwrap_or(RegKind::Uninit);
let merged = cur.meet(k);
if merged != cur {
self.slots.insert(off, merged);
changed = true;
}
}
changed
}
fn invalidate_slots(&mut self) {
for v in self.slots.values_mut() {
*v = RegKind::Unknown;
}
}
}
#[derive(Clone, Copy)]
struct Inst {
opcode: u8,
dst: usize,
src: usize,
offset: i16,
imm: i32,
}
fn decode(slot: &[u8]) -> Inst {
Inst {
opcode: slot[0],
dst: (slot[1] & 0x0f) as usize,
src: (slot[1] >> 4) as usize,
offset: i16::from_le_bytes([slot[2], slot[3]]),
imm: i32::from_le_bytes([slot[4], slot[5], slot[6], slot[7]]),
}
}
pub struct RegionAnalysis {
pub hints: Vec<u8>,
pub unresolved: Vec<usize>,
}
pub fn analyze(code: &[u8], data_lo: u64, data_hi: u64) -> RegionAnalysis {
let num_slots = code.len() / 8;
let mut hints = vec![REGION_UNKNOWN; num_slots];
let mut unresolved = Vec::new();
if num_slots == 0 {
return RegionAnalysis { hints, unresolved };
}
let mut states: Vec<State> = (0..num_slots).map(|_| State::top()).collect();
states[0].regs[1] = RegKind::Stack;
states[0].regs[R10] = RegKind::Stack;
let mut worklist: Vec<usize> = vec![0];
let mut on_list = vec![false; num_slots];
on_list[0] = true;
while let Some(pc) = worklist.pop() {
on_list[pc] = false;
let inst = decode(&code[pc * 8..pc * 8 + 8]);
let lddw_addr = lddw_full_imm(code, pc, &inst);
let out = transfer(&states[pc], &inst, lddw_addr, data_lo, data_hi);
for succ in successors(pc, &inst, num_slots) {
if states[succ].meet_from(&out) && !on_list[succ] {
on_list[succ] = true;
worklist.push(succ);
}
}
}
for pc in 0..num_slots {
let inst = decode(&code[pc * 8..pc * 8 + 8]);
let cls = inst.opcode & EBPF_CLS_MASK;
let base = match cls {
EBPF_CLS_LDX => inst.src, EBPF_CLS_ST | EBPF_CLS_STX => inst.dst, _ => continue,
};
let region = match states[pc].regs[base] {
RegKind::Stack => REGION_STACK,
RegKind::Data => REGION_DATA,
_ => REGION_UNKNOWN,
};
if cls == EBPF_CLS_LDX {
hints[pc] = region;
}
if region == REGION_UNKNOWN {
unresolved.push(pc);
}
}
RegionAnalysis { hints, unresolved }
}
fn lddw_full_imm(code: &[u8], pc: usize, inst: &Inst) -> u64 {
if inst.opcode != EBPF_OP_LDDW || (pc + 2) * 8 > code.len() {
return 0;
}
let hi = decode(&code[(pc + 1) * 8..(pc + 1) * 8 + 8]).imm;
(inst.imm as u32 as u64) | ((hi as u32 as u64) << 32)
}
fn successors(pc: usize, inst: &Inst, num_slots: usize) -> Vec<usize> {
let fallthrough = if inst.opcode == EBPF_OP_LDDW {
pc + 2
} else {
pc + 1
};
let cls = inst.opcode & EBPF_CLS_MASK;
let mut out = Vec::new();
let mut push = |s: usize| {
if s < num_slots {
out.push(s);
}
};
if cls == EBPF_CLS_JMP || cls == EBPF_CLS_JMP32 {
if inst.opcode == EBPF_OP_EXIT {
return out;
}
if inst.opcode == EBPF_OP_CALL {
match inst.src {
0 => push(fallthrough),
1 => {
push(fallthrough);
push((pc as i64 + 1 + inst.imm as i64) as usize);
}
_ => {}
}
return out;
}
let target = if inst.opcode == EBPF_OP_JA32 {
pc as i64 + 1 + inst.imm as i64
} else {
pc as i64 + 1 + inst.offset as i64
} as usize;
push(target);
if inst.opcode != EBPF_OP_JA && inst.opcode != EBPF_OP_JA32 {
push(fallthrough); }
return out;
}
push(fallthrough);
out
}
fn transfer(in_state: &State, inst: &Inst, lddw_addr: u64, data_lo: u64, data_hi: u64) -> State {
let mut s = in_state.clone();
let cls = inst.opcode & EBPF_CLS_MASK;
match cls {
EBPF_CLS_LD => {
if inst.opcode == EBPF_OP_LDDW {
s.regs[inst.dst] = if lddw_addr >= data_lo && lddw_addr < data_hi {
RegKind::Data
} else {
RegKind::Scalar
};
} else {
s.regs[inst.dst] = RegKind::Unknown;
}
}
EBPF_CLS_LDX => {
s.regs[inst.dst] = if inst.src == R10 {
match s.slots.get(&inst.offset).copied() {
Some(k @ (RegKind::Stack | RegKind::Data)) => k,
_ => RegKind::Scalar,
}
} else {
RegKind::Scalar
};
}
EBPF_CLS_ST | EBPF_CLS_STX => {
let is_atomic = cls == EBPF_CLS_STX && (inst.opcode & 0xe0) == 0xc0;
let value = if cls == EBPF_CLS_ST {
RegKind::Scalar
} else {
s.regs[inst.src]
};
if inst.dst == R10 {
let stored = if is_atomic {
RegKind::Unknown
} else if value == RegKind::Uninit {
RegKind::Unknown
} else {
value
};
s.slots.insert(inst.offset, stored);
} else if s.regs[inst.dst] != RegKind::Data {
s.invalidate_slots();
}
if is_atomic {
s.regs[inst.src] = RegKind::Unknown;
}
}
EBPF_CLS_ALU => {
s.regs[inst.dst] = RegKind::Scalar;
}
EBPF_CLS_ALU64 => {
let op = inst.opcode & EBPF_ALU_OP_MASK;
let is_reg = inst.opcode & EBPF_SRC_REG != 0;
match op {
EBPF_ALU_OP_MOV => {
s.regs[inst.dst] = if is_reg {
match s.regs[inst.src] {
RegKind::Uninit => RegKind::Unknown,
k => k,
}
} else {
RegKind::Scalar
};
}
EBPF_ALU_OP_ADD => {
s.regs[inst.dst] = if is_reg {
add_kinds(s.regs[inst.dst], s.regs[inst.src])
} else {
preserve_with_imm(s.regs[inst.dst])
};
}
EBPF_ALU_OP_SUB => {
s.regs[inst.dst] = if is_reg {
sub_kinds(s.regs[inst.dst], s.regs[inst.src])
} else {
preserve_with_imm(s.regs[inst.dst])
};
}
_ => s.regs[inst.dst] = RegKind::Scalar,
}
}
EBPF_CLS_JMP | EBPF_CLS_JMP32 => {
if inst.opcode == EBPF_OP_CALL {
s.regs[0] = RegKind::Scalar;
for r in 1..=5 {
s.regs[r] = RegKind::Unknown;
}
s.invalidate_slots();
}
}
_ => {}
}
s
}
fn add_kinds(a: RegKind, b: RegKind) -> RegKind {
match (a, b) {
(RegKind::Stack, RegKind::Scalar) | (RegKind::Scalar, RegKind::Stack) => RegKind::Stack,
(RegKind::Data, RegKind::Scalar) | (RegKind::Scalar, RegKind::Data) => RegKind::Data,
(RegKind::Scalar, RegKind::Scalar) => RegKind::Scalar,
_ => RegKind::Unknown,
}
}
fn sub_kinds(a: RegKind, b: RegKind) -> RegKind {
match (a, b) {
(RegKind::Stack, RegKind::Scalar) => RegKind::Stack,
(RegKind::Data, RegKind::Scalar) => RegKind::Data,
(RegKind::Stack, RegKind::Stack) | (RegKind::Data, RegKind::Data) => RegKind::Scalar,
(RegKind::Scalar, RegKind::Scalar) => RegKind::Scalar,
_ => RegKind::Unknown,
}
}
fn preserve_with_imm(a: RegKind) -> RegKind {
match a {
RegKind::Stack => RegKind::Stack,
RegKind::Data => RegKind::Data,
RegKind::Scalar => RegKind::Scalar,
_ => RegKind::Unknown,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn slot(opcode: u8, dst: u8, src: u8, offset: i16, imm: i32) -> [u8; 8] {
let mut s = [0u8; 8];
s[0] = opcode;
s[1] = (dst & 0x0f) | (src << 4);
s[2..4].copy_from_slice(&offset.to_le_bytes());
s[4..8].copy_from_slice(&imm.to_le_bytes());
s
}
fn flatten(slots: &[[u8; 8]]) -> Vec<u8> {
slots.iter().flatten().copied().collect()
}
const DATA_LO: u64 = 0x10000;
const DATA_HI: u64 = 0x20000;
#[test]
fn stack_load_via_r10_is_routed_to_stack() {
let code = flatten(&[
slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 2, 10, 0, 0),
slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 2, 0, 0, -8),
slot(EBPF_CLS_LDX | 0x18, 0, 2, 0, 0), slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let hints = analyze(&code, DATA_LO, DATA_HI).hints;
assert_eq!(hints[2], REGION_STACK);
}
#[test]
fn ctx_load_via_r1_is_routed_to_stack() {
let code = flatten(&[
slot(EBPF_CLS_LDX | 0x18, 0, 1, 0, 0),
slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let hints = analyze(&code, DATA_LO, DATA_HI).hints;
assert_eq!(hints[0], REGION_STACK);
}
#[test]
fn data_pointer_load_is_routed_to_data() {
let addr = (DATA_LO + 0x40) as i32;
let code = flatten(&[
slot(EBPF_OP_LDDW, 1, 0, 0, addr),
slot(0, 0, 0, 0, 0), slot(EBPF_CLS_LDX | 0x10, 0, 1, 0, 0), slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let hints = analyze(&code, DATA_LO, DATA_HI).hints;
assert_eq!(hints[2], REGION_DATA);
}
#[test]
fn loaded_pointer_is_unknown() {
let code = flatten(&[
slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 2, 10, 0, 0),
slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 2, 0, 0, -8),
slot(EBPF_CLS_LDX | 0x18, 2, 2, 0, 0),
slot(EBPF_CLS_LDX | 0x18, 0, 2, 0, 0),
slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let hints = analyze(&code, DATA_LO, DATA_HI).hints;
assert_eq!(hints[3], REGION_UNKNOWN);
}
#[test]
fn ambiguous_join_is_unknown() {
let code = flatten(&[
slot(EBPF_CLS_JMP | 0x10, 1, 0, 3, 0), slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 2, 10, 0, 0),
slot(EBPF_OP_JA, 0, 0, 2, 0),
slot(EBPF_OP_EXIT, 0, 0, 0, 0),
slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_MOV, 2, 0, 0, 12345),
slot(EBPF_CLS_LDX | 0x18, 0, 2, 0, 0),
slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let hints = analyze(&code, DATA_LO, DATA_HI).hints;
assert_eq!(hints[5], REGION_UNKNOWN);
}
#[test]
fn spilled_arg_pointer_is_recovered_via_fill() {
let code = flatten(&[
slot(EBPF_CLS_STX | 0x18, 10, 1, -8, 0), slot(EBPF_CLS_LDX | 0x18, 1, 10, -8, 0), slot(EBPF_CLS_LDX | 0x18, 0, 1, 0, 0), slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let result = analyze(&code, DATA_LO, DATA_HI);
assert_eq!(result.hints[2], REGION_STACK);
assert!(result.unresolved.is_empty());
}
#[test]
fn data_pointer_indexed_by_loaded_value_stays_data() {
let addr = DATA_LO as i32;
let code = flatten(&[
slot(EBPF_CLS_LDX | 0x10, 3, 10, -16, 0), slot(EBPF_OP_LDDW, 1, 0, 0, addr), slot(0, 0, 0, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_ADD, 1, 3, 0, 0), slot(EBPF_CLS_LDX | 0x10, 0, 1, 0, 0), slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let result = analyze(&code, DATA_LO, DATA_HI);
assert_eq!(result.hints[4], REGION_DATA);
assert!(result.unresolved.is_empty());
}
#[test]
fn call_return_used_as_index_keeps_pointer_region() {
let code = flatten(&[
slot(EBPF_OP_CALL, 0, 0, 0, 1), slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 6, 10, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_ADD, 6, 0, 0, 0), slot(EBPF_CLS_STX | 0x10, 6, 1, 0, 0), slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let result = analyze(&code, DATA_LO, DATA_HI);
assert!(
result.unresolved.is_empty(),
"unexpected unresolved: {:?}",
result.unresolved
);
}
#[test]
fn stack_byte_read_after_call_indexes_data_pointer() {
let code = flatten(&[
slot(EBPF_CLS_STX | 0x10, 10, 6, -32, 0), slot(EBPF_OP_CALL, 0, 0, 0, 1), slot(EBPF_CLS_LDX | 0x10, 2, 10, -32, 0), slot(EBPF_OP_LDDW, 1, 0, 0, DATA_LO as i32), slot(0, 0, 0, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_ADD, 1, 2, 0, 0), slot(EBPF_CLS_LDX | 0x10, 0, 1, 0, 0), slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let result = analyze(&code, DATA_LO, DATA_HI);
assert_eq!(result.hints[6], REGION_DATA);
assert!(
result.unresolved.is_empty(),
"unexpected unresolved: {:?}",
result.unresolved
);
}
#[test]
fn ja32_target_follows_imm_not_offset() {
let code = flatten(&[
slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 6, 10, 0, 0), slot(EBPF_OP_JA32, 0, 0, 0, 2), slot(EBPF_OP_LDDW, 6, 0, 0, DATA_LO as i32), slot(0, 0, 0, 0, 0), slot(EBPF_CLS_LDX | 0x18, 0, 6, 0, 0), slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let hints = analyze(&code, DATA_LO, DATA_HI).hints;
assert_eq!(hints[4], REGION_STACK);
}
#[test]
fn unresolved_lists_unclassifiable_accesses() {
let code = flatten(&[
slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 2, 10, 0, 0),
slot(EBPF_CLS_LDX | 0x18, 3, 2, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 4, 3, 0, 0),
slot(EBPF_CLS_LDX | 0x18, 0, 4, 0, 0), slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let result = analyze(&code, DATA_LO, DATA_HI);
assert_eq!(result.unresolved, vec![3]);
}
}