//! **The daemon-less OCI runtime backend — youki-core (`libcontainer`).**
//!
//! Where [`crate::container`] drives a container over the podman/Docker REST socket
//! (`bollard` → a *running daemon*), this backend runs a container **directly**: it
//! links youki's **`libcontainer`** crate — the pure-Rust low-level OCI runtime, a
//! `runc` alternative — and creates the Linux namespaces + cgroups-v2 and execs the
//! entrypoint *in-process*. **No daemon. No CLI.** This is the in-appliance engine
//! from Gemini's revised korp design: korp-server (static PID-1) links this and, at
//! boot, unpacks an OCI image's layers into a bundle rootfs, generates a
//! `config.json`, and drives the create/start/kill/delete lifecycle over cgroups-v2.
//!
//! **Zero-shell holds by construction.** The only place a runc-style runtime would
//! normally shell out is the rootless uid/gid map: youki execs `newuidmap`/`newgidmap`
//! *only* for a multi-line / non-self mapping. This backend authors a **single-line**
//! map that sends container-root straight to the caller's own uid
//! ([`Linux::rootless`]), which youki writes directly to `/proc/self/{uid,gid}_map` —
//! no helper binary. And any boot-time shell-out from inside a `scratch` bundle is an
//! instant ENOENT (there is no `/bin/sh`), the distroless correctness proof.
//!
//! Rootless caveat, stated honestly: cgroup delegation for a rootless container comes
//! from the caller's **systemd user session** — libcontainer forces the systemd cgroup
//! driver whenever a user namespace is present, and creates a transient scope under
//! `user@UID.service` over the **session D-Bus** (a pure-Rust dbus client, still no
//! CLI). So a rootless youki run needs: a kernel with unprivileged user namespaces, a
//! cgroups-v2 hierarchy with a delegated `user@UID.service` subtree, and `subuid`/`subgid`
//! entries. On a box without those, [`YoukiRuntime::create`] fails with a clear error
//! and the caller (test) loud-skips — it never fakes a start.
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use crate::{Error, Result};
use libcontainer::container::builder::ContainerBuilder;
use libcontainer::container::{Container, ContainerStatus};
use libcontainer::oci_spec::runtime::{Linux, ProcessBuilder, RootBuilder, Spec};
use libcontainer::signal::Signal;
use libcontainer::syscall::syscall::SyscallType;
/// The lifecycle state of a youki-run container, projected off libcontainer's
/// [`ContainerStatus`] into the same shape draupnir's other backends speak.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum YoukiState {
/// Created but not yet started (init process parked at the start barrier).
Created,
/// The entrypoint is running.
Running,
/// The container has exited / been stopped.
Stopped,
/// Paused (cgroup-frozen) — surfaced for completeness; this backend does not pause.
Paused,
/// Transient creating state.
Creating,
}
impl From<ContainerStatus> for YoukiState {
fn from(s: ContainerStatus) -> Self {
match s {
ContainerStatus::Creating => YoukiState::Creating,
ContainerStatus::Created => YoukiState::Created,
ContainerStatus::Running => YoukiState::Running,
ContainerStatus::Stopped => YoukiState::Stopped,
ContainerStatus::Paused => YoukiState::Paused,
}
}
}
/// A host **bind mount** into the container (`source` on the host → `destination`
/// inside the rootfs). The daemon-less analogue of a `-v host:container` — used, among
/// other things, to hand the container a directory it drops a **readiness sentinel**
/// into, which the host reads back (the youki equivalent of the korp-kvm `BOOT-OK`).
#[derive(Debug, Clone)]
pub struct BindMount {
/// Absolute host path to expose.
pub source: PathBuf,
/// Absolute path the mount appears at inside the container rootfs.
pub destination: String,
/// `true` → mount read-only; `false` → read-write.
pub read_only: bool,
}
/// A minimal container definition this backend turns into an OCI **bundle**
/// (`rootfs/` + `config.json`) and runs. Deliberately small — the fields youki needs
/// to create namespaces and exec an entrypoint rootless — not a re-modelling of the
/// whole OCI runtime spec.
#[derive(Debug, Clone)]
pub struct YoukiSpec {
/// The entrypoint argv, absolute-path first element (`["/bin/busybox","sh","-c",…]`).
pub args: Vec<String>,
/// Process environment as `KEY=VALUE`.
pub env: Vec<String>,
/// Working directory inside the container (defaults to `/`).
pub cwd: String,
/// Extra host bind mounts (e.g. a sentinel/readiness dir).
pub binds: Vec<BindMount>,
}
impl YoukiSpec {
/// A spec that runs `args` with a default `/`-cwd, a sane `PATH`, and no binds.
pub fn new(args: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
args: args.into_iter().map(Into::into).collect(),
env: vec!["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into()],
cwd: "/".into(),
binds: Vec::new(),
}
}
/// Attach a host bind mount (builder style).
pub fn with_bind(
mut self,
source: impl Into<PathBuf>,
destination: impl Into<String>,
read_only: bool,
) -> Self {
self.binds.push(BindMount {
source: source.into(),
destination: destination.into(),
read_only,
});
self
}
}
/// The daemon-less OCI runtime. Holds the youki **state root** (where libcontainer
/// keeps each container's `state.json` + notify socket) — one per process is fine.
#[derive(Debug, Clone)]
pub struct YoukiRuntime {
root: PathBuf,
}
impl YoukiRuntime {
/// A runtime rooted at `root` (created on demand). Each container's state lands in
/// `root/<id>/`.
pub fn with_root(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
/// A runtime rooted under `$XDG_RUNTIME_DIR/draupnir-youki` (the natural rootless
/// home — a tmpfs the caller owns), falling back to `/tmp` when unset.
pub fn new() -> Self {
let base = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into());
Self::with_root(PathBuf::from(base).join("draupnir-youki"))
}
/// The caller's effective uid — the host side of the single-line rootless id-map
/// (container-root → this uid, so youki writes `uid_map` directly, no `newuidmap`).
fn euid() -> u32 {
rustix::process::geteuid().as_raw()
}
/// The caller's effective gid — the host side of the single-line rootless gid-map.
fn egid() -> u32 {
rustix::process::getegid().as_raw()
}
/// **Unpack an OCI image's layer tarballs into a bundle rootfs** — the
/// image → bundle step of the daemon-less path. Each entry in `layers` is a
/// layer blob (`tar`, optionally gzip-compressed — auto-detected by the gzip magic)
/// applied in order over `rootfs`, exactly as an OCI image is assembled. Pure-Rust
/// (`tar` + `flate2`); no `podman pull`/`umoci`/`skopeo`. (OCI whiteouts are not yet
/// honoured — additive layers only; enough for a scratch/single-layer base.)
pub fn unpack_oci_layers(rootfs: &Path, layers: &[PathBuf]) -> Result<()> {
std::fs::create_dir_all(rootfs)
.map_err(|e| Error::Backend(format!("create rootfs {}: {e}", rootfs.display())))?;
for layer in layers {
let bytes = std::fs::read(layer)
.map_err(|e| Error::Backend(format!("read layer {}: {e}", layer.display())))?;
// gzip magic `1f 8b` → decompress; else treat as a plain tar.
let is_gzip = bytes.len() >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b;
let unpack = |reader: Box<dyn std::io::Read>| -> Result<()> {
let mut ar = tar::Archive::new(reader);
ar.set_preserve_permissions(true);
ar.unpack(rootfs).map_err(|e| {
Error::Backend(format!("unpack layer {} into rootfs: {e}", layer.display()))
})
};
if is_gzip {
unpack(Box::new(flate2::read::GzDecoder::new(
std::io::Cursor::new(bytes),
)))?;
} else {
unpack(Box::new(std::io::Cursor::new(bytes)))?;
}
}
Ok(())
}
/// **Author the bundle's `config.json`** for a **rootless** run of `spec` over the
/// rootfs at `bundle/rootfs`. Builds an OCI runtime [`Spec`] with the default mount
/// set (proc/dev/sys/…), the process argv/env/cwd, any host bind mounts, and the
/// single-line rootless uid/gid map + a fresh user namespace ([`Linux::rootless`]).
/// Writes it to `bundle/config.json`. Pure — no privilege, no runtime — so the
/// bundle can be inspected/tested without starting anything.
pub fn write_bundle_config(bundle: &Path, spec: &YoukiSpec) -> Result<()> {
use libcontainer::oci_spec::runtime::{Mount, MountBuilder};
if spec.args.is_empty() {
return Err(Error::Spec("youki spec needs a non-empty argv".into()));
}
let mut oci = Spec::default();
let process = ProcessBuilder::default()
.args(spec.args.clone())
.env(spec.env.clone())
.cwd(spec.cwd.clone())
.build()
.map_err(|e| Error::Spec(format!("build process spec: {e}")))?;
let root = RootBuilder::default()
.path("rootfs")
.readonly(false)
.build()
.map_err(|e| Error::Spec(format!("build root spec: {e}")))?;
// Rootless linux config: single-line id maps (container-root → caller uid/gid,
// size 1) + a fresh user namespace, network namespace dropped (share the host
// net so no bridge/netns setup is needed). This is the zero-shell map youki
// writes directly to /proc/self/{uid,gid}_map.
let linux = Linux::rootless(Self::euid(), Self::egid());
// Sanitize the default mount set for a **single-line rootless** map: the OCI
// default `/dev/pts` mount carries `gid=5` (the host `tty` group) and `mode=…`
// options, but only container-uid/gid 0 is mapped here, so libcontainer rejects
// any `uid=`/`gid=` option naming an id that isn't in the map ("invalid spec for
// new user namespace container"). Drop exactly those unmapped `uid=`/`gid=`
// options (id != 0) — the same adjustment youki's own rootless spec makes —
// leaving every other mount and option byte-identical.
let mut mounts: Vec<Mount> = oci.mounts().clone().unwrap_or_default();
for m in &mut mounts {
if let Some(opts) = m.options().clone() {
let kept: Vec<String> = opts
.into_iter()
.filter(|opt| {
let unmapped_id =
|v: &str| v.parse::<u32>().map(|n| n != 0).unwrap_or(false);
if let Some(v) = opt.strip_prefix("uid=") {
return !unmapped_id(v);
}
if let Some(v) = opt.strip_prefix("gid=") {
return !unmapped_id(v);
}
true
})
.collect();
m.set_options(Some(kept));
}
}
// Append the caller's bind mounts onto the default mount set.
for b in &spec.binds {
let opts = if b.read_only {
vec!["rbind".to_string(), "ro".to_string()]
} else {
vec!["rbind".to_string(), "rw".to_string()]
};
let m = MountBuilder::default()
.destination(PathBuf::from(&b.destination))
.typ("bind")
.source(b.source.clone())
.options(opts)
.build()
.map_err(|e| Error::Spec(format!("build bind mount {}: {e}", b.destination)))?;
mounts.push(m);
}
oci.set_process(Some(process))
.set_root(Some(root))
.set_linux(Some(linux))
.set_hostname(Some("draupnir-youki".to_string()))
.set_mounts(Some(mounts));
std::fs::create_dir_all(bundle)
.map_err(|e| Error::Backend(format!("create bundle {}: {e}", bundle.display())))?;
oci.save(bundle.join("config.json")).map_err(|e| {
Error::Backend(format!("write config.json into {}: {e}", bundle.display()))
})?;
Ok(())
}
/// **Create the container** from an already-authored `bundle` (`rootfs/` +
/// `config.json`) under id `id`, leaving it in the *created* state (init process
/// parked at the start barrier). This is where the namespaces + cgroups are set up;
/// a missing kernel capability (no unprivileged userns, no cgroup delegation) fails
/// here with a clear [`Error::Backend`] — never a fake create.
pub fn create(&self, id: &str, bundle: &Path) -> Result<Container> {
std::fs::create_dir_all(&self.root).map_err(|e| {
Error::Backend(format!(
"create youki state root {}: {e}",
self.root.display()
))
})?;
// **Idempotent boot** (owner): force-remove any stale container of the same id
// BEFORE creating a fresh one — a crashed/leftover prior run leaves a state dir
// that makes `build()` fail with "container already exists", the daemon-less youki
// twin of the OCI engine's rm-f-before-create. A still-running init is killed +
// reaped, then the state dir is swept. Best-effort: never blocks a fresh create.
self.force_cleanup(id);
ContainerBuilder::new(id.to_string(), SyscallType::default())
.with_root_path(self.root.clone())
.map_err(|e| Error::Backend(format!("youki root path {}: {e}", self.root.display())))?
.as_init(bundle)
.with_systemd(false)
.with_detach(true)
.build()
.map_err(|e| Error::Backend(format!("youki create `{id}`: {e}")))
}
/// Force-remove any stale container of `id` in the state root — the idempotent-boot
/// sweep [`create`](Self::create) runs first. Loads the existing container (if any) and
/// `delete(force=true)`s it (kills a still-running init + reaps its state), then removes
/// the leftover state dir as a belt-and-braces. Every step is ignore-on-error so a
/// fresh create is never blocked by teardown trouble (matches the OCI engine, which
/// `remove_container(force)`s and ignores "not found").
fn force_cleanup(&self, id: &str) {
let dir = self.root.join(id);
if !dir.exists() {
return;
}
if let Ok(mut existing) = Container::load(dir.clone()) {
let _ = existing.delete(true);
}
let _ = std::fs::remove_dir_all(&dir);
}
/// **Start** a created container — release the init barrier so the entrypoint execs.
pub fn start(container: &mut Container) -> Result<()> {
container
.start()
.map_err(|e| Error::Backend(format!("youki start `{}`: {e}", container.id())))
}
/// The container's current [`YoukiState`] (refreshes libcontainer's cached status).
pub fn state(container: &mut Container) -> YoukiState {
let _ = container.refresh_status();
container.status().into()
}
/// **Kill** the container's init process with `signal` (e.g. `SIGKILL`/`SIGTERM`).
/// A no-op-ish `Ok(())` when it is already stopped.
pub fn kill(container: &mut Container, signal: Signal) -> Result<()> {
container
.kill(signal, true)
.map_err(|e| Error::Backend(format!("youki kill `{}`: {e}", container.id())))
}
/// **Delete** the container, force-removing it (kills a still-running init first)
/// and reaping its state dir. Idempotent-ish: best-effort teardown.
pub fn delete(container: &mut Container) -> Result<()> {
container
.delete(true)
.map_err(|e| Error::Backend(format!("youki delete `{}`: {e}", container.id())))
}
/// **Create + start + wait-for-readiness + tear down — the youki `BOOT-OK` e2e in
/// one call.** Authors nothing (the caller owns the bundle); it drives the real
/// lifecycle: [`create`](Self::create) → [`start`](Self::start) → poll `ready` until
/// it returns `true` or `timeout` elapses → [`delete`](Self::delete). `ready` is the
/// host-side readiness probe (e.g. "the sentinel file the container wrote now says
/// `CONTAINER-READY`" or "the TCP port is open") — the container equivalent of
/// reading `KORP-APPLIANCE BOOT-OK` off a serial console. Returns the observed state
/// at the moment readiness held (or the last state before timeout).
///
/// Always tears the container down before returning (even on a readiness timeout),
/// so a youki proof leaves nothing behind. A create/start failure surfaces as `Err`.
pub fn run_until_ready(
&self,
id: &str,
bundle: &Path,
timeout: Duration,
mut ready: impl FnMut() -> bool,
) -> Result<YoukiState> {
let mut container = self.create(id, bundle)?;
if let Err(e) = Self::start(&mut container) {
let _ = Self::delete(&mut container);
return Err(e);
}
let deadline = Instant::now() + timeout;
let mut last;
let mut became_ready;
loop {
became_ready = ready();
last = Self::state(&mut container);
// A short-lived entrypoint may already have exited cleanly; if it wrote its
// readiness signal before exiting, the `ready()` check above still caught it.
if became_ready || Instant::now() >= deadline {
break;
}
std::thread::sleep(Duration::from_millis(100));
}
let _ = Self::delete(&mut container);
crate::functional_status(
"draupnir/youki",
"run_until_ready",
became_ready,
&if became_ready {
format!("youki container `{id}` reached readiness (daemon-less, rootless)")
} else {
format!("youki container `{id}` never signalled readiness within {timeout:?}")
},
);
if became_ready {
Ok(last)
} else {
Err(Error::Backend(format!(
"youki container `{id}` did not signal readiness within {timeout:?} (last state {last:?})"
)))
}
}
}
impl Default for YoukiRuntime {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spec_new_defaults_are_sane() {
let s = YoukiSpec::new(["/bin/busybox", "true"]);
assert_eq!(s.args, vec!["/bin/busybox".to_string(), "true".to_string()]);
assert_eq!(s.cwd, "/");
assert!(s.env.iter().any(|e| e.starts_with("PATH=")));
assert!(s.binds.is_empty());
}
#[test]
fn empty_argv_is_rejected_before_any_bundle_write() {
let dir = std::env::temp_dir().join(format!("youki-cfg-test-{}", std::process::id()));
let spec = YoukiSpec {
args: vec![],
env: vec![],
cwd: "/".into(),
binds: vec![],
};
let err = YoukiRuntime::write_bundle_config(&dir, &spec).unwrap_err();
assert!(matches!(err, Error::Spec(_)));
}
#[test]
fn write_bundle_config_emits_a_rootless_config_json() {
// Pure bundle authoring — no container is created, so this runs anywhere.
let bundle = std::env::temp_dir().join(format!("youki-bundle-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&bundle);
let spec = YoukiSpec::new(["/bin/busybox", "sh", "-c", "true"])
.with_bind("/tmp", "/signal", false);
YoukiRuntime::write_bundle_config(&bundle, &spec).expect("author config.json");
let text = std::fs::read_to_string(bundle.join("config.json")).expect("config.json exists");
// The rootless proofs: a user namespace + a uid map back to our own uid, and the
// bind-mounted signal dir. (String-level assertions keep this dep-light.)
assert!(
text.contains("\"user\""),
"config.json declares a user namespace"
);
assert!(
text.contains("uidMappings"),
"config.json carries a rootless uid map"
);
assert!(
text.contains("/signal"),
"config.json carries the bind-mounted signal dir"
);
let _ = std::fs::remove_dir_all(&bundle);
}
#[test]
fn force_cleanup_sweeps_a_stale_state_dir_so_a_fresh_create_is_idempotent() {
// Daemon-free: a leftover state dir (a crashed prior run) must be swept before a
// fresh create — otherwise libcontainer's `build()` fails with "already exists".
// Container::load on a bogus dir returns Err; the belt-and-braces remove_dir_all is
// what actually reaps it → this goes RED if that removal is reverted.
let root = std::env::temp_dir().join(format!("youki-idem-root-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let rt = YoukiRuntime::with_root(root.clone());
let id = "korp-falkordb";
let stale = root.join(id);
std::fs::create_dir_all(&stale).unwrap();
std::fs::write(stale.join("state.json"), b"{stale}").unwrap();
assert!(stale.exists(), "the stale state dir exists before cleanup");
rt.force_cleanup(id);
assert!(
!stale.exists(),
"force_cleanup swept the stale state dir (idempotent boot)"
);
// Idempotent on a clean root too — a no-op, never an error.
rt.force_cleanup(id);
let _ = std::fs::remove_dir_all(&root);
}
}