From c9eb520e7c49f9dbc04bad06a471d6e421eda86a Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 1 Jun 2026 17:22:09 +0200 Subject: [PATCH] priv: WriteNspawnFlags takes Vec instead of raw flag strings --- hive-c0re/src/priv_client.rs | 8 +++++--- hive-priv/src/main.rs | 37 +++++++++++++++++++----------------- hive-sh4re/src/priv_proto.rs | 17 +++++++++++++---- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs index cd5fa42b..14abf071 100644 --- a/hive-c0re/src/priv_client.rs +++ b/hive-c0re/src/priv_client.rs @@ -7,7 +7,7 @@ //! a persistent connection. use anyhow::{Context as _, Result, bail}; -use hive_sh4re::priv_proto::{PRIV_SOCK, PrivRequest, PrivResponse}; +use hive_sh4re::priv_proto::{PRIV_SOCK, BindMount, PrivRequest, PrivResponse}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::UnixStream; @@ -59,13 +59,15 @@ pub async fn list_containers() -> Result { Ok(stdout) } -pub async fn write_nspawn_flags(container: &str, extra_nspawn_flags: &[&str]) -> Result<()> { +pub async fn write_nspawn_flags(container: &str, binds: &[BindMount]) -> Result<()> { ok(call(&PrivRequest::WriteNspawnFlags { container: container.to_owned(), - extra_nspawn_flags: extra_nspawn_flags.iter().map(|s| s.to_string()).collect(), + binds: binds.to_vec(), }).await?) } +pub use hive_sh4re::priv_proto::BindMount; + pub async fn write_resource_limits( container: &str, memory_max: &str, diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 60fc750e..1a916e1c 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -20,7 +20,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context as _, Result, bail}; -use hive_sh4re::priv_proto::{AGENT_PREFIX, MANAGER_NAME, META_DIR, PRIV_SOCK, SIBLING_CONTAINERS, PrivRequest, PrivResponse}; +use hive_sh4re::priv_proto::{AGENT_PREFIX, MANAGER_NAME, META_DIR, PRIV_SOCK, SIBLING_CONTAINERS, BindMount, PrivRequest, PrivResponse}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; use tokio::process::Command; @@ -173,12 +173,13 @@ async fn exec(req: PrivRequest) -> Result<(String, String)> { PrivRequest::ListContainers => container_run(&["list"]).await, - PrivRequest::WriteNspawnFlags { ref container, ref extra_nspawn_flags } => { + PrivRequest::WriteNspawnFlags { ref container, ref binds } => { validate_container_system_name(container)?; - for flag in extra_nspawn_flags { - validate_nspawn_flag(flag)?; + for bind in binds { + validate_bind_path(&bind.host_path)?; + validate_bind_path(&bind.container_path)?; } - write_nspawn_flags(container, extra_nspawn_flags)?; + write_nspawn_flags(container, binds)?; Ok((String::new(), String::new())) } @@ -352,17 +353,15 @@ fn agent_flake_ref(name: &str) -> String { format!("{META_DIR}#{name}") } -/// Validate one nspawn flag entry. Must be non-empty and contain no -/// ASCII whitespace (would split the entry when the start script -/// expands `$EXTRA_NSPAWN_FLAGS` unquoted), double-quotes (would -/// break the `EXTRA_NSPAWN_FLAGS="..."` conf line), or null bytes. -fn validate_nspawn_flag(flag: &str) -> Result<()> { - if flag.is_empty() - || flag.bytes().any(|b| b == 0 || b == b'"' || b.is_ascii_whitespace()) +/// Validate a bind-mount path: must be absolute, non-empty, and contain +/// no newlines, null bytes, or double-quotes (which would break the +/// `EXTRA_NSPAWN_FLAGS="..."` conf line format). +fn validate_bind_path(path: &str) -> Result<()> { + if path.is_empty() + || !path.starts_with('/') + || path.bytes().any(|b| b == 0 || b == b'\n' || b == b'"') { - bail!( - "invalid nspawn flag {flag:?}: must be non-empty and contain no whitespace, double-quotes, or null bytes" - ); + bail!("invalid bind path {path:?}: must be an absolute path with no newlines, null bytes, or double-quotes"); } Ok(()) } @@ -371,7 +370,7 @@ fn validate_nspawn_flag(flag: &str) -> Result<()> { /// vars (`PRIVATE_NETWORK`, `HOST_ADDRESS*`, `LOCAL_ADDRESS*`, `HOST_BRIDGE`, /// `EXTRA_NSPAWN_FLAGS`), forces `PRIVATE_NETWORK=0` and blank network vars, /// then appends `EXTRA_NSPAWN_FLAGS=""`. -fn write_nspawn_flags(container: &str, extra_nspawn_flags: &[String]) -> Result<()> { +fn write_nspawn_flags(container: &str, binds: &[BindMount]) -> Result<()> { let path = format!("/etc/nixos-containers/{container}.conf"); let original = std::fs::read_to_string(&path) .with_context(|| format!("read {path}"))?; @@ -398,7 +397,11 @@ fn write_nspawn_flags(container: &str, extra_nspawn_flags: &[String]) -> Result< out.push_str("HOST_ADDRESS6=\n"); out.push_str("LOCAL_ADDRESS6=\n"); out.push_str("HOST_BRIDGE=\n"); - let flags_joined = extra_nspawn_flags.join(" "); + let flags: Vec = binds.iter().map(|b| { + let flag = if b.read_only { "--bind-ro" } else { "--bind" }; + format!("{flag}={}:{}", b.host_path, b.container_path) + }).collect(); + let flags_joined = flags.join(" "); out.push_str(&format!("EXTRA_NSPAWN_FLAGS=\"{flags_joined}\"\n")); std::fs::write(&path, out).with_context(|| format!("write {path}")) } diff --git a/hive-sh4re/src/priv_proto.rs b/hive-sh4re/src/priv_proto.rs index 63cf028a..37ebf686 100644 --- a/hive-sh4re/src/priv_proto.rs +++ b/hive-sh4re/src/priv_proto.rs @@ -22,6 +22,16 @@ pub const SIBLING_CONTAINERS: &[&str] = &["hive-forge", "hive-matrix", "hive-gat /// `{META_DIR}#{name}`, derived by `hive-priv` — never passed over the wire. pub const META_DIR: &str = "/var/lib/hyperhive/meta"; +/// One bind-mount entry for `WriteNspawnFlags`. +/// hive-priv constructs `--bind=:` (or `--bind-ro=`) +/// and validates both paths before writing the conf file. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BindMount { + pub host_path: String, + pub container_path: String, + pub read_only: bool, +} + /// A request to the privileged helper. /// /// Wire format: one JSON object per line over `/run/hive/priv.sock`. @@ -59,10 +69,9 @@ pub enum PrivRequest { // --- Config file writes --- /// Update `/etc/nixos-containers/.conf`: strip network-isolation - /// vars, force `PRIVATE_NETWORK=0`, and set `EXTRA_NSPAWN_FLAGS`. - /// Each entry in `extra_nspawn_flags` is one flag (e.g. `"--bind=/path"`); - /// hive-priv validates and space-joins them. Written by `lifecycle::set_nspawn_flags`. - WriteNspawnFlags { container: String, extra_nspawn_flags: Vec }, + /// vars, force `PRIVATE_NETWORK=0`, and set `EXTRA_NSPAWN_FLAGS` from the + /// provided bind-mount list. Written by `lifecycle::set_nspawn_flags`. + WriteNspawnFlags { container: String, binds: Vec }, /// Write `/run/systemd/system/container@.service.d/hyperhive-limits.conf` /// with `[Service]\nMemoryMax=\nCPUQuota=\n`.