priv: WriteNspawnFlags takes Vec<BindMount> instead of raw flag strings

This commit is contained in:
damocles 2026-06-01 17:22:09 +02:00 committed by mara
commit c9eb520e7c
3 changed files with 38 additions and 24 deletions

View file

@ -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<String> {
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,

View file

@ -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="<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<String> = 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}"))
}

View file

@ -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=<host_path>:<container_path>` (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/<container>.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<String> },
/// 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<BindMount> },
/// Write `/run/systemd/system/container@<container>.service.d/hyperhive-limits.conf`
/// with `[Service]\nMemoryMax=<memory_max>\nCPUQuota=<cpu_quota>\n`.