Compare commits

..
3 Commits
Author SHA1 Message Date
marvin 5b80885207 fix: validate image name before building registry path
CI / build (pull_request) Successful in 10s
CI / build (push) Successful in 10s
The user-supplied image/name argument was interpolated unsanitized into
registry/{name}/initrd, so a name containing '../' (e.g. 'slim run
../../.ssh/authorized_keys') escaped the XDG data dir and let slim
create directories and read/write files at attacker-chosen locations.

validate_image_name now rejects empty names, '.', '..' and anything
outside [A-Za-z0-9._:-] at the registry_dir chokepoint used by both
build and run, plus a defense-in-depth containment check that the
resolved path stays under the registry root.

Fixes sec-2 from the code review.
2026-08-26 20:11:57 +00:00
hulthe d517ab6a1a Fix minor issues
CI / build (push) Successful in 10s
2026-08-26 21:45:26 +02:00
hulthe e3bd77379f Add lockfile
CI / build (push) Successful in 10s
2026-08-26 19:25:20 +02:00
3 changed files with 407 additions and 42 deletions
+1 -4
View File
@@ -1,4 +1 @@
/target/
/target/debug/
/target/release/
Cargo.lock
target
Generated
+368
View File
@@ -0,0 +1,368 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "cpio"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938e716cb1ade5d6c8f959c13a7248b889c07491fc7e41167c3afe20f8f0de1e"
[[package]]
name = "crc32fast"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
dependencies = [
"cfg-if",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "simd-adler32"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "slim"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"cpio",
"flate2",
"tempfile",
"walkdir",
"xdg",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "xdg"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5"
+38 -38
View File
@@ -1,11 +1,11 @@
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use flate2::write::GzEncoder;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Parser, Debug)]
#[command(name = "slim")]
#[command(about = "Build bootable initrd VMs from container images")]
struct Cli {
#[command(subcommand)]
@@ -56,7 +56,7 @@ fn validate_image_name(name: &str) -> Result<()> {
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | ':' | '-'));
if !valid {
anyhow::bail!(
bail!(
"invalid image name '{name}': must be non-empty and contain only \
alphanumerics, '.', '_', ':' and '-' (no path separators or '..')"
);
@@ -69,21 +69,21 @@ fn registry_dir(image: &str) -> Result<PathBuf> {
let base = xdg::BaseDirectories::with_prefix("slim-rs");
let initrd_path = base
.place_data_file(format!("registry/{image}/initrd"))
.context("Failed to resolve XDG data home")?;
.context("Failed to write to XDG_DATA_HOME")?;
let reg_dir = initrd_path
.parent()
.context("registry path has no parent")?
.to_path_buf();
.expect("data file has a parent")
.to_owned();
// Defense in depth: the constructed path must stay inside the registry.
let registry_root = base
.place_data_file("registry/.root-check")
.context("Failed to resolve XDG data home")?
.context("Failed to write to XDG_DATA_HOME")?
.parent()
.context("registry path has no parent")?
.to_path_buf();
.expect("data file has a parent")
.to_owned();
if !reg_dir.starts_with(&registry_root) {
anyhow::bail!(
bail!(
"invalid image name '{image}': registry path escapes {}",
registry_root.display()
);
@@ -118,7 +118,7 @@ fn mount_image(image: &str) -> Result<PathBuf> {
.context("Failed to run podman image mount")?;
let mount_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !output.status.success() || mount_path.is_empty() {
anyhow::bail!(
bail!(
"podman image mount failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
@@ -138,9 +138,7 @@ fn build_artifacts(image: &str, mount_path: &Path) -> Result<()> {
.status()
.context("Failed to check for /vmlinuz inside the image mount")?;
if !has_kernel.success() {
anyhow::bail!(
"Image missing required /vmlinuz. Place kernel at /vmlinuz in Containerfile."
);
bail!("Image missing required /vmlinuz. Place kernel at /vmlinuz in Containerfile.");
}
let reg_dir = registry_dir(image)?;
@@ -159,7 +157,7 @@ fn build_artifacts(image: &str, mount_path: &Path) -> Result<()> {
.status()
.context("Failed to copy vmlinuz out of the image mount")?;
if !cp_status.success() {
anyhow::bail!("Failed to copy vmlinuz to registry");
bail!("Failed to copy vmlinuz to registry");
}
println!("Copied vmlinuz -> {}", vmlinuz_dst.display());
@@ -175,7 +173,7 @@ fn build_artifacts(image: &str, mount_path: &Path) -> Result<()> {
.context("Failed to spawn the cpio pipeline")?;
let initrd_file = File::create(&initrd_path).context("Failed to create initrd file")?;
let mut encoder = flate2::write::GzEncoder::new(initrd_file, flate2::Compression::default());
let mut encoder = GzEncoder::new(initrd_file, flate2::Compression::default());
let cpio_stdout = cpio.stdout.take().context("cpio stdout was not piped")?;
let copied = std::io::copy(&mut std::io::BufReader::new(cpio_stdout), &mut encoder);
let stream_result = copied
@@ -191,7 +189,7 @@ fn build_artifacts(image: &str, mount_path: &Path) -> Result<()> {
}
if !cpio_status.success() {
let _ = fs::remove_file(&initrd_path);
anyhow::bail!("find/cpio pipeline failed with status {cpio_status}");
bail!("find/cpio pipeline failed with status {cpio_status}");
}
println!("Created initrd -> {}", initrd_path.display());
@@ -203,7 +201,7 @@ fn run(name: &str) -> Result<()> {
let vmlinuz_path = reg_dir.join("vmlinuz");
let initrd_path = reg_dir.join("initrd");
if !vmlinuz_path.exists() || !initrd_path.exists() {
anyhow::bail!(
bail!(
"Registry missing vmlinuz/initrd for '{}'. Run `slim build` first.",
name
);
@@ -214,34 +212,36 @@ fn run(name: &str) -> Result<()> {
vmlinuz_path.display(),
initrd_path.display()
);
let cmdline = [
"console=ttyS0,115200",
"root=/dev/ram0",
"rw",
"earlyprintk=serial",
"nokaslr",
"raid=noautodetect",
]
.join(" ");
// Launch with a 5-second timeout for quick smoke verification
let status = Command::new("timeout")
.args([
"5",
"qemu-system-x86_64",
"-m",
"256",
"-nographic",
"-kernel",
vmlinuz_path.to_str().unwrap(),
"-initrd",
initrd_path.to_str().unwrap(),
"-append",
"console=ttyS0",
])
let status = Command::new("qemu-system-x86_64")
.args(["-m", "1024M"])
.arg("-kernel")
.arg(vmlinuz_path.as_os_str())
.arg("-initrd")
.arg(initrd_path.as_os_str())
.args(["-append", &cmdline])
.args(["-nographic"])
.status()
.context("Failed to launch qemu-system-x86_64")?;
// `timeout` exits 124 when it kills QEMU after the 5s smoke-test
// window; any other non-zero status is a real failure.
match status.code() {
Some(0) => println!("QEMU exited cleanly."),
Some(124) => println!("QEMU timed out after 5s (expected during smoke boot)."),
Some(126) | Some(127) => {
anyhow::bail!("Failed to start qemu-system-x86_64 (is it installed and in PATH?)")
bail!("Failed to start qemu-system-x86_64 (is it installed and in PATH?)")
}
Some(code) => anyhow::bail!("QEMU exited with code {code}"),
None => anyhow::bail!("QEMU terminated by a signal"),
Some(code) => bail!("QEMU exited with code {code}"),
None => bail!("QEMU terminated by a signal"),
}
Ok(())
}