Adds a boot flag choosing how virtiofsd is launched per --mount: - unshare (default, previous behavior): virtiofsd runs inside 'podman unshare' with --sandbox chroot, mapping guest UIDs into the invoking user's subuid range. - direct: virtiofsd runs as the invoking user with --sandbox none, mapping guest UIDs 1:1 to host UIDs. The direct mode is needed when the shared directory itself lives on a virtiofs/fuse mount that rejects subuid-range UIDs (e.g. nested boco): guest writes through the subuid mapping then fail with EINVAL, breaking anything that writes to shares as a non-root guest user (databases, agent workspaces). Direct mode also cannot use the chroot sandbox (unprivileged chroot() is not permitted), so it falls back to --sandbox none; served trees are then not symlink-escape-protected. Verified both modes boot and serve mounts (alpine guest).
This commit is contained in:
1 parent
8ad6754328
commit
1206728446
2 files changed
+79
-25
No files matched your search
+16
-6
@@ -4,10 +4,12 @@
|
||||
//! at build time) handles VM bootstrap; /boco/exec runs the container's
|
||||
//! CMD/ENTRYPOINT. Runtime overrides are delivered as `boco.cmd=<base64>`
|
||||
//! on the kernel cmdline. Host directories are shared into the guest via
|
||||
//! virtio-fs: one virtiofsd daemon (under `podman unshare` for rootless
|
||||
//! UID mapping) per `--mount`, plus a `-chardev socket` + `-device
|
||||
//! vhost-user-fs-pci` pair on the QEMU side. The guest mounts each share
|
||||
//! with `mount -t virtiofs <tag> <dest>` (see `boco.mount=` cmdline).
|
||||
//! virtio-fs: one virtiofsd daemon per `--mount`, plus a `-chardev socket`
|
||||
//! + `-device vhost-user-fs-pci` pair on the QEMU side. virtiofsd runs
|
||||
//! under `podman unshare` (rootless UID mapping) by default, or directly
|
||||
//! as the invoking user with `--mount-mode direct` (1:1 UID mapping).
|
||||
//! The guest mounts each share with `mount -t virtiofs <tag> <dest>`
|
||||
//! (see `boco.mount=` cmdline).
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use base64::Engine;
|
||||
@@ -17,7 +19,7 @@ use std::{fmt::Write as _, process::Command};
|
||||
use crate::{
|
||||
forward::PortForward,
|
||||
registry::{registry_base_dir, registry_dir},
|
||||
virtiofs::{self, VirtiofsShare},
|
||||
virtiofs::{self, MountMode, VirtiofsShare},
|
||||
};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
@@ -44,6 +46,13 @@ pub struct BootCmd {
|
||||
/// relative to the image's WORKINGDIR. Can be repeated.
|
||||
#[clap(long)]
|
||||
mount: Vec<String>,
|
||||
|
||||
/// How virtiofsd is launched: `unshare` runs it inside
|
||||
/// `podman unshare` (rootless UID mapping, guest UIDs map into the
|
||||
/// subuid range); `direct` runs it as the invoking user (guest UIDs
|
||||
/// map 1:1 to host UIDs — needed for nested virtiofs shares).
|
||||
#[clap(long, value_enum, default_value = "unshare")]
|
||||
mount_mode: MountMode,
|
||||
}
|
||||
|
||||
pub(crate) fn boot(
|
||||
@@ -53,6 +62,7 @@ pub(crate) fn boot(
|
||||
forward,
|
||||
cmd,
|
||||
mount,
|
||||
mount_mode,
|
||||
}: BootCmd,
|
||||
) -> Result<()> {
|
||||
let reg_dir = registry_dir(&name)?;
|
||||
@@ -108,7 +118,7 @@ pub(crate) fn boot(
|
||||
Some((h, g)) => (h, g.to_string()),
|
||||
None => (spec.as_str(), spec.to_string()),
|
||||
};
|
||||
let share = virtiofs::spawn_share(i, host_path, guest_path)?;
|
||||
let share = virtiofs::spawn_share(i, host_path, guest_path, mount_mode)?;
|
||||
let tag = share.tag.clone();
|
||||
let chardev = format!("char{i}");
|
||||
let socket = share
|
||||
|
||||
+63
-19
@@ -6,13 +6,21 @@
|
||||
//! hands the socket path to QEMU via `-chardev socket,path=...`, and
|
||||
//! cleans up when the VM exits.
|
||||
//!
|
||||
//! To serve files owned by arbitrary guest UIDs (e.g. when an image
|
||||
//! uses the `USER` directive) without running as root, virtiofsd is
|
||||
//! launched under `podman unshare` in a user namespace with
|
||||
//! `--sandbox chroot`. This reuses rootless podman's existing
|
||||
//! subuid/subgid mapping: guest UID 0 maps to the invoking user, and
|
||||
//! other guest UIDs map into the subuid range, so each guest UID
|
||||
//! becomes a distinct host UID.
|
||||
//! Launch modes ([`MountMode`]):
|
||||
//!
|
||||
//! * `unshare` (default): virtiofsd runs under `podman unshare` in a
|
||||
//! user namespace with `--sandbox chroot`. This serves files owned
|
||||
//! by arbitrary guest UIDs (e.g. when an image uses the `USER`
|
||||
//! directive) without running as root, reusing rootless podman's
|
||||
//! subuid/subgid mapping: guest UID 0 maps to the invoking user, and
|
||||
//! other guest UIDs map into the subuid range, so each guest UID
|
||||
//! becomes a distinct host UID.
|
||||
//!
|
||||
//! * `direct`: virtiofsd runs as the invoking user with `--sandbox
|
||||
//! none`. Guest UIDs map 1:1 to host UIDs. Use this when the
|
||||
//! shared directory lives on a filesystem that rejects subuid-range
|
||||
//! UIDs (e.g. nested virtiofs), or when the guest should act as the
|
||||
//! invoking user rather than the subuid range.
|
||||
//!
|
||||
//! Lifecycle: boco spawns all virtiofsd children before launching QEMU,
|
||||
//! waits for each listening socket to appear, then drops the
|
||||
@@ -28,10 +36,26 @@ use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use clap::ValueEnum;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::command::cmd;
|
||||
|
||||
/// How virtiofsd is launched for a share.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
|
||||
pub enum MountMode {
|
||||
/// Run virtiofsd inside `podman unshare` (rootless UID mapping:
|
||||
/// guest UID 0 maps to the invoking user, other guest UIDs map
|
||||
/// into the subuid range).
|
||||
Unshare,
|
||||
/// Run virtiofsd directly as the invoking user. Guest UIDs map 1:1
|
||||
/// to host UIDs. Needed when the shared directory itself lives on
|
||||
/// a virtiofs/fuse mount that rejects subuid-range UIDs (e.g.
|
||||
/// nested boco), where writes through the subuid mapping fail with
|
||||
/// EINVAL.
|
||||
Direct,
|
||||
}
|
||||
|
||||
/// Locations searched for the virtiofsd binary, in order.
|
||||
const VIRTIOFSD_CANDIDATES: &[&str] = &[
|
||||
"/usr/libexec/virtiofsd",
|
||||
@@ -114,7 +138,12 @@ fn wait_for_socket(socket: &Path) -> Result<()> {
|
||||
///
|
||||
/// `index` becomes the suffix of the tag (`boco{index}`) and is used to
|
||||
/// derive a unique socket filename.
|
||||
pub fn spawn_share(index: usize, host_path: &str, guest_path: String) -> Result<VirtiofsShare> {
|
||||
pub fn spawn_share(
|
||||
index: usize,
|
||||
host_path: &str,
|
||||
guest_path: String,
|
||||
mode: MountMode,
|
||||
) -> Result<VirtiofsShare> {
|
||||
let virtiofsd = find_virtiofsd()?;
|
||||
let canonical = resolve_host_dir(host_path)?;
|
||||
let host_str = canonical
|
||||
@@ -128,17 +157,29 @@ pub fn spawn_share(index: usize, host_path: &str, guest_path: String) -> Result<
|
||||
let tag = format!("boco{index}"); // short tag to fit on cmdline
|
||||
let socket = dir.path().join(format!("{tag}.sock"));
|
||||
|
||||
// TODO: Make rootless mode optional
|
||||
|
||||
// Run virtiofsd under `podman unshare` so it can serve files owned
|
||||
// by arbitrary guest UIDs without root, mapping them into the host
|
||||
// user's subuid range. `--sandbox chroot` is required because the
|
||||
// outer `podman unshare` already entered a user namespace (nested
|
||||
// user namespaces are not allowed); chroot still prevents symlink
|
||||
// escapes from the shared dir.
|
||||
let mut command = Command::new("podman");
|
||||
// In `unshare` mode, run virtiofsd under `podman unshare` so it can
|
||||
// serve files owned by arbitrary guest UIDs without root, mapping
|
||||
// them into the host user's subuid range. `--sandbox chroot` is
|
||||
// required because the outer `podman unshare` already entered a user
|
||||
// namespace (nested user namespaces are not allowed); chroot still
|
||||
// prevents symlink escapes from the shared dir.
|
||||
//
|
||||
// In `direct` mode, run virtiofsd as the invoking user with 1:1 UID
|
||||
// mapping. This cannot use the chroot sandbox (unprivileged
|
||||
// chroot is not permitted), so it falls back to `--sandbox none`.
|
||||
let mut command = match mode {
|
||||
MountMode::Unshare => {
|
||||
let mut command = Command::new("podman");
|
||||
command.args(["unshare", "--", virtiofsd_str, "--socket-path"]);
|
||||
command
|
||||
}
|
||||
MountMode::Direct => {
|
||||
let mut command = Command::new(virtiofsd_str);
|
||||
command.arg("--socket-path");
|
||||
command
|
||||
}
|
||||
};
|
||||
command
|
||||
.args(["unshare", "--", virtiofsd_str, "--socket-path"])
|
||||
.arg(&socket)
|
||||
.args([
|
||||
"--shared-dir",
|
||||
@@ -146,7 +187,10 @@ pub fn spawn_share(index: usize, host_path: &str, guest_path: String) -> Result<
|
||||
"--tag",
|
||||
&tag,
|
||||
"--sandbox",
|
||||
"chroot",
|
||||
match mode {
|
||||
MountMode::Unshare => "chroot",
|
||||
MountMode::Direct => "none",
|
||||
},
|
||||
"--cache",
|
||||
"auto",
|
||||
])
|
||||
|
||||
Reference in new issue
Block a user