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 <dave@dtucker.co.uk>
pull/146/head
Dave Tucker 3 years ago
parent 3a4c84fe17
commit 65a0b83205

@ -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<Cow<'a, Btf>>,
map_pin_path: Option<PathBuf>,
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<V: Pod>(&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<Bpf, BpfError> {
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);
}

@ -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,

@ -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,
}
}

@ -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<T: DerefMut<Target = Map>> PerfEventArray<T> {
Ok(PerfEventArray {
map: Arc::new(map),
// Safety: libc
page_size: unsafe { sysconf(_SC_PAGESIZE) } as usize,
page_size: page_size(),
})
}

@ -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<usize, Symbol>,
}
#[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<u8>,
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<String, &Symbol> = 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<String> },
#[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,8 +651,9 @@ impl From<KernelVersion> for u32 {
}
fn parse_map(section: &Section, name: &str) -> Result<Map, ParseError> {
let (def, data) = if name == ".bss" || name.starts_with(".data") || name.starts_with(".rodata")
{
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::<u32>() as u32,
@ -579,18 +661,22 @@ fn parse_map(section: &Section, name: &str) -> Result<Map, ParseError> {
// .bss will always have data.len() == 0
value_size: section.size as u32,
max_entries: 1,
map_flags: 0, /* FIXME: set rodata readonly */
map_flags: if kind == MapKind::Rodata {
BPF_F_RDONLY_PROG
} else {
0
},
..Default::default()
};
(def, section.data.to_vec())
} else {
(parse_map_def(name, section.data)?, Vec::new())
}
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<Vec<bpf_insn>, 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<T>(val: &T) -> &[u8] {
let size = mem::size_of::<T>();
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::<u32>() 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);
}
}

@ -253,6 +253,14 @@ pub(crate) fn bpf_map_get_next_key<K>(
}
}
// since kernel 5.2
pub(crate) fn bpf_map_freeze(fd: RawFd) -> SysResult {
let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
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,

@ -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 <T> to a byte slice
pub(crate) unsafe fn bytes_of<T>(val: &T) -> &[u8] {
let size = mem::size_of::<T>();
slice::from_raw_parts(slice::from_ref(val).as_ptr().cast(), size)
}
#[cfg(test)]
mod tests {
use super::*;

Loading…
Cancel
Save