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 5, 2025
1 parent e82253c commit c389190
Show file tree
Hide file tree
Showing 12 changed files with 497 additions and 60 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,13 @@ jobs:
- name: Extract debian kernels
run: |
set -euxo pipefail
# The wildcard '**/boot/*' extracts kernel images and config.
# The wildcard '**/modules/*' extracts kernel modules.
# Modules are required since not all parts of the kernel we want to
# test are built-in.
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 '**/boot/*' '**/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 }
122 changes: 122 additions & 0 deletions test-distro/src/depmod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! 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 _;
use clap::Parser;
use object::{Object, ObjectSection, ObjectSymbol};
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 } = Args::parse();

let modules_dir = if let Some(base_dir) = base_dir {
base_dir
} else {
resolve_modules_dir().context("Failed to resolve modules dir")?
};

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

let mut aliases = String::new();
for entry in WalkDir::new(modules_dir) {
let entry = entry.context("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 module_name = path
.file_stem()
.expect("a file with no file stem?")
.to_string_lossy()
.replace(".ko", "");
let mut f = File::open(path).context("Failed to open module file")?;
let stat = f.metadata().context("Failed to get metadata")?;
if extension == "xz" {
let mut decoder = XzDecoder::new(f);
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 aliases)
} else {
let mut buf = Vec::with_capacity(stat.len() as usize);
f.read_to_end(&mut buf)
.context("Failed to read module file")?;
read_aliases_from_module(&buf, &module_name, &mut aliases)
}
.with_context(|| {
format!("Failed to read aliases from module {}", path.display())
})?;
}
}
}
let mut f = BufWriter::new(&output);
f.write_all(aliases.as_bytes()).context("write")?;
f.flush().context("flush")?;
Ok(())
}

fn read_aliases_from_module(
contents: &[u8],
module_name: &str,
aliases: &mut String,
) -> Result<(), anyhow::Error> {
let obj = object::read::File::parse(contents).context("Failed to parse object file")?;

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()
.context("Failed to find .modinfo section")?;

obj.symbols().for_each(|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];
if let Ok(cstr) = std::ffi::CStr::from_bytes_with_nul(sym_data) {
let sym_str = cstr.to_string_lossy();
let alias = sym_str.replace("alias=", "");
aliases.push_str("alias ");
aliases.push_str(&alias);
aliases.push(' ');
aliases.push_str(module_name);
aliases.push('\n');
}
}
}
});
Ok(())
}
14 changes: 13 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,19 @@ 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,
// Borrowed from LinuxKit
// https://github.com/linuxkit/linuxkit/blob/master/pkg/init/cmd/rc.init/main.go#L190
data: Some("size=10m,nr_inodes=248418,mode=755"),
target_mode: None,
},
Mount {
source: "sysfs",
target: "/sys",
Expand Down Expand Up @@ -128,7 +141,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 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");
if modules_dir.exists() && modules_dir.is_dir() {
return Ok(modules_dir);
}

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

0 comments on commit c389190

Please sign in to comment.