From 796eb77ae4faabfa4c1a480294ed67c8061218b6 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Wed, 10 Jun 2026 13:57:58 +0200 Subject: [PATCH 1/3] Cleanup pasta process on exit --- composefs-run/src/main.rs | 4 ++++ composefs-run/src/run.rs | 23 ++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/composefs-run/src/main.rs b/composefs-run/src/main.rs index e391ef6..1409523 100644 --- a/composefs-run/src/main.rs +++ b/composefs-run/src/main.rs @@ -30,6 +30,10 @@ pub(crate) struct CleanupArgs { /// Allocated container IP (for netavark teardown + IPAM release) #[clap(long)] container_ip: Option, + + /// PID of the pasta process to kill on cleanup + #[clap(long)] + pasta_pid: Option, } #[derive(Clone, Debug)] diff --git a/composefs-run/src/run.rs b/composefs-run/src/run.rs index 679771a..7540683 100644 --- a/composefs-run/src/run.rs +++ b/composefs-run/src/run.rs @@ -94,6 +94,7 @@ pub fn run( } let mut container_ip = None; + let mut pasta_pid = None; let netns_path = if network != NetworkMode::Host { Some(setup_netns(&bundle_dir)?) } else { @@ -109,7 +110,11 @@ pub fn run( } else if network == NetworkMode::Pasta && let Some(ref ns) = netns_path { - setup_pasta(ns, &cli.publish)?; + pasta_pid = Some(setup_pasta( + ns, + &bundle_dir.join("pasta.pid"), + &cli.publish, + )?); } // ── OCI spec + exec ──────────────────────────────────────────────── @@ -139,6 +144,7 @@ pub fn run( &network, netns_path.as_deref(), container_ip, + pasta_pid, )?; let config_json = serde_json::to_string_pretty(&spec)?; @@ -178,6 +184,9 @@ pub fn cleanup() -> Result<()> { ); let rootfs = dir.join("bundle/rootfs"); let _ = rustix::mount::unmount(&rootfs, rustix::mount::UnmountFlags::DETACH); + if let Some(pid) = args.pasta_pid { + unsafe { libc::kill(pid, libc::SIGTERM) }; + } let netns = dir.join("bundle/netns"); if netns.exists() { if let (Some(id), Some(ip)) = (&args.container_id, args.container_ip) { @@ -421,7 +430,7 @@ fn setup_netns(bundle_dir: &Path) -> Result { Ok(netns_path) } -fn setup_pasta(netns_path: &Path, publish: &[PortSpec]) -> Result<()> { +fn setup_pasta(netns_path: &Path, pid_file: &Path, publish: &[PortSpec]) -> Result { let mut pasta_cmd = Command::new("pasta"); pasta_cmd .arg("--config-net") @@ -429,6 +438,8 @@ fn setup_pasta(netns_path: &Path, publish: &[PortSpec]) -> Result<()> { .arg("169.254.1.1") .arg("--netns") .arg(netns_path) + .arg("--pid") + .arg(pid_file) .arg("--quiet"); if publish.is_empty() { @@ -449,7 +460,8 @@ fn setup_pasta(netns_path: &Path, publish: &[PortSpec]) -> Result<()> { String::from_utf8_lossy(&output.stderr) ); - Ok(()) + let pid_str = fs::read_to_string(pid_file).context("Reading pasta PID file")?; + pid_str.trim().parse::().context("Parsing pasta PID") } fn create_detached_tmpfs() -> Result { @@ -597,6 +609,7 @@ fn build_runtime_spec( network: &NetworkMode, netns_path: Option<&Path>, container_ip: Option, + pasta_pid: Option, ) -> Result { use std::collections::HashSet; @@ -995,6 +1008,10 @@ fn build_runtime_spec( hook_args.push("--container-ip".into()); hook_args.push(ip.to_string()); } + if let Some(pid) = pasta_pid { + hook_args.push("--pasta-pid".into()); + hook_args.push(pid.to_string()); + } let hook = HookBuilder::default() .path(self_exe) .args(hook_args) From 89670ae6758829ba2b2cfbe2947a59b77e39f89b Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Wed, 10 Jun 2026 14:13:11 +0200 Subject: [PATCH 2/3] Update README.md Add a note about this being PoC. Clean up the feature list. --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f10bfa7..5bbf4ef 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # composefs-run +**NOTE**: This is currently a proof of concept, and not intended for +production use + A minimal container runner that runs OCI containers directly from [composefs-rs](https://github.com/containers/composefs-rs) repositories using [crun](https://github.com/containers/crun), without @@ -14,12 +17,11 @@ tracking. - **Rootless mode**: FUSE-based composefs + unprivileged overlayfs (`userxattr`), user namespace with subuid/subgid mapping, [pasta](https://passt.top/) networking -- SELinux labeling with MCS category separation -- Seccomp profiles (default from containers-common, or from image labels) -- Minimal host state: transient overlay in `/var/tmp`, tmpfs-backed bundle, - no global database -- Default to transient overlays, persistent overlay via `--overlay-dir` -- OCI poststop hooks for automatic cleanup +- Minimal host state, containers are just child processes. There is no + equivalent of podman ps/rm etc. +- No detached mode, always assumes `-i`. +- By default, writable overlays are transient, stored in /var/tmp. + This can be overridden with `--overlay-dir` ### Quick start From aaa470d971dd89cdf92ace0f6ee4c883e56a08ea Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Wed, 10 Jun 2026 14:08:39 +0200 Subject: [PATCH 3/3] Drop detached mode This doesn't really make sense without global state like "podman ps/rm" etc. Also, it wasn't working because we never ran crun delete. --- composefs-run/src/main.rs | 8 +--- composefs-run/src/run.rs | 73 +++--------------------------- composefs-run/tests/integration.rs | 2 +- 3 files changed, 10 insertions(+), 73 deletions(-) diff --git a/composefs-run/src/main.rs b/composefs-run/src/main.rs index 1409523..65911f4 100644 --- a/composefs-run/src/main.rs +++ b/composefs-run/src/main.rs @@ -313,14 +313,10 @@ pub(crate) struct Cli { #[clap(short = 't', long)] tty: bool, - /// Keep stdin open - #[clap(short = 'i', long)] + /// Accepted for compatibility, ignored + #[clap(short = 'i', long, hide = true)] interactive: bool, - /// Write the container init PID to this file - #[clap(long)] - pidfile: Option, - /// Add a host device to the container (HOST_PATH[:CONTAINER_PATH[:PERMISSIONS]]) #[clap(long, value_parser = clap::value_parser!(DeviceSpec))] device: Vec, diff --git a/composefs-run/src/run.rs b/composefs-run/src/run.rs index 7540683..42518ff 100644 --- a/composefs-run/src/run.rs +++ b/composefs-run/src/run.rs @@ -150,23 +150,13 @@ pub fn run( let config_json = serde_json::to_string_pretty(&spec)?; fs::write(bundle_dir.join("config.json"), &config_json)?; - if cli.interactive || tty { - let err = Command::new("crun") - .arg("run") - .arg("--bundle") - .arg(bundle_dir) - .arg(container_id) - .exec(); - Err(err).context("Failed to exec crun") - } else { - let image_name = cli.image.as_deref().unwrap_or("container"); - run_detached( - &bundle_dir, - container_id, - image_name, - cli.pidfile.as_deref(), - ) - } + let err = Command::new("crun") + .arg("run") + .arg("--bundle") + .arg(bundle_dir) + .arg(container_id) + .exec(); + Err(err).context("Failed to exec crun") } /// OCI poststop hook: unmount and remove the container state directory. @@ -201,55 +191,6 @@ pub fn cleanup() -> Result<()> { Ok(()) } -fn journal_stream_fd(identifier: &str) -> Result { - use std::io::Write; - use std::os::unix::net::UnixStream; - - let mut stream = UnixStream::connect("/run/systemd/journal/stdout") - .context("Connecting to journal socket")?; - - write!(stream, "{identifier}\n\n6\n0\n0\n0\n0\n")?; - stream.flush()?; - - Ok(std::os::fd::OwnedFd::from(stream)) -} - -fn run_detached( - bundle_dir: &Path, - container_id: &str, - image_name: &str, - pidfile: Option<&Path>, -) -> Result<()> { - let journal_fd = - journal_stream_fd(&format!("cfsrun:{image_name}")).context("Creating journal stream fd")?; - let journal_fd2 = rustix::io::dup(&journal_fd)?; - - let pidfile_path = match pidfile { - Some(pf) => pf.to_owned(), - None => bundle_dir.join("container.pid"), - }; - - let status = Command::new("crun") - .arg("run") - .arg("--detach") - .arg("--bundle") - .arg(bundle_dir) - .arg("--pid-file") - .arg(&pidfile_path) - .arg(container_id) - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::from(std::fs::File::from(journal_fd))) - .stderr(std::process::Stdio::from(std::fs::File::from(journal_fd2))) - .status() - .context("Failed to run crun")?; - ensure!(status.success(), "crun run --detach failed: {status}"); - - let pid = fs::read_to_string(&pidfile_path).context("Reading PID file")?; - println!("{}", pid.trim()); - - Ok(()) -} - /// Mount a composefs image via FUSE (rootless). fn mount_rootfs_with_fuse( repo_path: &Path, diff --git a/composefs-run/tests/integration.rs b/composefs-run/tests/integration.rs index 273e3a3..7b69688 100644 --- a/composefs-run/tests/integration.rs +++ b/composefs-run/tests/integration.rs @@ -48,7 +48,7 @@ fn repo() -> &'static Path { fn cfsrun() -> Command { let mut cmd = Command::new(env!("CARGO_BIN_EXE_cfsrun")); - cmd.arg("--repo").arg(repo()).arg("-i"); + cmd.arg("--repo").arg(repo()); cmd }