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 6, 2025
1 parent a8c9778 commit 3ee8b86
Show file tree
Hide file tree
Showing 13 changed files with 527 additions and 68 deletions.
32 changes: 32 additions & 0 deletions .github/scripts/find_kernels.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash

set -euo pipefail

IMAGES=()
while IFS= read -r -d ''; do
IMAGES+=("$REPLY")
done < <(find test/.tmp -name 'vmlinuz-*' -print0)

MODULES=()
for image in ${IMAGES[@]}; do
image_name=$(basename ${image})
image_name=${image_name#"vmlinuz-"}
MODULES+=($(find test/.tmp -type d -ipath "*modules*" -name "${image_name#"vmlinux-"}" | head -n 1))
done

images_len=${#IMAGES[@]}
modules_len=${#MODULES[@]}

if [ "${images_len}" != "${modules_len}" ]; then
echo "IMAGES=${IMAGES[@]}"
echo "MODULES=${MODULES[@]}"
echo "ERROR! len images != len modules"
exit 1
fi

args=""
for (( i=0; i<${images_len}; i++ )); do
args+="-i ${IMAGES[$i]} -m ${MODULES[$i]} "
done

echo ${args}
16 changes: 12 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ jobs:
run: |
set -euxo pipefail
sudo apt update
sudo apt -y install lynx qemu-system-{arm,x86}
sudo apt -y install lynx qemu-system-{arm,x86} musl-tools
echo /usr/lib/llvm-15/bin >> $GITHUB_PATH
- name: Install prerequisites
Expand All @@ -246,6 +246,7 @@ jobs:
echo $(brew --prefix curl)/bin >> $GITHUB_PATH
echo $(brew --prefix gnu-tar)/libexec/gnubin >> $GITHUB_PATH
echo $(brew --prefix llvm)/bin >> $GITHUB_PATH
brew install filosottile/musl-cross/musl-cross
- uses: dtolnay/rust-toolchain@nightly
with:
Expand Down Expand Up @@ -303,8 +304,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 All @@ -313,8 +319,10 @@ jobs:
- name: Run virtualized integration tests
run: |
set -euxo pipefail
find test/.tmp -name 'vmlinuz-*' -print0 | xargs -t -0 \
cargo xtask integration-test vm --cache-dir test/.tmp --github-api-token ${{ secrets.GITHUB_TOKEN }}
ARGS=$(.github/scripts/find_kernels.sh)
cargo xtask integration-test vm --cache-dir test/.tmp \
--github-api-token ${{ secrets.GITHUB_TOKEN }} \
${ARGS}
# Provides a single status check for the entire build workflow.
# This is used for merge automation, like Mergify, since GH actions
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 }
117 changes: 117 additions & 0 deletions test-distro/src/depmod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! 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 } = 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:?}"))?;
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();
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).with_context(|| format!("failed to open: {path:?}"))?;
let stat = f
.metadata()
.with_context(|| format!("Failed to get metadata for {path:?}"))?;
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 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:?}"))?;
read_aliases_from_module(&buf, &module_name, &mut output)
}
.with_context(|| format!("Failed to read aliases from module {path:?}"))?;
}
}
}

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("not an 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("no .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=", "");
writeln!(output, "alias {} {}", alias, module_name).expect("write");
}
}
}
});
Ok(())
}
12 changes: 11 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: None,
target_mode: None,
},
Mount {
source: "sysfs",
target: "/sys",
Expand Down Expand Up @@ -128,7 +139,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
26 changes: 26 additions & 0 deletions test-distro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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!("{modules_dir:?} doesn't exist"))?;
if stat.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);
let stat = modules_dir
.metadata()
.with_context(|| format!("{modules_dir:?} doesn't exist"))?;
anyhow::ensure!(stat.is_dir(), "{modules_dir:?} is not a directory",);
Ok(modules_dir)
}
Loading

0 comments on commit 3ee8b86

Please sign in to comment.