kmoddep/
kerman.rs

1use std::{
2    collections::{HashMap, HashSet},
3    ffi::OsStr,
4    fs::read_to_string,
5    io::Error,
6    path::{Path, PathBuf},
7    process::Command,
8};
9
10pub static MOD_D: &str = "/lib/modules";
11pub static MOD_DEP_F: &str = "modules.dep";
12pub static MOD_INFO_EXE: &str = "/usr/sbin/modinfo";
13
14/// Metadata about the kernel and details about it
15#[derive(Debug, Clone)]
16pub struct KernelInfo {
17    pub version: String,
18    path: PathBuf,
19    dep_path: PathBuf,
20    is_valid: bool,
21    _ext: String,
22
23    // Dependencies list in a format:
24    //     "modulename" -> ["kernel/path/to/a/module.ko.zst", "kernel/other.ko.zst"]
25    deplist: HashMap<String, Vec<String>>,
26
27    // Dependencies list in a format:
28    //     "modulename" -> ["module", "other"]
29    lookup_deplist: HashSet<String>,
30}
31
32impl KernelInfo {
33    /// Creates an instance of a KernelInfo struct with the version
34    /// of the kernel and paths to required points for module analysis.
35    ///
36    /// Root path is either "/" for the host filesystem or a mountpoint
37    /// to the root filesystem.
38    ///
39    /// NOTE: The module resolver is very simple here and won't scale that much
40    ///       if a kernel will have millions of modules. But as of 2024 it
41    ///       works OK with those dozen of thousands as for a generator.
42    ///       Generated CPIO anyway will contain already sorted list.
43    pub fn new(rootpath: &str, kver: &str) -> Result<Self, Error> {
44        Ok(KernelInfo {
45            version: kver.to_owned(),
46            path: PathBuf::from(if ["", "/"].contains(&rootpath) {
47                MOD_D.to_string()
48            } else {
49                format!("{}/{}", rootpath, MOD_D)
50            }),
51            dep_path: PathBuf::from(""),
52            deplist: HashMap::default(),
53            lookup_deplist: HashSet::default(),
54            _ext: "".to_string(),
55            is_valid: false,
56        }
57        .init()?)
58    }
59
60    /// Initialise the KernelInfo. This can be ran only once per an instance.
61    fn init(mut self) -> Result<Self, Error> {
62        if !self._ext.is_empty() {
63            return Ok(self);
64        }
65
66        self.path = self.path.join(&self.version);
67        self.dep_path = self.dep_path.join(self.path.as_os_str()).join(MOD_DEP_F);
68        self.load_deps()?;
69
70        Ok(self)
71    }
72
73    /// Return current kernel info root path.
74    pub fn get_kernel_path(&self) -> PathBuf {
75        PathBuf::from(&self.path)
76    }
77
78    /// Get modules extension system: .ko[.compression]
79    /// NOTE: assumption is that _all modules_ are with the same extension!
80    fn get_fext(&self, fname: Option<&OsStr>) -> String {
81        format!(
82            ".ko{}",
83            fname
84                .unwrap()
85                .to_owned()
86                .to_str()
87                .unwrap()
88                .rsplit_once(".ko")
89                .map_or("", |(_, l)| l)
90        )
91    }
92
93    /// Load module dependencies
94    /// Skip if there is no /lib/modules/<version>/kernel directory
95    fn load_deps(&mut self) -> Result<(), Error> {
96        if !self._ext.is_empty() {
97            return Ok(());
98        }
99
100        let modpath = self.get_kernel_path().join("kernel");
101        self.is_valid = Path::new(modpath.to_str().unwrap()).is_dir();
102        if self.is_valid {
103            for line in read_to_string(self.dep_path.as_os_str())?.lines() {
104                if let Some(sl) = line.split_once(':') {
105                    let (modpath, moddeps) = (sl.0.trim(), sl.1.trim());
106                    if self._ext.is_empty() {
107                        self._ext = self.get_fext(PathBuf::from(modpath).file_name());
108                    }
109
110                    let mut deplist: Vec<String> = vec![];
111                    let mut deplist_idx: Vec<String> = vec![];
112
113                    if !moddeps.is_empty() {
114                        deplist = moddeps.split(' ').map(|x| x.to_owned()).collect();
115                        deplist_idx = deplist
116                            .iter()
117                            .map(|x| {
118                                x.split('/')
119                                    .last()
120                                    .unwrap()
121                                    .split_once('.')
122                                    .unwrap()
123                                    .0
124                                    .to_string()
125                            })
126                            .collect();
127                    }
128
129                    self.deplist.insert(modpath.to_owned(), deplist);
130                    self.lookup_deplist.extend(deplist_idx.into_iter());
131                }
132            }
133        }
134
135        Ok(())
136    }
137
138    /// Returns true if there are actual modules on the media for this kernel.
139    /// There are often kernel paths left after a kernel was not completely purged.
140    pub fn is_valid(&self) -> bool {
141        self.is_valid
142    }
143
144    /// Get path of dependencies file
145    #[allow(dead_code)]
146    pub fn get_dep_path(&self) -> &str {
147        self.dep_path.to_str().unwrap()
148    }
149
150    /// Find a full path to a module
151    /// Example: "sunrpc.ko" will be resolved as "kernel/net/sunrpc/sunrpc.ko"
152    ///
153    /// Some modules are named differently on the disk than in the memory.
154    /// In this case they are tried to be resolved via external "modinfo".
155    fn expand_module_name<'a>(&'a self, name: &'a String) -> &String {
156        let mut m_name: String;
157        if !name.ends_with(self._ext.as_str()) {
158            m_name = format!("{}{}", name, self._ext); // "sunrpc" -> "sunrpc.ko"
159        } else {
160            m_name = name.to_owned();
161        }
162
163        if !m_name.starts_with("kernel/") {
164            // name or partial path
165            if !m_name.contains('/') {
166                m_name = format!("/{}", m_name); // "sunrpc.ko" -> "/sunrpc.ko"
167            }
168
169            for fmodname in self.deplist.keys() {
170                // Eliminate to a minimum 3rd fallback via modinfo by trying replacing underscore with a minus.
171                // This not always works, because some modules called mixed.
172                let mm_name = m_name.replace('_', "-");
173                if fmodname.ends_with(&m_name) || fmodname.ends_with(&mm_name) {
174                    return fmodname;
175                }
176            }
177        }
178
179        let out = Command::new(MOD_INFO_EXE).arg(name).output();
180        match out {
181            Ok(_) => match String::from_utf8(out.unwrap().stdout) {
182                Ok(data) => {
183                    for line in data.lines().map(|el| el.replace(' ', "")) {
184                        if line.starts_with("filename:/") && line.contains("/kernel/") {
185                            let t_modname = format!(
186                                "kernel/{}",
187                                line.split("/kernel/").collect::<Vec<&str>>()[1]
188                            );
189                            for fmodname in self.deplist.keys() {
190                                if *fmodname == t_modname {
191                                    return fmodname;
192                                }
193                            }
194                        }
195                    }
196                }
197                Err(_) => todo!(),
198            },
199            Err(_) => todo!(),
200        }
201
202        name
203    }
204
205    /// Resolve dependencies for one module
206    /// This is an internal method
207    fn get_mod_dep(&self, name: &String, mods: &mut HashSet<String>) {
208        let mdeps = self.deplist.get(name).unwrap();
209        for mdep in mdeps {
210            mods.insert(mdep.to_owned());
211
212            // If a dependency has its own dependencies
213            let d_mdeps = self.deplist.get(mdep).unwrap();
214            if !d_mdeps.is_empty() {
215                for d_dep in d_mdeps {
216                    mods.insert(d_dep.to_owned());
217                    self.get_mod_dep(d_dep, mods);
218                }
219            }
220        }
221    }
222
223    /// Resolve all module dependencies
224    pub fn get_deps_for(&self, names: &[String]) -> HashMap<String, Vec<String>> {
225        let mut mod_tree: HashMap<String, Vec<String>> = HashMap::new();
226        for kmodname in names {
227            let r_kmodname = self.expand_module_name(kmodname);
228            if !r_kmodname.contains('/') {
229                continue;
230            }
231
232            let mut mod_deps: HashSet<String> = HashSet::default();
233            let mut r_deps: Vec<String> = vec![];
234
235            self.get_mod_dep(r_kmodname, &mut mod_deps);
236
237            for v in mod_deps {
238                r_deps.push(v);
239            }
240            mod_tree.insert(r_kmodname.to_owned(), r_deps);
241        }
242
243        mod_tree
244    }
245
246    /// Return true if a given module is a dependency to something else
247    pub fn is_dep(&self, name: &str) -> bool {
248        self.lookup_deplist.contains(name)
249    }
250
251    /// Same as `get_deps_for`, except returns flattened list
252    /// for all modules with their dependencies.
253    pub fn get_deps_for_flatten(&self, names: &[String]) -> Vec<String> {
254        let mut buff: HashSet<String> = HashSet::default();
255        for (mname, mdeps) in &self.get_deps_for(names) {
256            buff.insert(mname.to_owned());
257            buff.extend(mdeps.to_owned());
258        }
259
260        buff.iter().map(|x| x.to_owned()).collect()
261    }
262
263    /// Get all found modules
264    pub fn get_disk_modules(&self) -> Vec<String> {
265        let mut buff: HashSet<String> = HashSet::default();
266
267        for (modname, moddeps) in &self.deplist {
268            buff.insert(modname.to_owned());
269            buff.extend(moddeps.to_owned());
270        }
271
272        let mut mods: Vec<String> = buff.iter().map(|x| x.to_string()).collect();
273        mods.sort();
274
275        mods
276    }
277}