1use crate::error::{AgentdError, AgentdResult};
7
8#[derive(Debug)]
14#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
15struct TmpfsSpec<'a> {
16 path: &'a str,
17 size_mib: Option<u32>,
18 mode: Option<u32>,
19 noexec: bool,
20}
21
22#[derive(Debug)]
24#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
25struct BlockRootSpec<'a> {
26 device: &'a str,
27 fstype: Option<&'a str>,
28}
29
30#[cfg(target_os = "linux")]
39pub fn init() -> AgentdResult<()> {
40 linux::mount_filesystems()?;
41 linux::mount_runtime()?;
42 linux::mount_block_root()?;
43 linux::apply_tmpfs_mounts()?;
44 crate::network::apply_network_config()?;
45 crate::tls::install_ca_cert()?;
46 linux::create_run_dir()?;
47 Ok(())
48}
49
50#[cfg(not(target_os = "linux"))]
52pub fn init() -> AgentdResult<()> {
53 Ok(())
54}
55
56#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
60fn parse_tmpfs_entry(entry: &str) -> AgentdResult<TmpfsSpec<'_>> {
61 let mut parts = entry.split(',');
62 let path = parts.next().unwrap(); if path.is_empty() {
64 return Err(AgentdError::Init("tmpfs entry has empty path".into()));
65 }
66
67 let mut size_mib = None;
68 let mut mode = None;
69 let mut noexec = false;
70
71 for opt in parts {
72 if opt == "noexec" {
73 noexec = true;
74 } else if let Some(val) = opt.strip_prefix("size=") {
75 size_mib = Some(
76 val.parse::<u32>()
77 .map_err(|_| AgentdError::Init(format!("invalid tmpfs size: {val}")))?,
78 );
79 } else if let Some(val) = opt.strip_prefix("mode=") {
80 mode = Some(
81 u32::from_str_radix(val, 8)
82 .map_err(|_| AgentdError::Init(format!("invalid octal tmpfs mode: {val}")))?,
83 );
84 } else {
85 return Err(AgentdError::Init(format!("unknown tmpfs option: {opt}")));
86 }
87 }
88
89 Ok(TmpfsSpec {
90 path,
91 size_mib,
92 mode,
93 noexec,
94 })
95}
96
97#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
99fn parse_block_root(val: &str) -> AgentdResult<BlockRootSpec<'_>> {
100 let mut parts = val.split(',');
101 let device = parts.next().unwrap();
102 if device.is_empty() {
103 return Err(AgentdError::Init(
104 "MSB_BLOCK_ROOT has empty device path".into(),
105 ));
106 }
107
108 let mut fstype = None;
109 for opt in parts {
110 if let Some(val) = opt.strip_prefix("fstype=") {
111 if val.is_empty() {
112 return Err(AgentdError::Init(
113 "MSB_BLOCK_ROOT has empty fstype value".into(),
114 ));
115 }
116 fstype = Some(val);
117 } else {
118 return Err(AgentdError::Init(format!(
119 "unknown MSB_BLOCK_ROOT option: {opt}"
120 )));
121 }
122 }
123
124 Ok(BlockRootSpec { device, fstype })
125}
126
127#[cfg(target_os = "linux")]
132mod linux {
133 use std::{os::unix::fs::symlink, path::Path};
134
135 use nix::{
136 mount::{MsFlags, mount},
137 sys::stat::Mode,
138 unistd::{chdir, chroot, mkdir},
139 };
140
141 use crate::error::{AgentdError, AgentdResult};
142
143 use super::TmpfsSpec;
144
145 pub fn mount_filesystems() -> AgentdResult<()> {
147 mkdir_ignore_exists("/dev")?;
149 mount_ignore_busy(
150 Some("devtmpfs"),
151 "/dev",
152 Some("devtmpfs"),
153 MsFlags::MS_RELATIME,
154 None::<&str>,
155 )?;
156
157 let nodev_noexec_nosuid =
159 MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
160
161 mkdir_ignore_exists("/proc")?;
162 mount_ignore_busy(
163 Some("proc"),
164 "/proc",
165 Some("proc"),
166 nodev_noexec_nosuid,
167 None::<&str>,
168 )?;
169
170 mkdir_ignore_exists("/sys")?;
172 mount_ignore_busy(
173 Some("sysfs"),
174 "/sys",
175 Some("sysfs"),
176 nodev_noexec_nosuid,
177 None::<&str>,
178 )?;
179
180 mkdir_ignore_exists("/sys/fs/cgroup")?;
182 mount_ignore_busy(
183 Some("cgroup2"),
184 "/sys/fs/cgroup",
185 Some("cgroup2"),
186 nodev_noexec_nosuid,
187 None::<&str>,
188 )?;
189
190 let noexec_nosuid = MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
192
193 mkdir_ignore_exists("/dev/pts")?;
194 mount_ignore_busy(
195 Some("devpts"),
196 "/dev/pts",
197 Some("devpts"),
198 noexec_nosuid,
199 None::<&str>,
200 )?;
201
202 mkdir_ignore_exists("/dev/shm")?;
204 mount_ignore_busy(
205 Some("tmpfs"),
206 "/dev/shm",
207 Some("tmpfs"),
208 noexec_nosuid,
209 None::<&str>,
210 )?;
211
212 if !Path::new("/dev/fd").exists() {
214 symlink("/proc/self/fd", "/dev/fd")
215 .map_err(|e| AgentdError::Init(format!("failed to symlink /dev/fd: {e}")))?;
216 }
217
218 Ok(())
219 }
220
221 pub fn mount_runtime() -> AgentdResult<()> {
223 mkdir_ignore_exists(microsandbox_protocol::RUNTIME_MOUNT_POINT)?;
224 mount_ignore_busy(
225 Some(microsandbox_protocol::RUNTIME_FS_TAG),
226 microsandbox_protocol::RUNTIME_MOUNT_POINT,
227 Some("virtiofs"),
228 MsFlags::empty(),
229 None::<&str>,
230 )?;
231 Ok(())
232 }
233
234 pub fn mount_block_root() -> AgentdResult<()> {
239 let val = match std::env::var(microsandbox_protocol::ENV_BLOCK_ROOT) {
240 Ok(v) if !v.is_empty() => v,
241 _ => return Ok(()),
242 };
243
244 let spec = super::parse_block_root(&val)?;
245
246 mkdir_ignore_exists("/newroot")?;
248
249 if let Some(fstype) = spec.fstype {
251 mount(
252 Some(spec.device),
253 "/newroot",
254 Some(fstype),
255 MsFlags::empty(),
256 None::<&str>,
257 )
258 .map_err(|e| {
259 AgentdError::Init(format!(
260 "failed to mount {} at /newroot as {fstype}: {e}",
261 spec.device
262 ))
263 })?;
264 } else {
265 try_mount(spec.device, "/newroot")?;
266 }
267
268 let msb_target = "/newroot/.msb";
270 mkdir_ignore_exists(msb_target)?;
271 mount(
272 Some(microsandbox_protocol::RUNTIME_MOUNT_POINT),
273 msb_target,
274 None::<&str>,
275 MsFlags::MS_BIND,
276 None::<&str>,
277 )
278 .map_err(|e| AgentdError::Init(format!("failed to bind-mount /.msb into /newroot: {e}")))?;
279
280 chdir("/newroot")
282 .map_err(|e| AgentdError::Init(format!("failed to chdir /newroot: {e}")))?;
283
284 mount(Some("."), "/", None::<&str>, MsFlags::MS_MOVE, None::<&str>)
285 .map_err(|e| AgentdError::Init(format!("failed to MS_MOVE /newroot to /: {e}")))?;
286
287 chroot(".").map_err(|e| AgentdError::Init(format!("failed to chroot: {e}")))?;
288
289 chdir("/")
290 .map_err(|e| AgentdError::Init(format!("failed to chdir / after chroot: {e}")))?;
291
292 mount_filesystems()?;
294
295 Ok(())
296 }
297
298 fn try_mount(device: &str, target: &str) -> AgentdResult<()> {
300 let content = std::fs::read_to_string("/proc/filesystems")
301 .map_err(|e| AgentdError::Init(format!("failed to read /proc/filesystems: {e}")))?;
302
303 for line in content.lines() {
304 if line.starts_with("nodev") {
306 continue;
307 }
308
309 let fstype = line.trim();
310 if fstype.is_empty() {
311 continue;
312 }
313
314 if mount(
315 Some(device),
316 target,
317 Some(fstype),
318 MsFlags::empty(),
319 None::<&str>,
320 )
321 .is_ok()
322 {
323 return Ok(());
324 }
325 }
326
327 Err(AgentdError::Init(format!(
328 "failed to mount {device} at {target}: no supported filesystem found"
329 )))
330 }
331
332 pub fn apply_tmpfs_mounts() -> AgentdResult<()> {
337 let val = match std::env::var(microsandbox_protocol::ENV_TMPFS) {
338 Ok(v) if !v.is_empty() => v,
339 _ => return Ok(()),
340 };
341
342 for entry in val.split(';') {
343 if entry.is_empty() {
344 continue;
345 }
346
347 let spec = super::parse_tmpfs_entry(entry)?;
348 mount_tmpfs(&spec)?;
349 }
350
351 Ok(())
352 }
353
354 fn mount_tmpfs(spec: &TmpfsSpec<'_>) -> AgentdResult<()> {
356 let path = spec.path;
357
358 let mode = spec
360 .mode
361 .unwrap_or(if path == "/tmp" || path == "/var/tmp" {
362 0o1777
363 } else {
364 0o755
365 });
366
367 std::fs::create_dir_all(path)
369 .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
370
371 let mut flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_RELATIME;
373 if spec.noexec {
374 flags |= MsFlags::MS_NOEXEC;
375 }
376
377 let mut data = String::new();
379 if let Some(mib) = spec.size_mib {
380 data.push_str(&format!("size={}", u64::from(mib) * 1024 * 1024));
381 }
382 if !data.is_empty() {
383 data.push(',');
384 }
385 data.push_str(&format!("mode={mode:o}"));
386
387 mount(
388 Some("tmpfs"),
389 path,
390 Some("tmpfs"),
391 flags,
392 Some(data.as_str()),
393 )
394 .map_err(|e| AgentdError::Init(format!("failed to mount tmpfs at {path}: {e}")))?;
395
396 Ok(())
397 }
398
399 pub fn create_run_dir() -> AgentdResult<()> {
401 mkdir_ignore_exists("/run")?;
402 Ok(())
403 }
404
405 fn mkdir_ignore_exists(path: &str) -> AgentdResult<()> {
407 match mkdir(path, Mode::from_bits_truncate(0o755)) {
408 Ok(()) => Ok(()),
409 Err(nix::Error::EEXIST) => Ok(()),
410 Err(e) => Err(e.into()),
411 }
412 }
413
414 fn mount_ignore_busy(
416 source: Option<&str>,
417 target: &str,
418 fstype: Option<&str>,
419 flags: MsFlags,
420 data: Option<&str>,
421 ) -> AgentdResult<()> {
422 match mount(source, target, fstype, flags, data) {
423 Ok(()) => Ok(()),
424 Err(nix::Error::EBUSY) => Ok(()),
425 Err(e) => Err(AgentdError::Init(format!("failed to mount {target}: {e}"))),
426 }
427 }
428}
429
430#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_parse_path_only() {
440 let spec = parse_tmpfs_entry("/tmp").unwrap();
441 assert_eq!(spec.path, "/tmp");
442 assert_eq!(spec.size_mib, None);
443 assert_eq!(spec.mode, None);
444 assert!(!spec.noexec);
445 }
446
447 #[test]
448 fn test_parse_with_size() {
449 let spec = parse_tmpfs_entry("/tmp,size=256").unwrap();
450 assert_eq!(spec.path, "/tmp");
451 assert_eq!(spec.size_mib, Some(256));
452 }
453
454 #[test]
455 fn test_parse_with_noexec() {
456 let spec = parse_tmpfs_entry("/tmp,noexec").unwrap();
457 assert_eq!(spec.path, "/tmp");
458 assert!(spec.noexec);
459 }
460
461 #[test]
462 fn test_parse_with_octal_mode() {
463 let spec = parse_tmpfs_entry("/tmp,mode=1777").unwrap();
464 assert_eq!(spec.mode, Some(0o1777));
465
466 let spec = parse_tmpfs_entry("/data,mode=755").unwrap();
467 assert_eq!(spec.mode, Some(0o755));
468 }
469
470 #[test]
471 fn test_parse_multi_options() {
472 let spec = parse_tmpfs_entry("/tmp,size=256,mode=1777,noexec").unwrap();
473 assert_eq!(spec.path, "/tmp");
474 assert_eq!(spec.size_mib, Some(256));
475 assert_eq!(spec.mode, Some(0o1777));
476 assert!(spec.noexec);
477 }
478
479 #[test]
480 fn test_parse_unknown_option_errors() {
481 let err = parse_tmpfs_entry("/tmp,bogus=42").unwrap_err();
482 assert!(err.to_string().contains("unknown tmpfs option"));
483 }
484
485 #[test]
486 fn test_parse_invalid_size_errors() {
487 let err = parse_tmpfs_entry("/tmp,size=abc").unwrap_err();
488 assert!(err.to_string().contains("invalid tmpfs size"));
489 }
490
491 #[test]
492 fn test_parse_invalid_mode_errors() {
493 let err = parse_tmpfs_entry("/tmp,mode=zzz").unwrap_err();
494 assert!(err.to_string().contains("invalid octal tmpfs mode"));
495 }
496
497 #[test]
498 fn test_parse_empty_path_errors() {
499 let err = parse_tmpfs_entry(",size=256").unwrap_err();
500 assert!(err.to_string().contains("empty path"));
501 }
502
503 #[test]
504 fn test_parse_block_root_device_only() {
505 let spec = parse_block_root("/dev/vda").unwrap();
506 assert_eq!(spec.device, "/dev/vda");
507 assert_eq!(spec.fstype, None);
508 }
509
510 #[test]
511 fn test_parse_block_root_with_fstype() {
512 let spec = parse_block_root("/dev/vda,fstype=ext4").unwrap();
513 assert_eq!(spec.device, "/dev/vda");
514 assert_eq!(spec.fstype, Some("ext4"));
515 }
516
517 #[test]
518 fn test_parse_block_root_empty_device_errors() {
519 let err = parse_block_root(",fstype=ext4").unwrap_err();
520 assert!(err.to_string().contains("empty device path"));
521 }
522
523 #[test]
524 fn test_parse_block_root_unknown_option_errors() {
525 let err = parse_block_root("/dev/vda,bogus=42").unwrap_err();
526 assert!(err.to_string().contains("unknown MSB_BLOCK_ROOT option"));
527 }
528
529 #[test]
530 fn test_parse_block_root_empty_fstype_errors() {
531 let err = parse_block_root("/dev/vda,fstype=").unwrap_err();
532 assert!(err.to_string().contains("empty fstype"));
533 }
534}