nucleus/filesystem/
lazy.rs1use crate::error::{NucleusError, Result};
2use crate::filesystem::ContextPopulator;
3use std::path::Path;
4use tracing::info;
5
6#[derive(Debug, Clone, Default, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
8pub enum ContextMode {
9 #[default]
11 #[serde(rename = "copy")]
12 Copy,
13 #[value(name = "bind")]
15 #[serde(rename = "bind", alias = "bind-mount")]
16 BindMount,
17}
18
19pub struct LazyContextPopulator;
21
22impl LazyContextPopulator {
23 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 fn bind_mount_context(source: &Path, dest: &Path) -> Result<()> {
39 ContextPopulator::new(source, dest).validate_source_tree()?;
40
41 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 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 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 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}