use crate::error::{NucleusError, Result};
use crate::filesystem::ContextPopulator;
use std::path::Path;
use tracing::info;
#[derive(Debug, Clone, clap::ValueEnum)]
pub enum ContextMode {
Copy,
#[value(name = "bind")]
BindMount,
}
pub struct LazyContextPopulator;
impl LazyContextPopulator {
pub fn populate(mode: &ContextMode, source: &Path, dest: &Path) -> Result<()> {
match mode {
ContextMode::Copy => {
let populator = ContextPopulator::new(source, dest);
populator.populate()
}
ContextMode::BindMount => Self::bind_mount_context(source, dest),
}
}
fn bind_mount_context(source: &Path, dest: &Path) -> Result<()> {
ContextPopulator::new(source, dest).validate_source_tree()?;
std::fs::create_dir_all(dest).map_err(|e| {
NucleusError::ContextError(format!("Failed to create destination {:?}: {}", dest, e))
})?;
info!(
"Bind mounting context: {:?} -> {:?} (read-only)",
source, dest
);
nix::mount::mount(
Some(source),
dest,
None::<&str>,
nix::mount::MsFlags::MS_BIND | nix::mount::MsFlags::MS_REC,
None::<&str>,
)
.map_err(|e| {
NucleusError::ContextError(format!(
"Failed to bind mount {:?} -> {:?}: {}",
source, dest, e
))
})?;
nix::mount::mount(
None::<&str>,
dest,
None::<&str>,
nix::mount::MsFlags::MS_BIND
| nix::mount::MsFlags::MS_REC
| nix::mount::MsFlags::MS_RDONLY
| nix::mount::MsFlags::MS_NOSUID
| nix::mount::MsFlags::MS_NODEV
| nix::mount::MsFlags::MS_NOEXEC
| nix::mount::MsFlags::MS_REMOUNT,
None::<&str>,
)
.map_err(|e| {
NucleusError::ContextError(format!("Failed to remount {:?} read-only: {}", dest, e))
})?;
info!("Context bind mounted successfully");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use nix::sys::stat::Mode;
use nix::unistd::mkfifo;
use tempfile::TempDir;
#[test]
fn test_context_mode_default() {
let mode = ContextMode::Copy;
assert!(matches!(mode, ContextMode::Copy));
}
#[test]
fn test_bind_mount_nonexistent_source() {
let result = LazyContextPopulator::bind_mount_context(
Path::new("/nonexistent/path"),
Path::new("/tmp/dest"),
);
assert!(result.is_err());
}
#[test]
fn test_bind_mount_context_rejects_special_files() {
let temp = TempDir::new().unwrap();
let src = temp.path().join("src");
let dst = temp.path().join("dst");
std::fs::create_dir_all(&src).unwrap();
let fifo_path = src.join("agent.fifo");
mkfifo(&fifo_path, Mode::from_bits_truncate(0o600)).unwrap();
let err = LazyContextPopulator::bind_mount_context(&src, &dst).unwrap_err();
assert!(
err.to_string().contains("special file"),
"bind-mounted contexts must reject host special files"
);
}
#[test]
fn test_bind_mount_context_remount_adds_hardening_flags() {
let source = include_str!("lazy.rs");
let fn_start = source.find("fn bind_mount_context").unwrap();
let after = &source[fn_start..];
let open = after.find('{').unwrap();
let mut depth = 0u32;
let mut fn_end = open;
for (i, ch) in after[open..].char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
fn_end = open + i + 1;
break;
}
}
_ => {}
}
}
let fn_body = &after[..fn_end];
assert!(
fn_body.contains("MS_NOSUID"),
"bind_mount_context must set MS_NOSUID"
);
assert!(
fn_body.contains("MS_NODEV"),
"bind_mount_context must set MS_NODEV"
);
assert!(
fn_body.contains("MS_NOEXEC"),
"bind_mount_context must set MS_NOEXEC"
);
}
}