Skip to content

Commit

Permalink
feat: Refactor init into test-distro
Browse files Browse the repository at this point in the history
The init module contains a small init system for running our integration
tests against a kernel. While we don't need a full-blown linux distro,
we do need some utilities.

Once such utility is `modprobe` which allows us to load kernel modules.
Rather than create a new module for this utility, I've instead
refactored `init` into `test-distro` which is a module that contains
multiple binaries.

The xtask code has been adjusted to ensure these binaries are inserted
into the correct places in our cpio archive, as well as bringing in the
kernel modules.

Signed-off-by: Dave Tucker <[email protected]>
  • Loading branch information
dave-tucker committed Feb 4, 2025
1 parent 921e457 commit f4cd4f9
Show file tree
Hide file tree
Showing 12 changed files with 418 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ jobs:
run: |
set -euxo pipefail
find test/.tmp -name '*.deb' -print0 | xargs -t -0 -I {} \
sh -c "dpkg --fsys-tarfile {} | tar -C test/.tmp --wildcards --extract '*vmlinuz*' --file -"
sh -c "dpkg --fsys-tarfile {} | tar -C test/.tmp --wildcards --extract '*vmlinuz*' '**/modules/*' --file -"
- name: Run local integration tests
if: runner.os == 'Linux'
Expand Down
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [
"aya-log-parser",
"aya-obj",
"aya-tool",
"init",
"test-distro",
"test/integration-common",
"test/integration-test",
"xtask",
Expand All @@ -33,7 +33,7 @@ default-members = [
"aya-log-parser",
"aya-obj",
"aya-tool",
"init",
"test-distro",
"test/integration-common",
# test/integration-test is omitted; including it in this list causes `cargo test` to run its
# tests, and that doesn't work unless they've been built with `cargo xtask`.
Expand Down Expand Up @@ -73,6 +73,7 @@ diff = { version = "0.1.13", default-features = false }
env_logger = { version = "0.11", default-features = false }
epoll = { version = "4.3.3", default-features = false }
futures = { version = "0.3.28", default-features = false }
glob = { version = "0.3.0", default-features = false }
hashbrown = { version = "0.15.0", default-features = false }
indoc = { version = "2.0", default-features = false }
libc = { version = "0.2.105", default-features = false }
Expand All @@ -99,8 +100,10 @@ test-log = { version = "0.2.13", default-features = false }
testing_logger = { version = "0.1.1", default-features = false }
thiserror = { version = "2.0.3", default-features = false }
tokio = { version = "1.24.0", default-features = false }
walkdir = { version = "2", default-features = false }
which = { version = "7.0.0", default-features = false }
xdpilone = { version = "1.0.5", default-features = false }
xz2 = { version = "0.1.7", default-features = false }

[profile.release.package.integration-ebpf]
debug = 2
Expand Down
13 changes: 0 additions & 13 deletions init/Cargo.toml

This file was deleted.

37 changes: 37 additions & 0 deletions test-distro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "test-distro"
version = "0.1.0"
publish = false
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
edition.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"] }
object = { workspace = true, features = ["elf", "read_core", "std"] }
clap = { workspace = true, default-features = true, features = ["derive"] }
nix = { workspace = true, features = [
"user",
"fs",
"mount",
"reboot",
"kmod",
"feature",
] }
glob = { workspace = true }
xz2 = { workspace = true }
walkdir = { workspace = true }
95 changes: 95 additions & 0 deletions test-distro/src/depmod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! 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 as _, Write as _},
};

use object::{Object, ObjectSection, ObjectSymbol};
use test_distro::resolve_modules_dir;
use walkdir::WalkDir;

