nucleus/filesystem/
lazy.rs1use crate::error::{NucleusError, Result};
2use crate::filesystem::ContextPopulator;
3use std::path::Path;
4use tracing::info;
5
6#[derive(Debug, Clone, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
8pub enum ContextMode {
9 Copy,
11 #[value(name = "bind")]
13 BindMount,
14}
15
16pub struct LazyContextPopulator;
18
19impl LazyContextPopulator {
20 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 fn bind_mount_context(source: &Path, dest: &Path) -> Result<()> {
36 ContextPopulator::new(source, dest).validate_source_tree()?;
37
38 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 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 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 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 {
141 fn_end = open + i + 1;
142 break;
143 }
144 }
145 _ => {}
146 }
147 }
148 let fn_body = &after[..fn_end];
149 assert!(
150 fn_body.contains("MS_NOSUID"),
151 "bind_mount_context must set MS_NOSUID"
152 );
153 assert!(
154 fn_body.contains("MS_NODEV"),
155 "bind_mount_context must set MS_NODEV"
156 );
157 assert!(
158 fn_body.contains("MS_NOEXEC"),
159 "bind_mount_context must set MS_NOEXEC"
160 );
161 }
162}