From 65a0b832057a007f8a64eb5c2e3de712e502d634 Mon Sep 17 00:00:00 2001 From: Dave Tucker Date: Wed, 15 Dec 2021 19:00:46 +0000 Subject: [PATCH] Mark .rodata maps as readonly and freeze on load This commit marks .rodata maps as BPF_F_RDONLY_PROG when loaded to prevent a BPF program mutating them. Initial map data is populated by the loader using the new `BpfLoader::set_global()` API. The loader will mark is marked as frozen using bpf_map_freeze to prevent map data being changed from userspace. Signed-off-by: Dave Tucker --- aya/src/bpf.rs | 48 ++++++- aya/src/maps/hash_map/hash_map.rs | 3 + aya/src/maps/mod.rs | 2 + aya/src/maps/perf/perf_event_array.rs | 5 +- aya/src/obj/mod.rs | 190 +++++++++++++++++++++----- aya/src/sys/bpf.rs | 8 ++ aya/src/util.rs | 14 +- 7 files changed, 231 insertions(+), 39 deletions(-) diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs index eea4bd55..1127556d 100644 --- a/aya/src/bpf.rs +++ b/aya/src/bpf.rs @@ -18,15 +18,15 @@ use crate::{ maps::{Map, MapError, MapLock, MapRef, MapRefMut}, obj::{ btf::{Btf, BtfError}, - Object, ParseError, ProgramSection, + MapKind, Object, ParseError, ProgramSection, }, programs::{ BtfTracePoint, CgroupSkb, CgroupSkbAttachType, FEntry, FExit, KProbe, LircMode2, Lsm, PerfEvent, ProbeKind, Program, ProgramData, ProgramError, RawTracePoint, SchedClassifier, SkMsg, SkSkb, SkSkbKind, SockOps, SocketFilter, TracePoint, UProbe, Xdp, }, - sys::bpf_map_update_elem_ptr, - util::{possible_cpus, POSSIBLE_CPUS}, + sys::{bpf_map_freeze, bpf_map_update_elem_ptr}, + util::{bytes_of, possible_cpus, POSSIBLE_CPUS}, }; pub(crate) const BPF_OBJ_NAME_LEN: usize = 16; @@ -102,6 +102,7 @@ impl Default for PinningType { pub struct BpfLoader<'a> { btf: Option>, map_pin_path: Option, + globals: HashMap<&'a str, &'a [u8]>, } impl<'a> BpfLoader<'a> { @@ -110,6 +111,7 @@ impl<'a> BpfLoader<'a> { BpfLoader { btf: Btf::from_sys_fs().ok().map(Cow::Owned), map_pin_path: None, + globals: HashMap::new(), } } @@ -155,6 +157,36 @@ impl<'a> BpfLoader<'a> { self } + /// Sets the value of a global variable + /// + /// From Rust eBPF, a global variable would be constructed as follows: + /// ```no run + /// #[no_mangle] + /// const VERSION = 0; + /// ``` + /// If using a struct, ensure that it is `#[repr(C)]` to ensure the size will + /// match that of the corresponding ELF symbol. + /// + /// From C eBPF, you would annotate a variable as `volatile const` + /// + /// # Example + /// + /// ```no_run + /// use aya::BpfLoader; + /// + /// let bpf = BpfLoader::new() + /// .set_global("VERSION", &2) + /// .load_file("file.o")?; + /// # Ok::<(), aya::BpfError>(()) + /// ``` + /// + pub fn set_global(&mut self, name: &'a str, value: &'a V) -> &mut BpfLoader<'a> { + // Safety: value is POD + let data = unsafe { bytes_of(value) }; + self.globals.insert(name, data); + self + } + /// Loads eBPF bytecode from a file. /// /// # Examples @@ -187,6 +219,7 @@ impl<'a> BpfLoader<'a> { /// ``` pub fn load(&mut self, data: &[u8]) -> Result { let mut obj = Object::parse(data)?; + obj.patch_map_data(self.globals.clone())?; if let Some(btf) = &self.btf { obj.relocate_btf(btf)?; @@ -229,7 +262,7 @@ impl<'a> BpfLoader<'a> { } PinningType::None => map.create(&name)?, }; - if !map.obj.data.is_empty() && name != ".bss" { + if !map.obj.data.is_empty() && map.obj.kind != MapKind::Bss { bpf_map_update_elem_ptr(fd, &0 as *const _, map.obj.data.as_mut_ptr(), 0).map_err( |(code, io_error)| MapError::SyscallError { call: "bpf_map_update_elem".to_owned(), @@ -238,6 +271,13 @@ impl<'a> BpfLoader<'a> { }, )?; } + if map.obj.kind == MapKind::Rodata { + bpf_map_freeze(fd).map_err(|(code, io_error)| MapError::SyscallError { + call: "bpf_map_freeze".to_owned(), + code, + io_error, + })?; + } maps.insert(name, map); } diff --git a/aya/src/maps/hash_map/hash_map.rs b/aya/src/maps/hash_map/hash_map.rs index 3b44c10c..b66f66d3 100644 --- a/aya/src/maps/hash_map/hash_map.rs +++ b/aya/src/maps/hash_map/hash_map.rs @@ -169,6 +169,7 @@ mod tests { }, section_index: 0, data: Vec::new(), + kind: obj::MapKind::Other, } } @@ -221,6 +222,7 @@ mod tests { }, section_index: 0, data: Vec::new(), + kind: obj::MapKind::Other, }, fd: None, pinned: false, @@ -280,6 +282,7 @@ mod tests { }, section_index: 0, data: Vec::new(), + kind: obj::MapKind::Other, }, fd: Some(42), pinned: false, diff --git a/aya/src/maps/mod.rs b/aya/src/maps/mod.rs index 3f124182..1b60d0c0 100644 --- a/aya/src/maps/mod.rs +++ b/aya/src/maps/mod.rs @@ -477,6 +477,7 @@ mod tests { use crate::{ bpf_map_def, generated::{bpf_cmd, bpf_map_type::BPF_MAP_TYPE_HASH}, + obj::MapKind, sys::{override_syscall, Syscall}, }; @@ -493,6 +494,7 @@ mod tests { }, section_index: 0, data: Vec::new(), + kind: MapKind::Other, } } diff --git a/aya/src/maps/perf/perf_event_array.rs b/aya/src/maps/perf/perf_event_array.rs index 9695ea60..73a04161 100644 --- a/aya/src/maps/perf/perf_event_array.rs +++ b/aya/src/maps/perf/perf_event_array.rs @@ -9,7 +9,6 @@ use std::{ }; use bytes::BytesMut; -use libc::{sysconf, _SC_PAGESIZE}; use crate::{ generated::bpf_map_type::BPF_MAP_TYPE_PERF_EVENT_ARRAY, @@ -18,6 +17,7 @@ use crate::{ Map, MapError, MapRefMut, }, sys::bpf_map_update_elem, + util::page_size, }; /// A ring buffer that can receive events from eBPF programs. @@ -177,8 +177,7 @@ impl> PerfEventArray { Ok(PerfEventArray { map: Arc::new(map), - // Safety: libc - page_size: unsafe { sysconf(_SC_PAGESIZE) } as usize, + page_size: page_size(), }) } diff --git a/aya/src/obj/mod.rs b/aya/src/obj/mod.rs index 2eee874f..61dd9af6 100644 --- a/aya/src/obj/mod.rs +++ b/aya/src/obj/mod.rs @@ -19,7 +19,7 @@ use relocation::*; use crate::{ bpf_map_def, - generated::{bpf_insn, bpf_map_type::BPF_MAP_TYPE_ARRAY}, + generated::{bpf_insn, bpf_map_type::BPF_MAP_TYPE_ARRAY, BPF_F_RDONLY_PROG}, obj::btf::{Btf, BtfError, BtfExt}, BpfError, }; @@ -43,11 +43,34 @@ pub struct Object { pub(crate) symbols_by_index: HashMap, } +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum MapKind { + Bss, + Data, + Rodata, + Other, +} + +impl From<&str> for MapKind { + fn from(s: &str) -> Self { + if s == ".bss" { + MapKind::Bss + } else if s.starts_with(".data") { + MapKind::Data + } else if s.starts_with(".rodata") { + MapKind::Rodata + } else { + MapKind::Other + } + } +} + #[derive(Debug, Clone)] pub struct Map { pub(crate) def: bpf_map_def, pub(crate) section_index: usize, pub(crate) data: Vec, + pub(crate) kind: MapKind, } #[derive(Debug, Clone)] @@ -238,6 +261,51 @@ impl Object { } } + pub fn patch_map_data(&mut self, globals: HashMap<&str, &[u8]>) -> Result<(), ParseError> { + let symbols: HashMap = self + .symbols_by_index + .iter() + .filter(|(_, s)| s.name.is_some()) + .map(|(_, s)| (s.name.as_ref().unwrap().clone(), s)) + .collect(); + + for (name, data) in globals { + if let Some(symbol) = symbols.get(name) { + if data.len() as u64 != symbol.size { + return Err(ParseError::InvalidGlobalData { + name: name.to_string(), + sym_size: symbol.size, + data_size: data.len(), + }); + } + let (_, map) = self + .maps + .iter_mut() + // assumption: there is only one map created per section where we're trying to + // patch data. this assumption holds true for the .rodata section at least + .find(|(_, m)| symbol.section_index == Some(SectionIndex(m.section_index))) + .ok_or_else(|| ParseError::MapNotFound { + index: symbol.section_index.unwrap_or(SectionIndex(0)).0, + })?; + let start = symbol.address as usize; + let end = start + symbol.size as usize; + if start > end || end > map.data.len() { + return Err(ParseError::InvalidGlobalData { + name: name.to_string(), + sym_size: symbol.size, + data_size: data.len(), + }); + } + map.data.splice(start..end, data.iter().cloned()); + } else { + return Err(ParseError::SymbolNotFound { + name: name.to_owned(), + }); + } + } + Ok(()) + } + fn parse_btf(&mut self, section: &Section) -> Result<(), BtfError> { self.btf = Some(Btf::parse(section.data, self.endianness)?); @@ -417,6 +485,19 @@ pub enum ParseError { #[error("invalid symbol, index `{index}` name: {}", .name.as_ref().unwrap_or(&"[unknown]".into()))] InvalidSymbol { index: usize, name: Option }, + + #[error("symbol {name} has size `{sym_size}`, but provided data is of size `{data_size}`")] + InvalidGlobalData { + name: String, + sym_size: u64, + data_size: usize, + }, + + #[error("symbol with name {name} not found in the symbols table")] + SymbolNotFound { name: String }, + + #[error("map for section with index {index} not found")] + MapNotFound { index: usize }, } #[derive(Debug)] @@ -570,27 +651,32 @@ impl From for u32 { } fn parse_map(section: &Section, name: &str) -> Result { - let (def, data) = if name == ".bss" || name.starts_with(".data") || name.starts_with(".rodata") - { - let def = bpf_map_def { - map_type: BPF_MAP_TYPE_ARRAY as u32, - key_size: mem::size_of::() as u32, - // We need to use section.size here since - // .bss will always have data.len() == 0 - value_size: section.size as u32, - max_entries: 1, - map_flags: 0, /* FIXME: set rodata readonly */ - ..Default::default() - }; - (def, section.data.to_vec()) - } else { - (parse_map_def(name, section.data)?, Vec::new()) + let kind = MapKind::from(name); + let (def, data) = match kind { + MapKind::Bss | MapKind::Data | MapKind::Rodata => { + let def = bpf_map_def { + map_type: BPF_MAP_TYPE_ARRAY as u32, + key_size: mem::size_of::() as u32, + // We need to use section.size here since + // .bss will always have data.len() == 0 + value_size: section.size as u32, + max_entries: 1, + map_flags: if kind == MapKind::Rodata { + BPF_F_RDONLY_PROG + } else { + 0 + }, + ..Default::default() + }; + (def, section.data.to_vec()) + } + MapKind::Other => (parse_map_def(name, section.data)?, Vec::new()), }; - Ok(Map { section_index: section.index.0, def, data, + kind, }) } @@ -634,7 +720,6 @@ fn copy_instructions(data: &[u8]) -> Result, ParseError> { mod tests { use matches::assert_matches; use object::Endianness; - use std::slice; use super::*; use crate::PinningType; @@ -662,8 +747,8 @@ mod tests { } fn bytes_of(val: &T) -> &[u8] { - let size = mem::size_of::(); - unsafe { slice::from_raw_parts(slice::from_ref(val).as_ptr().cast(), size) } + // Safety: This is for testing only + unsafe { crate::util::bytes_of(val) } } #[test] @@ -786,7 +871,7 @@ mod tests { #[test] fn test_parse_map_error() { assert!(matches!( - parse_map(&fake_section(BpfSectionKind::Maps, "maps/foo", &[]), "foo"), + parse_map(&fake_section(BpfSectionKind::Maps, "maps/foo", &[]), "foo",), Err(ParseError::InvalidMapDefinition { .. }) )); } @@ -821,7 +906,8 @@ mod tests { id: 0, pinning: PinningType::None, }, - data + data, + .. }) if data.is_empty() )) } @@ -849,8 +935,9 @@ mod tests { id: 0, pinning: PinningType::None, }, - data - }) if data == map_data && value_size == map_data.len() as u32 + data, + kind + }) if data == map_data && value_size == map_data.len() as u32 && kind == MapKind::Bss )) } @@ -871,7 +958,7 @@ mod tests { BpfSectionKind::Program, "kprobe/foo", &42u32.to_ne_bytes(), - ),), + )), Err(ParseError::InvalidProgramCode) ); } @@ -913,7 +1000,7 @@ mod tests { map_flags: 5, ..Default::default() }) - ),), + )), Ok(()) ); assert!(obj.maps.get("foo").is_some()); @@ -923,13 +1010,13 @@ mod tests { fn test_parse_section_data() { let mut obj = fake_obj(); assert_matches!( - obj.parse_section(fake_section(BpfSectionKind::Data, ".bss", b"map data"),), + obj.parse_section(fake_section(BpfSectionKind::Data, ".bss", b"map data")), Ok(()) ); assert!(obj.maps.get(".bss").is_some()); assert_matches!( - obj.parse_section(fake_section(BpfSectionKind::Data, ".rodata", b"map data"),), + obj.parse_section(fake_section(BpfSectionKind::Data, ".rodata", b"map data")), Ok(()) ); assert!(obj.maps.get(".rodata").is_some()); @@ -939,19 +1026,19 @@ mod tests { BpfSectionKind::Data, ".rodata.boo", b"map data" - ),), + )), Ok(()) ); assert!(obj.maps.get(".rodata.boo").is_some()); assert_matches!( - obj.parse_section(fake_section(BpfSectionKind::Data, ".data", b"map data"),), + obj.parse_section(fake_section(BpfSectionKind::Data, ".data", b"map data")), Ok(()) ); assert!(obj.maps.get(".data").is_some()); assert_matches!( - obj.parse_section(fake_section(BpfSectionKind::Data, ".data.boo", b"map data"),), + obj.parse_section(fake_section(BpfSectionKind::Data, ".data.boo", b"map data")), Ok(()) ); assert!(obj.maps.get(".data.boo").is_some()); @@ -1240,4 +1327,45 @@ mod tests { }) ); } + + #[test] + fn test_patch_map_data() { + let mut obj = fake_obj(); + obj.maps.insert( + ".rodata".to_string(), + Map { + def: bpf_map_def { + map_type: BPF_MAP_TYPE_ARRAY as u32, + key_size: mem::size_of::() as u32, + value_size: 3, + max_entries: 1, + map_flags: BPF_F_RDONLY_PROG, + id: 1, + pinning: PinningType::None, + }, + section_index: 1, + data: vec![0, 0, 0], + kind: MapKind::Rodata, + }, + ); + obj.symbols_by_index.insert( + 1, + Symbol { + index: 1, + section_index: Some(SectionIndex(1)), + name: Some("my_config".to_string()), + address: 0, + size: 3, + is_definition: true, + is_text: false, + }, + ); + + let test_data: &[u8] = &[1, 2, 3]; + obj.patch_map_data(HashMap::from([("my_config", test_data)])) + .unwrap(); + + let map = obj.maps.get(".rodata").unwrap(); + assert_eq!(test_data, map.data); + } } diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs index 992c9ed0..5ef38d89 100644 --- a/aya/src/sys/bpf.rs +++ b/aya/src/sys/bpf.rs @@ -253,6 +253,14 @@ pub(crate) fn bpf_map_get_next_key( } } +// since kernel 5.2 +pub(crate) fn bpf_map_freeze(fd: RawFd) -> SysResult { + let mut attr = unsafe { mem::zeroed::() }; + let u = unsafe { &mut attr.__bindgen_anon_2 }; + u.map_fd = fd as u32; + sys_bpf(bpf_cmd::BPF_MAP_FREEZE, &attr) +} + // since kernel 5.7 pub(crate) fn bpf_link_create( prog_fd: RawFd, diff --git a/aya/src/util.rs b/aya/src/util.rs index a2496c90..f2c31e68 100644 --- a/aya/src/util.rs +++ b/aya/src/util.rs @@ -4,12 +4,13 @@ use std::{ ffi::CString, fs::{self, File}, io::{self, BufReader}, + mem, slice, str::FromStr, }; use crate::generated::{TC_H_MAJ_MASK, TC_H_MIN_MASK}; -use libc::if_nametoindex; +use libc::{if_nametoindex, sysconf, _SC_PAGESIZE}; use io::BufRead; @@ -143,6 +144,17 @@ macro_rules! include_bytes_aligned { }}; } +pub(crate) fn page_size() -> usize { + // Safety: libc + (unsafe { sysconf(_SC_PAGESIZE) }) as usize +} + +// bytes_of converts a to a byte slice +pub(crate) unsafe fn bytes_of(val: &T) -> &[u8] { + let size = mem::size_of::(); + slice::from_raw_parts(slice::from_ref(val).as_ptr().cast(), size) +} + #[cfg(test)] mod tests { use super::*;