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