mirror of https://github.com/aya-rs/aya
				
				
				
			Merge pull request #1160 from dave-tucker/modprobe
						commit
						29f4f2b780
					
				| @ -0,0 +1,30 @@ | ||||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import os | ||||
| import glob | ||||
| import sys | ||||
| from typing import List | ||||
| 
 | ||||
| def find_kernels(directory: str) -> List[str]: | ||||
|     return glob.glob(f"{directory}/**/vmlinuz-*", recursive=True) | ||||
| 
 | ||||
| def find_modules_directory(directory: str, kernel: str) -> str: | ||||
|     matches = glob.glob(f"{directory}/**/modules/{kernel}", recursive=True) | ||||
|     if len(matches) != 1: | ||||
|         raise RuntimeError(f"Expected to find exactly one modules directory. Found {len(matches)}.") | ||||
|     return matches[0] | ||||
| 
 | ||||
| def main() -> None: | ||||
|     images = find_kernels('test/.tmp') | ||||
|     modules = [] | ||||
| 
 | ||||
|     for image in images: | ||||
|         image_name = os.path.basename(image).replace('vmlinuz-', '') | ||||
|         module_dir = find_modules_directory('test/.tmp', image_name) | ||||
|         modules.append(module_dir) | ||||
| 
 | ||||
|     args = ' '.join(f"{image}:{module}" for image, module in zip(images, modules)) | ||||
|     print(args) | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
| @ -1,7 +1,8 @@ | ||||
| { | ||||
|     "rust-analyzer.check.allTargets": true, | ||||
|     "rust-analyzer.check.command": "clippy", | ||||
|     "search.exclude": { | ||||
|         "/xtask/public-api/*.txt": true, | ||||
|     }, | ||||
|   "rust-analyzer.check.allTargets": true, | ||||
|   "rust-analyzer.check.command": "clippy", | ||||
|   "search.exclude": { | ||||
|     "/xtask/public-api/*.txt": true | ||||
|   }, | ||||
|   "yaml.format.singleQuote": true | ||||
| } | ||||
|  | ||||
| @ -1,18 +0,0 @@ | ||||
| [package] | ||||
| name = "init" | ||||
| publish = false | ||||
| version = "0.1.0" | ||||
| 
 | ||||
| authors.workspace = true | ||||
| edition.workspace = true | ||||
| homepage.workspace = true | ||||
| license.workspace = true | ||||
| repository.workspace = true | ||||
| rust-version.workspace = true | ||||
| 
 | ||||
| [lints] | ||||
| workspace = true | ||||
| 
 | ||||
| [dependencies] | ||||
| anyhow = { workspace = true, features = ["std"] } | ||||
| nix = { workspace = true, features = ["fs", "mount", "reboot"] } | ||||
| @ -0,0 +1,38 @@ | ||||
| [package] | ||||
| name = "test-distro" | ||||
| publish = false | ||||
| version = "0.1.0" | ||||
| 
 | ||||
| authors.workspace = true | ||||
| edition.workspace = true | ||||
| homepage.workspace = true | ||||
| license.workspace = true | ||||
| repository.workspace = true | ||||
| 
 | ||||
| [[bin]] | ||||
| name = "init" | ||||
| path = "src/init.rs" | ||||
| 
 | ||||
| [[bin]] | ||||
| name = "modprobe" | ||||
| path = "src/modprobe.rs" | ||||
| 
 | ||||
| [[bin]] | ||||
| name = "depmod" | ||||
| path = "src/depmod.rs" | ||||
| 
 | ||||
| [dependencies] | ||||
| anyhow = { workspace = true, features = ["std"] } | ||||
| clap = { workspace = true, default-features = true, features = ["derive"] } | ||||
| glob = { workspace = true } | ||||
| nix = { workspace = true, features = [ | ||||
|     "user", | ||||
|     "fs", | ||||
|     "mount", | ||||
|     "reboot", | ||||
|     "kmod", | ||||
|     "feature", | ||||
| ] } | ||||
| object = { workspace = true, features = ["elf", "read_core", "std"] } | ||||
| walkdir = { workspace = true } | ||||
| xz2 = { workspace = true } | ||||
| @ -0,0 +1,140 @@ | ||||
| //! depmod is used to build the modules.alias file to assist with loading
 | ||||
| //! kernel modules.
 | ||||
| //!
 | ||||
| //! This implementation is incredibly naive and is only designed to work within
 | ||||
| //! the constraints of the test environment. Not for production use.
 | ||||
| 
 | ||||
