1use super::overlay_fs::{OverlayFileSystem, OverlayMode};
2use super::usage::{
3 RootFilesystemResourceLimits, DEFAULT_MAX_FILESYSTEM_BYTES, DEFAULT_MAX_INODE_COUNT,
4};
5use super::vfs::{
6 normalize_path, MemoryFileSystem, VfsError, VfsResult, VirtualFileSystem, VirtualStat,
7 VirtualUtimeSpec, MAX_PATH_LENGTH,
8};
9use crate::posix::vfs::VirtualDirEntry;
10use base64::Engine;
11use serde::Deserialize;
12use std::collections::BTreeSet;
13
14const BUNDLED_BASE_FILESYSTEM_JSON: &str =
19 include_str!(concat!(env!("OUT_DIR"), "/base-filesystem.json"));
20pub const ROOT_FILESYSTEM_SNAPSHOT_FORMAT: &str = "secure_exec_filesystem_snapshot_v1";
21const LEGACY_AGENTOS_ROOT_FILESYSTEM_SNAPSHOT_FORMAT: &str = "agentos_filesystem_snapshot_v1";
22const ROOT_FILESYSTEM_SNAPSHOT_FIXED_OVERHEAD_BYTES: usize = 4 * 1024;
23const ROOT_FILESYSTEM_SNAPSHOT_ENTRY_OVERHEAD_BYTES: usize = MAX_PATH_LENGTH + 1024;
24const DEFAULT_ROOT_DIRECTORIES: &[&str] = &[
25 "/",
26 "/dev",
27 "/proc",
28 "/tmp",
29 "/bin",
30 "/lib",
31 "/sbin",
32 "/boot",
33 "/etc",
34 "/root",
35 "/run",
36 "/srv",
37 "/sys",
38 "/opt",
39 "/mnt",
40 "/media",
41 "/home",
42 "/usr",
43 "/usr/bin",
44 "/usr/games",
45 "/usr/include",
46 "/usr/lib",
47 "/usr/libexec",
48 "/usr/man",
49 "/usr/local",
50 "/usr/local/bin",
51 "/usr/sbin",
52 "/usr/share",
53 "/usr/share/man",
54 "/var",
55 "/var/cache",
56 "/var/empty",
57 "/var/lib",
58 "/var/lock",
59 "/var/log",
60 "/var/run",
61 "/var/spool",
62 "/var/tmp",
63 "/etc/agentos",
64];
65const KERNEL_RESERVED_BOOTSTRAP_PATH_PREFIXES: &[&str] = &["/dev", "/proc", "/sys"];
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct RootFilesystemError {
69 message: String,
70}
71
72impl RootFilesystemError {
73 fn new(message: impl Into<String>) -> Self {
74 Self {
75 message: message.into(),
76 }
77 }
78}
79
80impl std::fmt::Display for RootFilesystemError {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.write_str(&self.message)
83 }
84}
85
86impl std::error::Error for RootFilesystemError {}
87
88impl From<VfsError> for RootFilesystemError {
89 fn from(error: VfsError) -> Self {
90 Self::new(error.to_string())
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum FilesystemEntryKind {
96 File,
97 Directory,
98 Symlink,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct FilesystemEntry {
103 pub path: String,
104 pub kind: FilesystemEntryKind,
105 pub mode: u32,
106 pub uid: u32,
107 pub gid: u32,
108 pub content: Option<Vec<u8>>,
109 pub target: Option<String>,
110}
111
112impl FilesystemEntry {
113 pub fn directory(path: impl Into<String>) -> Self {
114 Self {
115 path: path.into(),
116 kind: FilesystemEntryKind::Directory,
117 mode: 0o755,
118 uid: 0,
119 gid: 0,
120 content: None,
121 target: None,
122 }
123 }
124
125 pub fn file(path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
126 Self {
127 path: path.into(),
128 kind: FilesystemEntryKind::File,
129 mode: 0o644,
130 uid: 0,
131 gid: 0,
132 content: Some(content.into()),
133 target: None,
134 }
135 }
136
137 pub fn symlink(path: impl Into<String>, target: impl Into<String>) -> Self {
138 Self {
139 path: path.into(),
140 kind: FilesystemEntryKind::Symlink,
141 mode: 0o777,
142 uid: 0,
143 gid: 0,
144 content: None,
145 target: Some(target.into()),
146 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct RootFilesystemSnapshot {
152 pub entries: Vec<FilesystemEntry>,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct RootFilesystemImportLimits {
157 pub max_encoded_snapshot_bytes: Option<usize>,
158 pub max_filesystem_bytes: Option<u64>,
159 pub max_inode_count: Option<usize>,
160}
161
162impl RootFilesystemImportLimits {
163 pub fn from_resource_limits(limits: &impl RootFilesystemResourceLimits) -> Self {
164 Self {
165 max_encoded_snapshot_bytes: encoded_snapshot_limit(
166 limits.max_filesystem_bytes(),
167 limits.max_inode_count(),
168 ),
169 max_filesystem_bytes: limits.max_filesystem_bytes(),
170 max_inode_count: limits.max_inode_count(),
171 }
172 }
173}
174
175impl Default for RootFilesystemImportLimits {
176 fn default() -> Self {
177 Self {
178 max_encoded_snapshot_bytes: encoded_snapshot_limit(
179 Some(DEFAULT_MAX_FILESYSTEM_BYTES),
180 Some(DEFAULT_MAX_INODE_COUNT),
181 ),
182 max_filesystem_bytes: Some(DEFAULT_MAX_FILESYSTEM_BYTES),
183 max_inode_count: Some(DEFAULT_MAX_INODE_COUNT),
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum RootFilesystemMode {
190 Ephemeral,
191 ReadOnly,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct RootFilesystemDescriptor {
196 pub mode: RootFilesystemMode,
197 pub disable_default_base_layer: bool,
198 pub lowers: Vec<RootFilesystemSnapshot>,
199 pub bootstrap_entries: Vec<FilesystemEntry>,
200}
201
202impl Default for RootFilesystemDescriptor {
203 fn default() -> Self {
204 Self {
205 mode: RootFilesystemMode::Ephemeral,
206 disable_default_base_layer: false,
207 lowers: Vec::new(),
208 bootstrap_entries: Vec::new(),
209 }
210 }
211}
212
213#[derive(Debug)]
214pub struct RootFileSystem {
215 overlay: OverlayFileSystem,
216 mode: RootFilesystemMode,
217 bootstrap_finished: bool,
218}
219
220impl RootFileSystem {
221 pub fn from_descriptor(
222 descriptor: RootFilesystemDescriptor,
223 ) -> Result<Self, RootFilesystemError> {
224 Self::from_descriptor_with_import_limits(descriptor, &RootFilesystemImportLimits::default())
225 }
226
227 pub fn from_descriptor_with_import_limits(
228 descriptor: RootFilesystemDescriptor,
229 limits: &RootFilesystemImportLimits,
230 ) -> Result<Self, RootFilesystemError> {
231 let mut lower_snapshots = descriptor.lowers.clone();
232 if !descriptor.disable_default_base_layer {
233 lower_snapshots.push(load_bundled_base_snapshot_with_limits(limits)?);
234 } else if lower_snapshots.is_empty() {
235 lower_snapshots.push(minimal_root_snapshot());
236 }
237 validate_descriptor_import_limits(
238 &lower_snapshots,
239 &descriptor.bootstrap_entries,
240 limits,
241 "root filesystem descriptor",
242 )?;
243
244 let lowers = lower_snapshots
245 .iter()
246 .map(snapshot_to_memory_filesystem)
247 .collect::<Result<Vec<_>, _>>()?;
248
249 let mut root = Self {
250 overlay: OverlayFileSystem::new(lowers, OverlayMode::Ephemeral),
251 mode: descriptor.mode,
252 bootstrap_finished: false,
253 };
254 root.apply_bootstrap_entries(&descriptor.bootstrap_entries)?;
255 Ok(root)
256 }
257
258 pub fn apply_bootstrap_entries(
259 &mut self,
260 entries: &[FilesystemEntry],
261 ) -> Result<(), RootFilesystemError> {
262 if self.bootstrap_finished {
263 return Err(RootFilesystemError::new(
264 "root filesystem bootstrap is already finished",
265 ));
266 }
267
268 for entry in sort_entries(entries.to_vec()) {
269 if is_kernel_reserved_bootstrap_path(&entry.path) {
270 continue;
271 }
272 apply_entry(&mut self.overlay, &entry)?;
273 }
274 Ok(())
275 }
276
277 pub fn finish_bootstrap(&mut self) {
278 if self.bootstrap_finished {
279 return;
280 }
281 self.bootstrap_finished = true;
282 if self.mode == RootFilesystemMode::ReadOnly {
283 self.overlay.lock_writes();
284 }
285 }
286
287 pub fn snapshot(&mut self) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
288 Ok(RootFilesystemSnapshot {
289 entries: snapshot_virtual_filesystem(&mut self.overlay, "/")?,
290 })
291 }
292
293 pub fn check_rename_copy_up_limits(
294 &mut self,
295 old_path: &str,
296 new_path: &str,
297 max_bytes: Option<u64>,
298 max_inodes: Option<usize>,
299 ) -> VfsResult<()> {
300 self.overlay
301 .check_rename_copy_up_limits(old_path, new_path, max_bytes, max_inodes)
302 }
303}
304
305impl VirtualFileSystem for RootFileSystem {
306 fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
307 self.overlay.read_file(path)
308 }
309
310 fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
311 self.overlay.read_dir(path)
312 }
313
314 fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
315 self.overlay.read_dir_limited(path, max_entries)
316 }
317
318 fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
319 self.overlay.read_dir_with_types(path)
320 }
321
322 fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
323 self.overlay.write_file(path, content.into())
324 }
325
326 fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
327 self.overlay.create_file_exclusive(path, content.into())
328 }
329
330 fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
331 self.overlay.append_file(path, content.into())
332 }
333
334 fn create_dir(&mut self, path: &str) -> VfsResult<()> {
335 self.overlay.create_dir(path)
336 }
337
338 fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
339 self.overlay.mkdir(path, recursive)
340 }
341
342 fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
343 self.overlay.mknod(path, mode, rdev)
344 }
345
346 fn exists(&self, path: &str) -> bool {
347 self.overlay.exists(path)
348 }
349
350 fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
351 self.overlay.stat(path)
352 }
353
354 fn remove_file(&mut self, path: &str) -> VfsResult<()> {
355 self.overlay.remove_file(path)
356 }
357
358 fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
359 self.overlay.remove_dir(path)
360 }
361
362 fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
363 self.overlay.rename(old_path, new_path)
364 }
365
366 fn realpath(&self, path: &str) -> VfsResult<String> {
367 self.overlay.realpath(path)
368 }
369
370 fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
371 self.overlay.symlink(target, link_path)
372 }
373
374 fn read_link(&self, path: &str) -> VfsResult<String> {
375 self.overlay.read_link(path)
376 }
377
378 fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
379 self.overlay.lstat(path)
380 }
381
382 fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
383 self.overlay.link(old_path, new_path)
384 }
385
386 fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
387 self.overlay.chmod(path, mode)
388 }
389
390 fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
391 self.overlay.chown(path, uid, gid)
392 }
393
394 fn chown_spec(
395 &mut self,
396 path: &str,
397 uid: u32,
398 gid: u32,
399 follow_symlinks: bool,
400 ) -> VfsResult<()> {
401 self.overlay.chown_spec(path, uid, gid, follow_symlinks)
402 }
403
404 fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
405 self.overlay.lchown(path, uid, gid)
406 }
407
408 fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
409 self.overlay.get_xattr(path, name, follow_symlinks)
410 }
411
412 fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
413 self.overlay.list_xattrs(path, follow_symlinks)
414 }
415
416 fn set_xattr(
417 &mut self,
418 path: &str,
419 name: &str,
420 value: Vec<u8>,
421 flags: u32,
422 follow_symlinks: bool,
423 ) -> VfsResult<()> {
424 self.overlay
425 .set_xattr(path, name, value, flags, follow_symlinks)
426 }
427
428 fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
429 self.overlay.remove_xattr(path, name, follow_symlinks)
430 }
431
432 fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
433 self.overlay.utimes(path, atime_ms, mtime_ms)
434 }
435
436 fn utimes_spec(
437 &mut self,
438 path: &str,
439 atime: VirtualUtimeSpec,
440 mtime: VirtualUtimeSpec,
441 follow_symlinks: bool,
442 ) -> VfsResult<()> {
443 self.overlay
444 .utimes_spec(path, atime, mtime, follow_symlinks)
445 }
446
447 fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
448 self.overlay.truncate(path, length)
449 }
450
451 fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
452 self.overlay.allocate(path, offset, length)
453 }
454
455 fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
456 self.overlay.insert_range(path, offset, length)
457 }
458
459 fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
460 self.overlay.collapse_range(path, offset, length)
461 }
462
463 fn zero_range(
464 &mut self,
465 path: &str,
466 offset: u64,
467 length: u64,
468 keep_size: bool,
469 ) -> VfsResult<()> {
470 self.overlay.zero_range(path, offset, length, keep_size)
471 }
472
473 fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
474 self.overlay.punch_hole(path, offset, length)
475 }
476
477 fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
478 self.overlay.allocated_ranges(path)
479 }
480
481 fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
482 self.overlay.unwritten_ranges(path)
483 }
484
485 fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
486 self.overlay.pread(path, offset, length)
487 }
488
489 fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
490 self.overlay.pwrite(path, content, offset)
491 }
492}
493
494#[derive(Debug, Deserialize)]
495struct RawBaseFilesystemSnapshot {
496 filesystem: RawFilesystemEntries,
497}
498
499#[derive(Debug, Deserialize)]
500struct RawFilesystemEntries {
501 entries: Vec<RawFilesystemEntry>,
502}
503
504#[derive(Debug, Deserialize)]
505struct RawFilesystemEntry {
506 path: String,
507 #[serde(rename = "type")]
508 kind: RawFilesystemEntryKind,
509 mode: String,
510 uid: u32,
511 gid: u32,
512 #[serde(default)]
513 content: Option<String>,
514 #[serde(default)]
515 encoding: Option<String>,
516 #[serde(default)]
517 target: Option<String>,
518}
519
520#[derive(Debug, Deserialize)]
521#[serde(rename_all = "snake_case")]
522enum RawFilesystemEntryKind {
523 File,
524 Directory,
525 Symlink,
526}
527
528#[derive(Debug, Deserialize)]
529struct RawSnapshotExport {
530 format: String,
531 filesystem: RawFilesystemEntries,
532}
533
534#[derive(Debug, serde::Serialize)]
535struct SnapshotExport<'a> {
536 format: &'static str,
537 filesystem: SnapshotFilesystem<'a>,
538}
539
540#[derive(Debug, serde::Serialize)]
541struct SnapshotFilesystem<'a> {
542 entries: Vec<SerializedFilesystemEntry<'a>>,
543}
544
545#[derive(Debug, serde::Serialize)]
546struct SerializedFilesystemEntry<'a> {
547 path: &'a str,
548 #[serde(rename = "type")]
549 kind: &'static str,
550 mode: String,
551 uid: u32,
552 gid: u32,
553 #[serde(skip_serializing_if = "Option::is_none")]
554 content: Option<String>,
555 #[serde(skip_serializing_if = "Option::is_none")]
556 encoding: Option<&'static str>,
557 #[serde(skip_serializing_if = "Option::is_none")]
558 target: Option<&'a str>,
559}
560
561pub fn encode_snapshot(snapshot: &RootFilesystemSnapshot) -> Result<Vec<u8>, RootFilesystemError> {
562 let serialized_entries = snapshot
563 .entries
564 .iter()
565 .map(|entry| SerializedFilesystemEntry {
566 path: &entry.path,
567 kind: match entry.kind {
568 FilesystemEntryKind::File => "file",
569 FilesystemEntryKind::Directory => "directory",
570 FilesystemEntryKind::Symlink => "symlink",
571 },
572 mode: format!("{:o}", entry.mode),
573 uid: entry.uid,
574 gid: entry.gid,
575 content: entry
576 .content
577 .as_ref()
578 .map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes)),
579 encoding: entry.content.as_ref().map(|_| "base64"),
580 target: entry.target.as_deref(),
581 })
582 .collect::<Vec<_>>();
583
584 serde_json::to_vec(&SnapshotExport {
585 format: ROOT_FILESYSTEM_SNAPSHOT_FORMAT,
586 filesystem: SnapshotFilesystem {
587 entries: serialized_entries,
588 },
589 })
590 .map_err(|error| RootFilesystemError::new(format!("serialize root snapshot: {error}")))
591}
592
593pub fn decode_snapshot(bytes: &[u8]) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
594 decode_snapshot_with_import_limits(bytes, &RootFilesystemImportLimits::default())
595}
596
597pub fn decode_snapshot_with_import_limits(
598 bytes: &[u8],
599 limits: &RootFilesystemImportLimits,
600) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
601 validate_encoded_snapshot_size(bytes, limits, "root snapshot")?;
602 let raw: RawSnapshotExport = serde_json::from_slice(bytes)
603 .map_err(|error| RootFilesystemError::new(format!("parse root snapshot: {error}")))?;
604 if !is_supported_root_filesystem_snapshot_format(&raw.format) {
605 return Err(RootFilesystemError::new(format!(
606 "unsupported root snapshot format: {}",
607 raw.format
608 )));
609 }
610 raw_entries_to_snapshot(raw.filesystem.entries, limits, "root snapshot")
611}
612
613pub fn is_supported_root_filesystem_snapshot_format(format: &str) -> bool {
614 format == ROOT_FILESYSTEM_SNAPSHOT_FORMAT
615 || format == LEGACY_AGENTOS_ROOT_FILESYSTEM_SNAPSHOT_FORMAT
616}
617
618pub fn load_bundled_base_snapshot_with_limits(
619 limits: &RootFilesystemImportLimits,
620) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
621 validate_encoded_snapshot_size(
622 BUNDLED_BASE_FILESYSTEM_JSON.as_bytes(),
623 limits,
624 "bundled base filesystem",
625 )?;
626 let raw: RawBaseFilesystemSnapshot = serde_json::from_str(BUNDLED_BASE_FILESYSTEM_JSON)
627 .map_err(|error| {
628 RootFilesystemError::new(format!("parse bundled base filesystem: {error}"))
629 })?;
630 raw_entries_to_snapshot(raw.filesystem.entries, limits, "bundled base filesystem")
631}
632
633fn minimal_root_snapshot() -> RootFilesystemSnapshot {
634 let mut entries = DEFAULT_ROOT_DIRECTORIES
635 .iter()
636 .map(|path| FilesystemEntry::directory(*path))
637 .collect::<Vec<_>>();
638 entries.push(FilesystemEntry::file("/usr/bin/env", Vec::new()));
639 RootFilesystemSnapshot { entries }
640}
641
642fn convert_raw_entry(raw: RawFilesystemEntry) -> Result<FilesystemEntry, RootFilesystemError> {
643 let content = match raw.content {
644 Some(content) => match raw.encoding.as_deref() {
645 Some("base64") => Some(
646 base64::engine::general_purpose::STANDARD
647 .decode(content)
648 .map_err(|error| {
649 RootFilesystemError::new(format!(
650 "decode base64 content for {}: {error}",
651 raw.path
652 ))
653 })?,
654 ),
655 Some("utf8") | None => Some(content.into_bytes()),
656 Some(other) => {
657 return Err(RootFilesystemError::new(format!(
658 "unsupported content encoding for {}: {other}",
659 raw.path
660 )));
661 }
662 },
663 None => None,
664 };
665
666 Ok(FilesystemEntry {
667 path: raw.path,
668 kind: match raw.kind {
669 RawFilesystemEntryKind::File => FilesystemEntryKind::File,
670 RawFilesystemEntryKind::Directory => FilesystemEntryKind::Directory,
671 RawFilesystemEntryKind::Symlink => FilesystemEntryKind::Symlink,
672 },
673 mode: u32::from_str_radix(&raw.mode, 8).map_err(|error| {
674 RootFilesystemError::new(format!("parse mode {}: {error}", raw.mode))
675 })?,
676 uid: raw.uid,
677 gid: raw.gid,
678 content,
679 target: raw.target,
680 })
681}
682
683fn raw_entries_to_snapshot(
684 raw_entries: Vec<RawFilesystemEntry>,
685 limits: &RootFilesystemImportLimits,
686 context: &str,
687) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
688 if let Some(limit) = limits.max_inode_count {
689 if raw_entries.len() > limit {
690 return Err(RootFilesystemError::new(format!(
691 "{context} contains {} entries, exceeding limit {limit}",
692 raw_entries.len()
693 )));
694 }
695 }
696
697 let entries = raw_entries
698 .into_iter()
699 .map(convert_raw_entry)
700 .collect::<Result<Vec<_>, _>>()?;
701 validate_entry_import_limits(&entries, limits, context)?;
702 Ok(RootFilesystemSnapshot { entries })
703}
704
705pub fn validate_snapshot_import_limits(
706 snapshot: &RootFilesystemSnapshot,
707 limits: &RootFilesystemImportLimits,
708 context: &str,
709) -> Result<(), RootFilesystemError> {
710 validate_entry_import_limits(&snapshot.entries, limits, context)
711}
712
713fn validate_descriptor_import_limits(
714 lowers: &[RootFilesystemSnapshot],
715 bootstrap_entries: &[FilesystemEntry],
716 limits: &RootFilesystemImportLimits,
717 context: &str,
718) -> Result<(), RootFilesystemError> {
719 let explicit_entry_count = lowers
720 .iter()
721 .map(|snapshot| snapshot.entries.len())
722 .sum::<usize>()
723 .saturating_add(bootstrap_entries.len());
724 let mut inode_paths = BTreeSet::new();
725 for snapshot in lowers {
726 collect_materialized_entry_paths(&snapshot.entries, &mut inode_paths);
727 }
728 collect_materialized_entry_paths(bootstrap_entries, &mut inode_paths);
729 let inode_count = inode_paths.len();
730 if let Some(limit) = limits.max_inode_count {
731 if explicit_entry_count > limit {
732 return Err(RootFilesystemError::new(format!(
733 "{context} contains {explicit_entry_count} entries, exceeding limit {limit}"
734 )));
735 }
736
737 if inode_count > limit {
738 return Err(RootFilesystemError::new(format!(
739 "{context} contains {inode_count} entries, exceeding limit {limit}"
740 )));
741 }
742 }
743
744 let mut bytes = 0_u64;
745 for snapshot in lowers {
746 bytes = bytes.saturating_add(entry_content_bytes(&snapshot.entries));
747 }
748 bytes = bytes.saturating_add(entry_content_bytes(bootstrap_entries));
749 if let Some(limit) = limits.max_filesystem_bytes {
750 if bytes > limit {
751 return Err(RootFilesystemError::new(format!(
752 "{context} contains {bytes} bytes, exceeding limit {limit}"
753 )));
754 }
755 }
756 Ok(())
757}
758
759fn validate_entry_import_limits(
760 entries: &[FilesystemEntry],
761 limits: &RootFilesystemImportLimits,
762 context: &str,
763) -> Result<(), RootFilesystemError> {
764 if let Some(limit) = limits.max_inode_count {
765 if entries.len() > limit {
766 return Err(RootFilesystemError::new(format!(
767 "{context} contains {} entries, exceeding limit {limit}",
768 entries.len()
769 )));
770 }
771
772 let inode_count = materialized_entry_inode_count(entries);
773 if inode_count > limit {
774 return Err(RootFilesystemError::new(format!(
775 "{context} contains {inode_count} entries, exceeding limit {limit}"
776 )));
777 }
778 }
779
780 let bytes = entry_content_bytes(entries);
781 if let Some(limit) = limits.max_filesystem_bytes {
782 if bytes > limit {
783 return Err(RootFilesystemError::new(format!(
784 "{context} contains {bytes} bytes, exceeding limit {limit}"
785 )));
786 }
787 }
788 Ok(())
789}
790
791fn validate_encoded_snapshot_size(
792 bytes: &[u8],
793 limits: &RootFilesystemImportLimits,
794 context: &str,
795) -> Result<(), RootFilesystemError> {
796 if let Some(limit) = limits.max_encoded_snapshot_bytes {
797 if bytes.len() > limit {
798 return Err(RootFilesystemError::new(format!(
799 "{context} contains {} encoded bytes, exceeding limit {limit}",
800 bytes.len()
801 )));
802 }
803 }
804 Ok(())
805}
806
807fn entry_content_bytes(entries: &[FilesystemEntry]) -> u64 {
808 entries.iter().fold(0_u64, |total, entry| {
809 total.saturating_add(match entry.kind {
810 FilesystemEntryKind::File => entry
811 .content
812 .as_ref()
813 .map(|content| usize_to_u64(content.len()))
814 .unwrap_or(0),
815 FilesystemEntryKind::Directory => 0,
816 FilesystemEntryKind::Symlink => entry
817 .target
818 .as_ref()
819 .map(|target| usize_to_u64(target.len()))
820 .unwrap_or(0),
821 })
822 })
823}
824
825fn materialized_entry_inode_count(entries: &[FilesystemEntry]) -> usize {
826 let mut paths = BTreeSet::new();
827 collect_materialized_entry_paths(entries, &mut paths);
828 paths.len()
829}
830
831fn collect_materialized_entry_paths(entries: &[FilesystemEntry], paths: &mut BTreeSet<String>) {
832 for entry in entries {
833 collect_materialized_path(&entry.path, paths);
834 }
835}
836
837fn collect_materialized_path(path: &str, paths: &mut BTreeSet<String>) {
838 let normalized = normalize_path(path);
839 paths.insert(normalized.clone());
840
841 let mut parent = String::new();
842 let segments = normalized
843 .split('/')
844 .filter(|segment| !segment.is_empty())
845 .collect::<Vec<_>>();
846 for segment in segments.iter().take(segments.len().saturating_sub(1)) {
847 parent.push('/');
848 parent.push_str(segment);
849 paths.insert(parent.clone());
850 }
851}
852
853fn usize_to_u64(value: usize) -> u64 {
854 u64::try_from(value).unwrap_or(u64::MAX)
855}
856
857const fn u64_limit_to_usize(value: u64) -> usize {
858 if value > usize::MAX as u64 {
859 usize::MAX
860 } else {
861 value as usize
862 }
863}
864
865const fn encoded_snapshot_limit(
866 max_filesystem_bytes: Option<u64>,
867 max_inode_count: Option<usize>,
868) -> Option<usize> {
869 let Some(max_filesystem_bytes) = max_filesystem_bytes else {
870 return None;
871 };
872
873 Some(
874 u64_limit_to_usize(max_filesystem_bytes)
875 .saturating_mul(2)
876 .saturating_add(match max_inode_count {
877 Some(max_inode_count) => {
878 max_inode_count.saturating_mul(ROOT_FILESYSTEM_SNAPSHOT_ENTRY_OVERHEAD_BYTES)
879 }
880 None => 0,
881 })
882 .saturating_add(ROOT_FILESYSTEM_SNAPSHOT_FIXED_OVERHEAD_BYTES),
883 )
884}
885
886fn snapshot_to_memory_filesystem(
887 snapshot: &RootFilesystemSnapshot,
888) -> Result<MemoryFileSystem, RootFilesystemError> {
889 let mut filesystem = MemoryFileSystem::new();
890 for entry in sort_entries(snapshot.entries.clone()) {
891 apply_entry_to_memory_filesystem(&mut filesystem, &entry)?;
892 }
893 Ok(filesystem)
894}
895
896fn apply_entry_to_memory_filesystem(
897 filesystem: &mut MemoryFileSystem,
898 entry: &FilesystemEntry,
899) -> Result<(), RootFilesystemError> {
900 ensure_parent_directories(filesystem, &entry.path)?;
901
902 match entry.kind {
903 FilesystemEntryKind::Directory => {
904 filesystem.mkdir(&entry.path, true)?;
905 filesystem.chmod(&entry.path, entry.mode)?;
906 filesystem.chown(&entry.path, entry.uid, entry.gid)?;
907 }
908 FilesystemEntryKind::File => {
909 filesystem.write_file(&entry.path, entry.content.clone().unwrap_or_default())?;
910 filesystem.chmod(&entry.path, entry.mode)?;
911 filesystem.chown(&entry.path, entry.uid, entry.gid)?;
912 }
913 FilesystemEntryKind::Symlink => {
914 let Some(target) = entry.target.as_deref() else {
915 return Err(RootFilesystemError::new(format!(
916 "missing symlink target for {}",
917 entry.path
918 )));
919 };
920 filesystem.symlink_with_metadata(
921 target,
922 &entry.path,
923 entry.mode,
924 entry.uid,
925 entry.gid,
926 )?;
927 }
928 }
929
930 Ok(())
931}
932
933fn apply_entry(
934 filesystem: &mut impl VirtualFileSystem,
935 entry: &FilesystemEntry,
936) -> Result<(), RootFilesystemError> {
937 ensure_parent_directories(filesystem, &entry.path)?;
938
939 match entry.kind {
940 FilesystemEntryKind::Directory => {
941 filesystem.mkdir(&entry.path, true)?;
942 filesystem.chmod(&entry.path, entry.mode)?;
943 filesystem.chown(&entry.path, entry.uid, entry.gid)?;
944 }
945 FilesystemEntryKind::File => {
946 filesystem.write_file(&entry.path, entry.content.clone().unwrap_or_default())?;
947 filesystem.chmod(&entry.path, entry.mode)?;
948 filesystem.chown(&entry.path, entry.uid, entry.gid)?;
949 }
950 FilesystemEntryKind::Symlink => {
951 let Some(target) = entry.target.as_deref() else {
952 return Err(RootFilesystemError::new(format!(
953 "missing symlink target for {}",
954 entry.path
955 )));
956 };
957 filesystem.symlink(target, &entry.path)?;
958 }
959 }
960
961 Ok(())
962}
963
964fn ensure_parent_directories(
965 filesystem: &mut impl VirtualFileSystem,
966 path: &str,
967) -> Result<(), RootFilesystemError> {
968 let normalized = normalize_path(path);
969 let mut current = String::new();
970 let segments = normalized
971 .split('/')
972 .filter(|segment| !segment.is_empty())
973 .collect::<Vec<_>>();
974
975 for segment in segments.iter().take(segments.len().saturating_sub(1)) {
976 current.push('/');
977 current.push_str(segment);
978
979 if filesystem.exists(¤t) {
980 continue;
981 }
982
983 filesystem.create_dir(¤t)?;
984 filesystem.chmod(¤t, 0o755)?;
985 filesystem.chown(¤t, 0, 0)?;
986 }
987
988 Ok(())
989}
990
991fn sort_entries(mut entries: Vec<FilesystemEntry>) -> Vec<FilesystemEntry> {
992 entries.sort_by(|left, right| {
993 let depth_left = if left.path == "/" {
994 0
995 } else {
996 left.path.split('/').filter(|part| !part.is_empty()).count()
997 };
998 let depth_right = if right.path == "/" {
999 0
1000 } else {
1001 right
1002 .path
1003 .split('/')
1004 .filter(|part| !part.is_empty())
1005 .count()
1006 };
1007 depth_left
1008 .cmp(&depth_right)
1009 .then_with(|| left.path.cmp(&right.path))
1010 });
1011 entries
1012}
1013
1014fn snapshot_virtual_filesystem(
1015 filesystem: &mut impl VirtualFileSystem,
1016 root_path: &str,
1017) -> Result<Vec<FilesystemEntry>, RootFilesystemError> {
1018 let mut entries = Vec::new();
1019 snapshot_path(filesystem, root_path, &mut entries)?;
1020 Ok(entries)
1021}
1022
1023fn snapshot_path(
1024 filesystem: &mut impl VirtualFileSystem,
1025 path: &str,
1026 entries: &mut Vec<FilesystemEntry>,
1027) -> Result<(), RootFilesystemError> {
1028 let stat = if path == "/" {
1029 filesystem.stat(path)?
1030 } else {
1031 filesystem.lstat(path)?
1032 };
1033
1034 if stat.is_symbolic_link {
1035 entries.push(FilesystemEntry {
1036 path: path.to_owned(),
1037 kind: FilesystemEntryKind::Symlink,
1038 mode: stat.mode,
1039 uid: stat.uid,
1040 gid: stat.gid,
1041 content: None,
1042 target: Some(filesystem.read_link(path)?),
1043 });
1044 return Ok(());
1045 }
1046
1047 if stat.is_directory {
1048 entries.push(FilesystemEntry {
1049 path: path.to_owned(),
1050 kind: FilesystemEntryKind::Directory,
1051 mode: stat.mode,
1052 uid: stat.uid,
1053 gid: stat.gid,
1054 content: None,
1055 target: None,
1056 });
1057
1058 let mut children = filesystem
1059 .read_dir_with_types(path)?
1060 .into_iter()
1061 .map(|entry| entry.name)
1062 .filter(|name| name != "." && name != "..")
1063 .collect::<Vec<_>>();
1064 children.sort();
1065
1066 for child in children {
1067 let child_path = if path == "/" {
1068 format!("/{child}")
1069 } else {
1070 format!("{path}/{child}")
1071 };
1072 snapshot_path(filesystem, &child_path, entries)?;
1073 }
1074 return Ok(());
1075 }
1076
1077 entries.push(FilesystemEntry {
1078 path: path.to_owned(),
1079 kind: FilesystemEntryKind::File,
1080 mode: stat.mode,
1081 uid: stat.uid,
1082 gid: stat.gid,
1083 content: Some(filesystem.read_file(path)?),
1084 target: None,
1085 });
1086 Ok(())
1087}
1088
1089fn is_kernel_reserved_bootstrap_path(path: &str) -> bool {
1090 let normalized = normalize_path(path);
1091 KERNEL_RESERVED_BOOTSTRAP_PATH_PREFIXES
1092 .iter()
1093 .any(|prefix| normalized == *prefix || normalized.starts_with(&format!("{prefix}/")))
1094}