Skip to main content

nucleus/filesystem/
lazy.rs

1use crate::error::{NucleusError, Result};
2use crate::filesystem::ContextPopulator;
3use std::path::Path;
4use tracing::info;
5
6/// Context population mode
7#[derive(Debug, Clone, clap::ValueEnum)]
8pub enum ContextMode {
9    /// Traditional copy (default, backward compatible)
10    Copy,
11    /// Bind mount for zero-copy, instant access
12    #[value(name = "bind")]
13    BindMount,
14}
15
16/// Lazy context populator that supports both copy and bind mount modes
17pub struct LazyContextPopulator;
18
19impl LazyContextPopulator {
20    /// Populate context using the specified mode
21    ///
22    /// For BindMount mode, the bind mount must happen before pivot_root
23    /// since the source is on the host filesystem.
24    pub fn populate(mode: &ContextMode, source: &Path, dest: &Path) -> Result<()> {
25        match mode {
26            ContextMode::Copy => {
27                let populator = ContextPopulator::new(source, dest);
28                populator.populate()
29            }
30            ContextMode::BindMount => Self::bind_mount_context(source, dest),
31        }
32    }
33
34    /// Bind mount source directory to destination (read-only)
35    fn bind_mount_context(source: &Path, dest: &Path) -> Result<()> {
36        ContextPopulator::new(source, dest).validate_source_tree()?;
37
38        // Ensure destination exists
39        std::fs::create_dir_all(dest).map_err(|e| {
40            NucleusError::ContextError(format!("Failed to create destination {:?}: {}", dest, e))
41        })?;
42
43        info!(
44            "Bind mounting context: {:?} -> {:?} (read-only)",
45            source, dest
46        );
47
48        // Initial bind mount
49        nix::mount::mount(
50            Some(source),
51            dest,
52            None::<&str>,
53            nix::mount::MsFlags::MS_BIND | nix::mount::MsFlags::MS_REC,
54            None::<&str>,
55        )
56        .map_err(|e| {
57            NucleusError::ContextError(format!(
58                "Failed to bind mount {:?} -> {:?}: {}",
59                source, dest, e
60            ))
61        })?;
62
63        // Remount read-only
64        nix::mount::mount(
65            None::<&str>,
66            dest,
67            None::<&str>,
68            nix::mount::MsFlags::MS_BIND
69                | nix::mount::MsFlags::MS_REC
70                | nix::mount::MsFlags::MS_RDONLY
71                | nix::mount::MsFlags::MS_NOSUID
72                | nix::mount::MsFlags::MS_NODEV
73                | nix::mount::MsFlags::MS_NOEXEC
74                | nix::mount::MsFlags::MS_REMOUNT,
75            None::<&str>,
76        )
77        .map_err(|e| {
78            NucleusError::ContextError(format!("Failed to remount {:?} read-only: {}", dest, e))
79        })?;
80
81        info!("Context bind mounted successfully");
82        Ok(())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use nix::sys::stat::Mode;
90    use nix::unistd::mkfifo;
91    use tempfile::TempDir;
92
93    #[test]
94    fn test_context_mode_default() {
95        let mode = ContextMode::Copy;
96        assert!(matches!(mode, ContextMode::Copy));
97    }
98
99    #[test]
100    fn test_bind_mount_nonexistent_source() {
101        let result = LazyContextPopulator::bind_mount_context(
102            Path::new("/nonexistent/path"),
103            Path::new("/tmp/dest"),
104        );
105        assert!(result.is_err());
106    }
107
108    #[test]
109    fn test_bind_mount_context_rejects_special_files() {
110        let temp = TempDir::new().unwrap();
111        let src = temp.path().join("src");
112        let dst = temp.path().join("dst");
113        std::fs::create_dir_all(&src).unwrap();
114
115        let fifo_path = src.join("agent.fifo");
116        mkfifo(&fifo_path, Mode::from_bits_truncate(0o600)).unwrap();
117
118        let err = LazyContextPopulator::bind_mount_context(&src, &dst).unwrap_err();
119        assert!(
120            err.to_string().contains("special file"),
121            "bind-mounted contexts must reject host special files"
122        );
123    }
124
125    #[test]
126    fn test_bind_mount_context_remount_adds_hardening_flags() {
127        // Verify bind_mount_context applies hardening mount flags.
128        // Uses brace-matched extraction instead of scanning to EOF (SEC-MED-03).
129        let source = include_str!("lazy.rs");
130        let fn_start = source.find("fn bind_mount_context").unwrap();
131        let after = &source[fn_start..];
132        let open = after.find('{').unwrap();
133        let mut depth = 0u32;
134        let mut fn_end = open;
135        for (i, ch) in after[open..].char_indices() {
136            match ch {
137                '{' => depth += 1,
138                '}' => {
139                    depth -= 1;
140                    if depth == 0 { fn_end = open + i + 1; break; }
141                }
142                _ => {}
143            }
144        }
145        let fn_body = &after[..fn_end];
146        assert!(fn_body.contains("MS_NOSUID"), "bind_mount_context must set MS_NOSUID");
147        assert!(fn_body.contains("MS_NODEV"), "bind_mount_context must set MS_NODEV");
148        assert!(fn_body.contains("MS_NOEXEC"), "bind_mount_context must set MS_NOEXEC");
149    }
150}