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, Hash)]
pub(crate) enum RegKind {
Uninit,
Stack(StackKind),
Data,
Scalar,
Unknown,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub(crate) enum StackKind {
Current(Option<i32>),
Foreign,
}
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::Stack(a), RegKind::Stack(b)) => match (a, b) {
(StackKind::Current(a), StackKind::Current(b)) => {
if a == b {
RegKind::Stack(StackKind::Current(a))
} else {
RegKind::Stack(StackKind::Current(None))
}
}
(StackKind::Foreign, StackKind::Foreign) => RegKind::Stack(StackKind::Foreign),
_ => RegKind::Stack(StackKind::Current(None)),
},
_ => RegKind::Unknown,
}
}
fn region(self) -> u8 {
match self {
RegKind::Stack(_) => REGION_STACK,
RegKind::Data => REGION_DATA,
_ => REGION_UNKNOWN,
}
}
fn is_pointer(self) -> bool {
matches!(self, RegKind::Stack(_) | RegKind::Data)
}
fn is_stack(self) -> bool {
matches!(self, RegKind::Stack(_))
}
fn foreign_for_call(self) -> Self {
match self {
RegKind::Stack(_) => RegKind::Stack(StackKind::Foreign),
other => other,
}
}
fn aliases_current_stack(self) -> bool {
!matches!(self, RegKind::Stack(StackKind::Foreign))
}
}
const R10: usize = 10;
#[derive(Clone, PartialEq, Eq)]
struct State {
regs: [RegKind; NUM_REGS],
slots: std::collections::BTreeMap<i32, 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;
}
}
fn invalidate_stack_write(&mut self, start: Option<i32>, width: usize) {
let Some(start) = start else {
self.invalidate_slots();
return;
};
let Some(end) = start.checked_add(width as i32) else {
self.invalidate_slots();
return;
};
for (&slot_off, value) in self.slots.iter_mut() {
let slot_start = slot_off as i32;
let Some(slot_end) = slot_start.checked_add(8) else {
*value = RegKind::Unknown;
continue;
};
if start < slot_end && slot_start < end {
*value = RegKind::Unknown;
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub(crate) struct PointerSignature {
regs: [RegKind; NUM_REGS],
}
impl PointerSignature {
pub(crate) fn entry() -> Self {
let mut regs = [RegKind::Uninit; NUM_REGS];
regs[1] = RegKind::Stack(StackKind::Current(None));
regs[R10] = RegKind::Stack(StackKind::Current(Some(0)));
Self { regs }
}
fn apply_to_state(self, state: &mut State) {
state.regs = self.regs;
state.regs[R10] = RegKind::Stack(StackKind::Current(Some(0)));
}
fn from_state(state: &State) -> Self {
let mut regs = state.regs;
for (reg, kind) in regs.iter_mut().enumerate() {
if reg != R10 {
*kind = kind.foreign_for_call();
}
}
regs[R10] = RegKind::Stack(StackKind::Current(Some(0)));
Self { regs }
}
#[cfg(any(test, feature = "testing"))]
pub(crate) fn from_regs_for_testing(regs: [RegKind; NUM_REGS]) -> Self {
Self { regs }
}
}
#[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]]),
}
}
fn access_width(opcode: u8) -> usize {
match opcode & 0x18 {
0x00 => 4, 0x08 => 2, 0x10 => 1, 0x18 => 8, _ => 8,
}
}
fn stack_access_start(base: RegKind, offset: i16) -> Option<i32> {
let RegKind::Stack(StackKind::Current(Some(base_off))) = base else {
return None;
};
base_off.checked_add(offset as i32)
}
#[cfg(any(test, feature = "testing"))]
pub struct RegionAnalysis {
#[allow(dead_code)]
pub hints: Vec<u8>,
pub unresolved: Vec<usize>,
}
pub(crate) struct FunctionRegionAnalysis {
pub(crate) hints: Vec<u8>,
pub(crate) unresolved: Vec<usize>,
pub(crate) call_signatures: std::collections::HashMap<usize, PointerSignature>,
}
#[cfg(any(test, feature = "testing"))]
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();
let mut reached = vec![false; num_slots];
states[0].regs[1] = RegKind::Stack(StackKind::Current(None));
states[0].regs[R10] = RegKind::Stack(StackKind::Current(Some(0)));
reached[0] = true;
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) {
let was_reached = reached[succ];
reached[succ] = true;
let changed = states[succ].meet_from(&out);
if (!was_reached || changed) && !on_list[succ] {
on_list[succ] = true;
worklist.push(succ);
}
}
}
for pc in 0..num_slots {
if !reached[pc] {
continue;
}
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 = states[pc].regs[base].region();
if cls == EBPF_CLS_LDX {
hints[pc] = region;
}
if region == REGION_UNKNOWN {
unresolved.push(pc);
}
}
RegionAnalysis { hints, unresolved }
}
pub(crate) fn analyze_function(
code: &[u8],
start_pc: usize,
end_pc: usize,
incoming: PointerSignature,
data_lo: u64,
data_hi: u64,
) -> FunctionRegionAnalysis {
let num_slots = code.len() / 8;
let mut hints = vec![REGION_UNKNOWN; num_slots];
let mut unresolved = Vec::new();
let mut call_signatures = std::collections::HashMap::new();
if start_pc >= end_pc || end_pc > num_slots {
return FunctionRegionAnalysis {
hints,
unresolved,
call_signatures,
};
}
let mut states: Vec<State> = (0..num_slots).map(|_| State::top()).collect();
let mut reached = vec![false; num_slots];
incoming.apply_to_state(&mut states[start_pc]);
reached[start_pc] = true;
let mut worklist = vec![start_pc];
let mut on_list = vec![false; num_slots];
on_list[start_pc] = true;
while let Some(pc) = worklist.pop() {
on_list[pc] = false;
let inst = decode(&code[pc * 8..pc * 8 + 8]);
if inst.opcode == EBPF_OP_CALL && inst.src == 1 {
call_signatures.insert(pc, PointerSignature::from_state(&states[pc]));
}
let lddw_addr = lddw_full_imm(code, pc, &inst);
let out = transfer(&states[pc], &inst, lddw_addr, data_lo, data_hi);
for succ in function_successors(pc, &inst, num_slots, start_pc, end_pc) {
let was_reached = reached[succ];
reached[succ] = true;
let changed = states[succ].meet_from(&out);
if (!was_reached || changed) && !on_list[succ] {
on_list[succ] = true;
worklist.push(succ);
}
}
}
for pc in start_pc..end_pc {
if !reached[pc] {
continue;
}
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 = states[pc].regs[base].region();
if cls == EBPF_CLS_LDX {
hints[pc] = region;
}
if region == REGION_UNKNOWN {
unresolved.push(pc);
}
}
FunctionRegionAnalysis {
hints,
unresolved,
call_signatures,
}
}
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 function_successors(
pc: usize,
inst: &Inst,
num_slots: usize,
start_pc: usize,
end_pc: usize,
) -> Vec<usize> {
let mut succs = successors(pc, inst, num_slots);
if inst.opcode == EBPF_OP_CALL && inst.src == 1 {
let fallthrough = pc + 1;
succs.clear();
if fallthrough < num_slots {
succs.push(fallthrough);
}
}
succs
.into_iter()
.filter(|&succ| succ >= start_pc && succ < end_pc)
.collect()
}
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 as i32)).copied() {
Some(k) if k.is_pointer() => 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]
};
let width = access_width(inst.opcode);
let stack_base = if inst.dst == R10 {
RegKind::Stack(StackKind::Current(Some(0)))
} else {
s.regs[inst.dst]
};
if stack_base.is_stack() {
if stack_base.aliases_current_stack() {
let start = stack_access_start(stack_base, inst.offset);
s.invalidate_stack_write(start, width);
}
let stored = if is_atomic {
RegKind::Unknown
} else if value == RegKind::Uninit {
RegKind::Unknown
} else {
value
};
if !is_atomic && width == 8 {
if let Some(start) = stack_access_start(stack_base, inst.offset) {
s.slots.insert(start, 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 {
add_imm_kind(s.regs[inst.dst], inst.imm)
};
}
EBPF_ALU_OP_SUB => {
s.regs[inst.dst] = if is_reg {
sub_kinds(s.regs[inst.dst], s.regs[inst.src])
} else {
add_imm_kind(s.regs[inst.dst], inst.imm.wrapping_neg())
};
}
_ => 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
}
fn add_kinds(a: RegKind, b: RegKind) -> RegKind {
match (a, b) {
(RegKind::Stack(_), RegKind::Scalar) | (RegKind::Scalar, RegKind::Stack(_)) => match (a, b) {
(RegKind::Stack(StackKind::Foreign), _) | (_, RegKind::Stack(StackKind::Foreign)) => {
RegKind::Stack(StackKind::Foreign)
}
_ => RegKind::Stack(StackKind::Current(None)),
},
(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(StackKind::Foreign), RegKind::Scalar) => RegKind::Stack(StackKind::Foreign),
(RegKind::Stack(_), RegKind::Scalar) => RegKind::Stack(StackKind::Current(None)),
(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 add_imm_kind(a: RegKind, imm: i32) -> RegKind {
match a {
RegKind::Stack(StackKind::Current(Some(off))) => {
RegKind::Stack(StackKind::Current(off.checked_add(imm)))
}
RegKind::Stack(StackKind::Current(None)) => RegKind::Stack(StackKind::Current(None)),
RegKind::Stack(StackKind::Foreign) => RegKind::Stack(StackKind::Foreign),
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 spilled_stack_pointer_survives_helper_call() {
let code = flatten(&[
slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 6, 10, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 6, 0, 0, -64), slot(EBPF_CLS_STX | 0x18, 10, 6, -8, 0), slot(EBPF_OP_CALL, 0, 0, 0, 1), slot(EBPF_CLS_LDX | 0x18, 1, 10, -8, 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[5], REGION_STACK);
assert!(
result.unresolved.is_empty(),
"unexpected unresolved: {:?}",
result.unresolved
);
}
#[test]
fn spilled_stack_pointer_survives_stack_alias_store() {
let code = flatten(&[
slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 6, 10, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 6, 0, 0, -64), slot(EBPF_CLS_STX | 0x18, 10, 6, -8, 0), slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 7, 10, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 7, 0, 0, -64), slot(EBPF_CLS_ST | 0x10, 7, 0, 0, 1), slot(EBPF_CLS_LDX | 0x18, 1, 10, -8, 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[7], REGION_STACK);
assert!(
result.unresolved.is_empty(),
"unexpected unresolved: {:?}",
result.unresolved
);
}
#[test]
fn stack_alias_store_invalidates_overlapping_pointer_spill() {
let code = flatten(&[
slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 6, 10, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 6, 0, 0, -64), slot(EBPF_CLS_STX | 0x18, 10, 6, -8, 0), slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 7, 10, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 7, 0, 0, -8), slot(EBPF_CLS_ST | 0x18, 7, 0, 0, 0), slot(EBPF_CLS_LDX | 0x18, 1, 10, -8, 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[7], REGION_UNKNOWN);
assert_eq!(result.unresolved, vec![7]);
}
#[test]
fn direct_partial_stack_store_invalidates_pointer_spill() {
let code = flatten(&[
slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 6, 10, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 6, 0, 0, -64), slot(EBPF_CLS_STX | 0x18, 10, 6, -8, 0), slot(EBPF_CLS_ST | 0x10, 10, 0, -7, 0), slot(EBPF_CLS_LDX | 0x18, 1, 10, -8, 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[5], REGION_UNKNOWN);
assert_eq!(result.unresolved, vec![5]);
}
#[test]
fn known_stack_alias_store_updates_pointer_spill() {
let code = flatten(&[
slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 6, 10, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 6, 0, 0, -64), slot(EBPF_CLS_ALU64 | EBPF_SRC_REG | EBPF_ALU_OP_MOV, 7, 10, 0, 0), slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 7, 0, 0, -8), slot(EBPF_CLS_STX | 0x18, 7, 6, 0, 0), slot(EBPF_CLS_LDX | 0x18, 1, 10, -8, 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_STACK);
assert!(
result.unresolved.is_empty(),
"unexpected unresolved: {:?}",
result.unresolved
);
}
#[test]
fn foreign_stack_store_does_not_invalidate_current_frame_spill() {
let code = flatten(&[
slot(EBPF_CLS_STX | 0x18, 10, 1, -8, 0), slot(EBPF_CLS_ALU64 | EBPF_ALU_OP_ADD, 1, 0, 0, 1), slot(EBPF_CLS_ST | 0x10, 1, 0, 0, 0), slot(EBPF_CLS_LDX | 0x18, 2, 10, -8, 0), slot(EBPF_CLS_LDX | 0x10, 0, 2, 0, 0), slot(EBPF_OP_EXIT, 0, 0, 0, 0),
]);
let mut regs = [RegKind::Uninit; NUM_REGS];
regs[1] = RegKind::Stack(StackKind::Foreign);
regs[R10] = RegKind::Stack(StackKind::Current(Some(0)));
let result = analyze_function(
&code,
0,
code.len() / 8,
PointerSignature { regs },
DATA_LO,
DATA_HI,
);
assert_eq!(result.hints[4], REGION_STACK);
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]);
}
#[test]
fn unreachable_memory_accesses_are_not_unresolved() {
let code = flatten(&[
slot(EBPF_OP_EXIT, 0, 0, 0, 0),
slot(EBPF_CLS_LDX | 0x18, 0, 4, 0, 0), ]);
let result = analyze(&code, DATA_LO, DATA_HI);
assert!(result.unresolved.is_empty());
}
}