Browse Source

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 <[email protected]>
Dave Tucker 3 years ago
parent
commit
65a0b83205

+ 44 - 4
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<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);
         }
 

+ 3 - 0
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,

+ 2 - 0
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,
         }
     }
 

+ 2 - 3
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<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(),
         })
     }
 

+ 159 - 31
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<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,27 +651,32 @@ 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 def = bpf_map_def {
-            map_type: BPF_MAP_TYPE_ARRAY as u32,
-            key_size: mem::size_of::<u32>() 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::<u32>() 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<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);
+    }
 }

+ 8 - 0
aya/src/sys/bpf.rs

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

+ 13 - 1
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 <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::*;