fn main() {
let modules_dir = resolve_modules_dir().expect("Failed to resolve modules dir");

let output = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(modules_dir.join("modules.alias"))
.expect("Failed to open modules.alias file");

for entry in WalkDir::new(modules_dir) {
let entry = entry.expect("Failed to read entry");
if entry.file_type().is_file() {
let path = entry.path();
if let Some(extension) = path.extension() {
if extension != "ko" && extension != "xz" {
continue;
}
let mut f = File::open(path).expect("Failed to open module file");
let mut contents: Vec<u8> = Vec::new();
f.read_to_end(&mut contents).expect("read");

if extension == "xz" {
let mut decompressed = Vec::new();
xz2::read::XzDecoder::new(&contents[..])
.read_to_end(&mut decompressed)
.expect("Failed to decompress module");
contents = decompressed;
}

let obj =
object::read::File::parse(&*contents).expect("Failed to parse object file");

let module_name = path.file_stem().unwrap().to_string_lossy();
// Remove the .ko extension if this was compressed
let module_name = module_name.replace(".ko", "");

let (section_idx, data) = obj
.sections()
.filter_map(|s| {
if let Ok(name) = s.name() {
if name == ".modinfo" {
if let Ok(data) = s.data() {
return Some((s.index(), data));
}
}
}
None
})
.next()
.expect("Failed to find .modinfo section");

let aliases = obj
.symbols()
.filter_map(|s| {
if let Ok(name) = s.name() {
if name.contains("alias") && s.section_index() == Some(section_idx) {
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).ok()?;
let sym_str = cstr.to_string_lossy();
let alias = sym_str.replace("alias=", "");
return Some(alias);
}
}
None
})
.collect::<Vec<_>>();

let mut f = BufWriter::new(&output);
for alias in aliases {
f.write_all(format!("alias {} {}\n", alias, module_name).as_bytes())
.expect("write");
}
f.flush().expect("flush");
}
}
}
}
26 changes: 25 additions & 1 deletion init/src/main.rs → test-distro/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ fn run() -> anyhow::Result<()> {
data: None,
target_mode: Some(RXRXRX),
},
Mount {
source: "dev",
target: "/dev",
fstype: "devtmpfs",
flags: nix::mount::MsFlags::empty()
| nix::mount::MsFlags::MS_NOSUID
| nix::mount::MsFlags::MS_NOEXEC
| nix::mount::MsFlags::MS_RELATIME,
data: Some("size=10m,nr_inodes=248418,mode=755"),
target_mode: None,
},
Mount {
source: "sysfs",
target: "/sys",
Expand Down Expand Up @@ -120,6 +131,20 @@ fn run() -> anyhow::Result<()> {
})
.collect::<Vec<_>>();

let modules_alias = std::path::Path::new("/lib/modules/modules.alias");
assert!(!modules_alias.exists(), "modules.alias exists!");

// Run depmod to build the modules.alias file.
// NOTE: This is SLOOOOW. Making this faster is a future optimization.
// This is required since the kernel may ask for a module to be loaded in
// using one of the registered aliases. Aliases can't be easily derived from
// any means other than reading them from the modules directly.
std::process::Command::new("/sbin/depmod")
.status()
.with_context(|| "failed to execute /sbin/depmod")?;

assert!(modules_alias.exists(), "modules.alias does not exist!");

// Iterate files in /bin.
let read_dir = std::fs::read_dir("/bin").context("read_dir(/bin) failed")?;
let errors = read_dir
Expand All @@ -128,7 +153,6 @@ fn run() -> anyhow::Result<()> {
let path = entry.path();
let status = std::process::Command::new(&path)
.args(&args)
.env("RUST_LOG", "debug")
.status()
.with_context(|| format!("failed to execute {}", path.display()))?;

Expand Down
24 changes: 24 additions & 0 deletions test-distro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::path::PathBuf;

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 mut modules_dir = PathBuf::from("/lib/modules");

if !modules_dir.join("kernel").exists() {
let utsname = uname().expect("Failed to get uname");
let release = utsname.release();
if modules_dir.join(release).exists() {
modules_dir = modules_dir.join(release);
} else {
anyhow::bail!(
"No kernel modules found for release: {}",
release.to_string_lossy()
);
}
}
Ok(modules_dir)
}
Loading

0 comments on commit f4cd4f9

Please sign in to comment.