| use std::{ | ||||
|     fs::File, | ||||
|     io::{BufWriter, Read, Write as _}, | ||||
|     path::PathBuf, | ||||
| }; | ||||
| 
 | ||||
| use anyhow::{Context as _, anyhow}; | ||||
| use clap::Parser; | ||||
| use object::{Object, ObjectSection, ObjectSymbol, Section}; | ||||
| use test_distro::resolve_modules_dir; | ||||
| use walkdir::WalkDir; | ||||
| use xz2::read::XzDecoder; | ||||
| 
 | ||||
| #[derive(Parser)] | ||||
| struct Args { | ||||
|     #[clap(long, short)] | ||||
|     base_dir: Option<PathBuf>, | ||||
| } | ||||
| 
 | ||||
| fn main() -> anyhow::Result<()> { | ||||
|     let Args { base_dir } = Parser::parse(); | ||||
| 
 | ||||
|     let modules_dir = if let Some(base_dir) = base_dir { | ||||
|         base_dir | ||||
|     } else { | ||||
|         resolve_modules_dir().context("failed to resolve modules dir")? | ||||
|     }; | ||||
| 
 | ||||
|     let modules_alias = modules_dir.join("modules.alias"); | ||||
|     let f = std::fs::OpenOptions::new() | ||||
|         .create(true) | ||||
|         .write(true) | ||||
|         .truncate(true) | ||||
|         .open(&modules_alias) | ||||
|         .with_context(|| format!("failed to open: {}", modules_alias.display()))?; | ||||
|     let mut output = BufWriter::new(&f); | ||||
|     for entry in WalkDir::new(modules_dir) { | ||||
|         let entry = entry.context("failed to read entry in walkdir")?; | ||||
|         if entry.file_type().is_file() { | ||||
|             let path = entry.path(); | ||||
| 
 | ||||
|             let module_name = path | ||||
|                 .file_name() | ||||
|                 .ok_or_else(|| anyhow!("{} does not have a file name", path.display()))? | ||||
|                 .to_str() | ||||
|                 .ok_or_else(|| anyhow!("{} is not valid utf-8", path.display()))?; | ||||
| 
 | ||||
|             let (module_name, compressed) = | ||||
|                 if let Some(module_name) = module_name.strip_suffix(".xz") { | ||||
|                     (module_name, true) | ||||
|                 } else { | ||||
|                     (module_name, false) | ||||
|                 }; | ||||
| 
 | ||||
|             let module_name = if let Some(module_name) = module_name.strip_suffix(".ko") { | ||||
|                 module_name | ||||
|             } else { | ||||
|                 // Not a kernel module
 | ||||
|                 continue; | ||||
|             }; | ||||
| 
 | ||||
|             let mut f = | ||||
|                 File::open(path).with_context(|| format!("failed to open: {}", path.display()))?; | ||||
|             let stat = f | ||||
|                 .metadata() | ||||
|                 .with_context(|| format!("failed to get metadata for {}", path.display()))?; | ||||
| 
 | ||||
|             if compressed { | ||||
|                 let mut decoder = XzDecoder::new(f); | ||||
|                 // We don't know the size of the decompressed data, so we assume it's
 | ||||
|                 // no more than twice the size of the compressed data.
 | ||||
|                 let mut decompressed = Vec::with_capacity(stat.len() as usize * 2); | ||||
|                 decoder.read_to_end(&mut decompressed)?; | ||||
|                 read_aliases_from_module(&decompressed, module_name, &mut output) | ||||
|             } else { | ||||
|                 let mut buf = Vec::with_capacity(stat.len() as usize); | ||||
|                 f.read_to_end(&mut buf) | ||||
|                     .with_context(|| format!("failed to read: {}", path.display()))?; | ||||
|                 read_aliases_from_module(&buf, module_name, &mut output) | ||||
|             } | ||||
|             .with_context(|| format!("failed to read aliases from module {}", path.display()))?; | ||||
|         } | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| fn read_aliases_from_module( | ||||
|     contents: &[u8], | ||||
|     module_name: &str, | ||||
|     output: &mut BufWriter<&File>, | ||||
| ) -> Result<(), anyhow::Error> { | ||||
|     let obj = object::read::File::parse(contents).context("failed to parse")?; | ||||
| 
 | ||||
|     let section = (|| -> anyhow::Result<Option<Section<'_, '_, &[u8]>>> { | ||||
|         for s in obj.sections() { | ||||
|             let name = s | ||||
|                 .name_bytes() | ||||
|                 .with_context(|| format!("failed to get name of section idx {}", s.index()))?; | ||||
|             if name == b".modinfo" { | ||||
|                 return Ok(Some(s)); | ||||
|             } | ||||
|         } | ||||
|         Ok(None) | ||||
|     })()?; | ||||
|     let section = section.context("failed to find .modinfo section")?; | ||||
|     let section_idx = section.index(); | ||||
|     let data = section | ||||
|         .data() | ||||
|         .context("failed to get modinfo section data")?; | ||||
| 
 | ||||
|     for s in obj.symbols() { | ||||
|         if s.section_index() != Some(section_idx) { | ||||
|             continue; | ||||
|         } | ||||
|         let name = s | ||||
|             .name() | ||||
|             .with_context(|| format!("failed to get name of symbol idx {}", s.index()))?; | ||||
|         if name.contains("alias") { | ||||
|             let start = s.address() as usize; | ||||
|             let end = start + s.size() as usize; | ||||
|             let sym_data = &data[start..end]; | ||||
|             let cstr = std::ffi::CStr::from_bytes_with_nul(sym_data) | ||||
|                 .with_context(|| format!("failed to convert {:?} to cstr", sym_data))?; | ||||
|             let sym_str = cstr | ||||
|                 .to_str() | ||||
|                 .with_context(|| format!("failed to convert {:?} to str", cstr))?; | ||||
|             let alias = sym_str | ||||
|                 .strip_prefix("alias=") | ||||
|                 .with_context(|| format!("failed to strip prefix 'alias=' from {}", sym_str))?; | ||||
|             writeln!(output, "alias {} {}", alias, module_name).expect("write"); | ||||
|         } | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
| @ -0,0 +1,30 @@ | ||||
| use std::path::PathBuf; | ||||
| 
 | ||||
| use anyhow::Context as _; | ||||
| use nix::sys::utsname::uname; | ||||
| 
 | ||||
| /// Kernel modules are in `/lib/modules`.
 | ||||
| /// They may be in the root of this directory,
 | ||||
| /// or in subdirectory named after the kernel release.
 | ||||
| pub fn resolve_modules_dir() -> anyhow::Result<PathBuf> { | ||||
|     let modules_dir = PathBuf::from("/lib/modules"); | ||||
|     let stat = modules_dir | ||||
|         .metadata() | ||||
|         .with_context(|| format!("stat(): {}", modules_dir.display()))?; | ||||
|     if stat.is_dir() { | ||||
|         return Ok(modules_dir); | ||||
|     } | ||||
| 
 | ||||
|     let utsname = uname().context("uname()")?; | ||||
|     let release = utsname.release(); | ||||
|     let modules_dir = modules_dir.join(release); | ||||
|     let stat = modules_dir | ||||
|         .metadata() | ||||
|         .with_context(|| format!("stat(): {}", modules_dir.display()))?; | ||||
|     anyhow::ensure!( | ||||
|         stat.is_dir(), | ||||
|         "{} is not a directory", | ||||
|         modules_dir.display() | ||||
|     ); | ||||
|     Ok(modules_dir) | ||||
| } | ||||
| @ -0,0 +1,140 @@ | ||||
| //! modprobe is used to load kernel modules into the kernel.
 | ||||
| //!
 | ||||
| //! This implementation is incredibly naive and is only designed to work within
 | ||||
| //! the constraints of the test environment. Not for production use.
 | ||||
| 
 | ||||
| use std::{ | ||||
|     fs::File, | ||||
|     io::{BufRead as _, Read as _}, | ||||
|     path::Path, | ||||
| }; | ||||
| 
 | ||||
| use anyhow::{Context as _, anyhow, bail}; | ||||
| use clap::Parser; | ||||
| use glob::glob; | ||||
| use nix::kmod::init_module; | ||||
| use test_distro::resolve_modules_dir; | ||||
| 
 | ||||
| macro_rules! output { | ||||
|     ($quiet:expr, $($arg:tt)*) => { | ||||
|         if !$quiet { | ||||
|             println!($($arg)*); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| #[derive(Parser)] | ||||
| struct Args { | ||||
|     /// Suppress all output and don't return an error code.
 | ||||
|     #[clap(short, long, default_value = "false")] | ||||
|     quiet: bool, | ||||
| 
 | ||||
|     /// The name of the module to load.
 | ||||
|     /// This can be either an alias like `net-sched-sch-ingress` or a module
 | ||||
|     /// name like `sch_ingress`.
 | ||||
|     name: String, | ||||
| } | ||||
| 
 | ||||
| fn main() -> anyhow::Result<()> { | ||||
|     let Args { quiet, name } = Parser::parse(); | ||||
|     let ret = try_main(quiet, name); | ||||
|     if quiet { Ok(()) } else { ret } | ||||
| } | ||||
| 
 | ||||
| fn try_main(quiet: bool, name: String) -> anyhow::Result<()> { | ||||
|     let modules_dir = resolve_modules_dir()?; | ||||
| 
 | ||||
|     output!(quiet, "resolving alias for module: {}", name); | ||||
|     let module = resolve_alias(quiet, &modules_dir, &name)?; | ||||
| 
 | ||||
|     let pattern = format!( | ||||
|         "{}/kernel/**/{}.ko*", | ||||
|         modules_dir | ||||
|             .to_str() | ||||
|             .ok_or_else(|| anyhow!("failed to convert {} to string", modules_dir.display()))?, | ||||
|         module | ||||
|     ); | ||||
|     let module_path = glob(&pattern) | ||||
|         .with_context(|| format!("failed to glob: {}", pattern))? | ||||
|         .next() | ||||
|         .ok_or_else(|| anyhow!("module not found: {}", module))? | ||||
|         .context("glob error")?; | ||||
| 
 | ||||
|     output!(quiet, "loading module: {}", module_path.display()); | ||||
|     let mut f = | ||||
|         File::open(&module_path).with_context(|| format!("open(): {}", module_path.display()))?; | ||||
| 
 | ||||
|     let stat = f | ||||
|         .metadata() | ||||
|         .with_context(|| format!("stat(): {}", module_path.display()))?; | ||||
| 
 | ||||
|     let extension = module_path | ||||
|         .as_path() | ||||
|         .extension() | ||||
|         .ok_or_else(|| anyhow!("module has no extension: {}", module_path.display()))?; | ||||
| 
 | ||||
|     let contents = if extension == "xz" { | ||||
|         output!(quiet, "decompressing module"); | ||||
|         let mut decompressed = Vec::with_capacity(stat.len() as usize * 2); | ||||
|         xz2::read::XzDecoder::new(f).read_to_end(&mut decompressed)?; | ||||
|         decompressed | ||||
|     } else { | ||||
|         let mut contents: Vec<u8> = Vec::with_capacity(stat.len() as usize); | ||||
|         f.read_to_end(&mut contents)?; | ||||
|         contents | ||||
|     }; | ||||
| 
 | ||||
|     if !contents.starts_with(&[0x7f, 0x45, 0x4c, 0x46]) { | ||||
|         bail!("module is not an valid ELF file"); | ||||
|     } | ||||
| 
 | ||||
|     match init_module(&contents, c"") { | ||||
|         Ok(()) => { | ||||
|             output!(quiet, "module loaded successfully"); | ||||
|             Ok(()) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             if e == nix::errno::Errno::EEXIST { | ||||
|                 Err(anyhow!("module already loaded")) | ||||
|             } else { | ||||
|                 Err(anyhow!("failed to load module: {}", e)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn resolve_alias(quiet: bool, module_dir: &Path, name: &str) -> anyhow::Result<String> { | ||||
|     let modules_alias = module_dir.join("modules.alias"); | ||||
|     output!( | ||||
|         quiet, | ||||
|         "opening modules.alias file: {}", | ||||
|         modules_alias.display() | ||||
|     ); | ||||
|     let alias_file = File::open(&modules_alias) | ||||
|         .with_context(|| format!("open(): {}", modules_alias.display()))?; | ||||
|     let alias_file = std::io::BufReader::new(alias_file); | ||||
| 
 | ||||
|     for line in alias_file.lines() { | ||||
|         let line = line?; | ||||
|         if line.starts_with("alias ") { | ||||
|             let mut parts = line.split_whitespace(); | ||||
|             let prefix = parts.next(); | ||||
|             if prefix != Some("alias") { | ||||
|                 bail!("alias line incorrect prefix: {}", line); | ||||
|             } | ||||
|             let alias = parts | ||||
|                 .next() | ||||
|                 .with_context(|| format!("alias line missing alias: {}", line))?; | ||||
|             let module = parts | ||||
|                 .next() | ||||
|                 .with_context(|| format!("alias line missing module: {}", line))?; | ||||
|             if parts.next().is_some() { | ||||
|                 bail!("alias line has too many parts: {}", line); | ||||
|             } | ||||
|             if alias == name { | ||||
|                 return Ok(module.to_string()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     bail!("alias not found: {}", name) | ||||
| } | ||||
					Loading…
					
					
				
		Reference in New Issue