tests/state/
linux.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Functions to gather BPF information from a Linux userspace
4///
5/// See `BPFState` for more details.
6use std::num::ParseIntError;
7use std::os::unix::fs::DirEntryExt;
8use std::path::PathBuf;
9use std::process::Command;
10use std::{collections::HashMap, path::Path};
11
12use anyhow::{anyhow, Context, Result};
13use libbpf_rs::{skel::Skel, MapCore};
14use serde::Deserialize;
15
16use super::{BPFState, MapState, PinState};
17
18/// Helper structure to deserialize standard maps from `bpftool`
19#[derive(Deserialize)]
20struct BPFToolMapDump {
21    key: Vec<String>,
22    value: Vec<String>,
23}
24
25/// Helper structure to deserialize PerCPU map values from `bpftool`
26#[derive(Deserialize)]
27struct BPFToolPerCPUMapDumpValue {
28    value: Vec<String>,
29}
30
31/// Helper structure to deserialize PerCPU maps from `bpftool`
32#[derive(Deserialize)]
33struct BPFToolPerCPUMapDump {
34    key: Vec<String>,
35    values: Vec<BPFToolPerCPUMapDumpValue>,
36}
37
38/// Runs `bpftool` with the given arguments and returns output from stdout
39fn bpftool_command(args: &str) -> Result<String> {
40    let output = Command::new("bpftool")
41        .args(args.split_whitespace())
42        .output()?;
43    Ok(String::from_utf8(output.stdout)?)
44}
45
46/// Converts strings output by `bpftool` into a vector of bytes that
47/// can be compared against other [BPFState] objects in a consistent manner
48fn bpftool_str_to_bytes(strings: &[String]) -> Result<Vec<u8>, ParseIntError> {
49    strings
50        .iter()
51        .map(|string| parse_int::parse::<u8>(string))
52        .collect()
53}
54
55/// Creates a [HashMap] of [MapState] objects with information harvested
56/// by running bpftool
57fn bpftool_maps(skel: &dyn Skel, ground_truth: &BPFState) -> Result<HashMap<String, MapState>> {
58    let mut hashmap = HashMap::new();
59    for map in skel.object().maps() {
60        let name = map
61            .name()
62            .to_str()
63            .context(format!("Cannot convert {:#?}", map.name()))?;
64        let map_id = map.info()?.info.id;
65        let gt_map_state = match ground_truth.maps.get(name) {
66            Some(map_state) => map_state,
67            None => return Err(anyhow!("\"{}\" map not found in ground truth", name)),
68        };
69        let show_str = bpftool_command(&format!("-j map show id {map_id}"))?;
70        let mut state: MapState = serde_json::from_slice(show_str.as_bytes())?;
71        state.is_static = gt_map_state.is_static;
72
73        // don't read the data if the map isn't static, it won't be tested
74        if state.bytes_key != 0 && state.is_static {
75            let dump_str = bpftool_command(&format!("-j map dump id {}", &state.id))?;
76            if state._type.starts_with("percpu") {
77                let data: Vec<BPFToolPerCPUMapDump> = serde_json::from_slice(dump_str.as_bytes())?;
78                for entry in data.iter() {
79                    let key = bpftool_str_to_bytes(&entry.key)?;
80                    let mut values = Vec::new();
81                    for value in entry.values.iter() {
82                        values.push(bpftool_str_to_bytes(&value.value)?);
83                    }
84                    state.data.insert(key, values);
85                }
86            } else {
87                let data: Vec<BPFToolMapDump> = serde_json::from_slice(dump_str.as_bytes())?;
88                for entry in data {
89                    let key = bpftool_str_to_bytes(&entry.key)?;
90                    let value = bpftool_str_to_bytes(&entry.value)?;
91                    state.data.insert(key, vec![value]);
92                }
93            }
94        }
95        hashmap.insert(name.to_string(), state);
96    }
97    Ok(hashmap)
98}
99
100/// Checks whether the pin directory exists on the BPF filesystem
101/// and is a valid directory
102fn check_pin_dir(pin_dir: &Path) -> Result<()> {
103    if !pin_dir.exists() {
104        return Err(anyhow!(
105            "Pin dir `{}` doesn't exist",
106            pin_dir.to_string_lossy()
107        ));
108    }
109    // Test that the path points to a directory
110    if !pin_dir.is_dir() {
111        return Err(anyhow!(
112            "Pin dir `{}` is not a directory",
113            pin_dir.to_string_lossy()
114        ));
115    }
116    Ok(())
117}
118
119/// Gathers [PinState] information by reading the Linux filesystem
120fn linux_pins(pin_dir: &PathBuf) -> Result<PinState> {
121    check_pin_dir(pin_dir)?;
122    let mut pins = HashMap::new();
123    for pin in std::fs::read_dir(pin_dir)? {
124        let pin = pin?;
125        pins.insert(pin.file_name().to_string_lossy().to_string(), pin.ino());
126    }
127    Ok(PinState {
128        dir: pin_dir.to_string_lossy().to_string(),
129        pins,
130    })
131}
132
133/// Creates a [BPFState] object from the perspective of a
134/// superuser in Linux
135pub fn linux_state(
136    skel: &dyn Skel,
137    pin_dir: &PathBuf,
138    ground_truth: &BPFState,
139) -> Result<BPFState> {
140    Ok(BPFState {
141        maps: bpftool_maps(skel, ground_truth)?,
142        pins: linux_pins(pin_dir)?,
143        ..Default::default()
144    })
145}