Skip to main content

secure_exec_sidecar_core/
root_fs.rs

1use base64::Engine;
2use secure_exec_bridge::FilesystemSnapshot;
3use secure_exec_kernel::mount_table::MountTable;
4use secure_exec_kernel::root_fs::{
5    decode_snapshot_with_import_limits, is_supported_root_filesystem_snapshot_format,
6    FilesystemEntry, FilesystemEntryKind, RootFileSystem,
7    RootFilesystemDescriptor as KernelRootFilesystemDescriptor, RootFilesystemImportLimits,
8    RootFilesystemMode as KernelRootFilesystemMode, RootFilesystemSnapshot,
9};
10use secure_exec_kernel::vfs::{normalize_path, VirtualFileSystem};
11use secure_exec_sidecar_protocol::protocol::{
12    RootFilesystemDescriptor as ProtocolRootFilesystemDescriptor,
13    RootFilesystemEntry as ProtocolRootFilesystemEntry,
14    RootFilesystemEntryEncoding as ProtocolRootFilesystemEntryEncoding,
15    RootFilesystemEntryKind as ProtocolRootFilesystemEntryKind,
16    RootFilesystemLowerDescriptor as ProtocolRootFilesystemLowerDescriptor,
17    RootFilesystemMode as ProtocolRootFilesystemMode,
18    SnapshotRootFilesystemLower as ProtocolSnapshotRootFilesystemLower,
19};
20use secure_exec_vm_config as vm_config;
21use std::error::Error;
22use std::fmt;
23use vfs::posix::usage::RootFilesystemResourceLimits;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct SidecarCoreError {
27    message: String,
28}
29
30impl SidecarCoreError {
31    pub fn new(message: impl Into<String>) -> Self {
32        Self {
33            message: message.into(),
34        }
35    }
36}
37
38impl fmt::Display for SidecarCoreError {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.write_str(&self.message)
41    }
42}
43
44impl Error for SidecarCoreError {}
45
46pub fn root_filesystem_descriptor_from_config(
47    config: &vm_config::RootFilesystemConfig,
48) -> Result<KernelRootFilesystemDescriptor, SidecarCoreError> {
49    root_filesystem_descriptor_from_config_with_import_limits(
50        config,
51        &RootFilesystemImportLimits::default(),
52    )
53}
54
55fn root_filesystem_descriptor_from_config_with_import_limits(
56    config: &vm_config::RootFilesystemConfig,
57    import_limits: &RootFilesystemImportLimits,
58) -> Result<KernelRootFilesystemDescriptor, SidecarCoreError> {
59    Ok(KernelRootFilesystemDescriptor {
60        mode: root_filesystem_mode_from_config(config.mode),
61        disable_default_base_layer: config.disable_default_base_layer,
62        lowers: config
63            .lowers
64            .iter()
65            .map(|lower| root_filesystem_lower_from_config(lower, import_limits))
66            .collect::<Result<Vec<_>, _>>()?,
67        bootstrap_entries: config
68            .bootstrap_entries
69            .iter()
70            .map(root_filesystem_entry_from_config)
71            .collect::<Result<Vec<_>, _>>()?,
72    })
73}
74
75pub fn root_filesystem_protocol_descriptor_from_config(
76    config: &vm_config::RootFilesystemConfig,
77) -> ProtocolRootFilesystemDescriptor {
78    ProtocolRootFilesystemDescriptor {
79        mode: match config.mode {
80            vm_config::RootFilesystemMode::Ephemeral => ProtocolRootFilesystemMode::Ephemeral,
81            vm_config::RootFilesystemMode::ReadOnly => ProtocolRootFilesystemMode::ReadOnly,
82        },
83        disable_default_base_layer: config.disable_default_base_layer,
84        lowers: config
85            .lowers
86            .iter()
87            .map(root_filesystem_protocol_lower_from_config)
88            .collect(),
89        bootstrap_entries: config
90            .bootstrap_entries
91            .iter()
92            .map(root_filesystem_protocol_entry_from_config)
93            .collect(),
94    }
95}
96
97pub fn build_root_filesystem(
98    config: &vm_config::RootFilesystemConfig,
99    limits: &impl RootFilesystemResourceLimits,
100) -> Result<RootFileSystem, SidecarCoreError> {
101    build_root_filesystem_with_loaded_snapshot(config, None, limits)
102}
103
104pub fn build_root_filesystem_with_loaded_snapshot(
105    config: &vm_config::RootFilesystemConfig,
106    loaded_snapshot: Option<&FilesystemSnapshot>,
107    limits: &impl RootFilesystemResourceLimits,
108) -> Result<RootFileSystem, SidecarCoreError> {
109    let import_limits = RootFilesystemImportLimits::from_resource_limits(limits);
110    let descriptor = if let Some(restored) = supported_loaded_snapshot(loaded_snapshot) {
111        KernelRootFilesystemDescriptor {
112            mode: root_filesystem_mode_from_config(config.mode),
113            disable_default_base_layer: true,
114            lowers: vec![
115                decode_snapshot_with_import_limits(&restored.bytes, &import_limits).map_err(
116                    |error| {
117                        SidecarCoreError::new(format!("decode restored root filesystem: {error}"))
118                    },
119                )?,
120            ],
121            bootstrap_entries: config
122                .bootstrap_entries
123                .iter()
124                .map(root_filesystem_entry_from_config)
125                .collect::<Result<Vec<_>, _>>()?,
126        }
127    } else {
128        root_filesystem_descriptor_from_config_with_import_limits(config, &import_limits)?
129    };
130    RootFileSystem::from_descriptor_with_import_limits(descriptor, &import_limits)
131        .map_err(|error| SidecarCoreError::new(format!("build root filesystem: {error}")))
132}
133
134pub fn build_root_mount_table(
135    config: &vm_config::RootFilesystemConfig,
136    limits: &impl RootFilesystemResourceLimits,
137) -> Result<MountTable, SidecarCoreError> {
138    Ok(MountTable::new(build_root_filesystem(config, limits)?))
139}
140
141pub fn build_root_mount_table_with_loaded_snapshot(
142    config: &vm_config::RootFilesystemConfig,
143    loaded_snapshot: Option<&FilesystemSnapshot>,
144    limits: &impl RootFilesystemResourceLimits,
145) -> Result<MountTable, SidecarCoreError> {
146    Ok(MountTable::new(build_root_filesystem_with_loaded_snapshot(
147        config,
148        loaded_snapshot,
149        limits,
150    )?))
151}
152
153pub fn root_filesystem_mode_from_config(
154    mode: vm_config::RootFilesystemMode,
155) -> KernelRootFilesystemMode {
156    match mode {
157        vm_config::RootFilesystemMode::Ephemeral => KernelRootFilesystemMode::Ephemeral,
158        vm_config::RootFilesystemMode::ReadOnly => KernelRootFilesystemMode::ReadOnly,
159    }
160}
161
162pub fn protocol_root_filesystem_mode(mode: ProtocolRootFilesystemMode) -> KernelRootFilesystemMode {
163    match mode {
164        ProtocolRootFilesystemMode::Ephemeral => KernelRootFilesystemMode::Ephemeral,
165        ProtocolRootFilesystemMode::ReadOnly => KernelRootFilesystemMode::ReadOnly,
166    }
167}
168
169fn supported_loaded_snapshot(snapshot: Option<&FilesystemSnapshot>) -> Option<&FilesystemSnapshot> {
170    snapshot.filter(|snapshot| is_supported_root_filesystem_snapshot_format(&snapshot.format))
171}
172
173fn root_filesystem_lower_from_config(
174    lower: &vm_config::RootFilesystemLowerDescriptor,
175    import_limits: &RootFilesystemImportLimits,
176) -> Result<RootFilesystemSnapshot, SidecarCoreError> {
177    match lower {
178        vm_config::RootFilesystemLowerDescriptor::Snapshot { entries } => {
179            Ok(RootFilesystemSnapshot {
180                entries: entries
181                    .iter()
182                    .map(root_filesystem_entry_from_config)
183                    .collect::<Result<Vec<_>, _>>()?,
184            })
185        }
186        vm_config::RootFilesystemLowerDescriptor::BundledBaseFilesystem => Ok(
187            secure_exec_kernel::root_fs::load_bundled_base_snapshot_with_limits(import_limits)
188                .map_err(|error| {
189                    SidecarCoreError::new(format!("load bundled base filesystem lower: {error}"))
190                })?,
191        ),
192    }
193}
194
195fn root_filesystem_entry_from_config(
196    entry: &vm_config::RootFilesystemEntry,
197) -> Result<FilesystemEntry, SidecarCoreError> {
198    let mode = entry.mode.unwrap_or(match entry.kind {
199        vm_config::RootFilesystemEntryKind::File => {
200            if entry.executable {
201                0o755
202            } else {
203                0o644
204            }
205        }
206        vm_config::RootFilesystemEntryKind::Directory => 0o755,
207        vm_config::RootFilesystemEntryKind::Symlink => 0o777,
208    });
209
210    let content = match entry.content.as_ref() {
211        Some(content) => match entry.encoding {
212            Some(vm_config::RootFilesystemEntryEncoding::Base64) => Some(
213                base64::engine::general_purpose::STANDARD
214                    .decode(content)
215                    .map_err(|error| {
216                        SidecarCoreError::new(format!(
217                            "invalid base64 root filesystem content for {}: {error}",
218                            entry.path
219                        ))
220                    })?,
221            ),
222            Some(vm_config::RootFilesystemEntryEncoding::Utf8) | None => {
223                Some(content.as_bytes().to_vec())
224            }
225        },
226        None => None,
227    };
228
229    Ok(FilesystemEntry {
230        path: normalize_path(&entry.path),
231        kind: match entry.kind {
232            vm_config::RootFilesystemEntryKind::File => FilesystemEntryKind::File,
233            vm_config::RootFilesystemEntryKind::Directory => FilesystemEntryKind::Directory,
234            vm_config::RootFilesystemEntryKind::Symlink => FilesystemEntryKind::Symlink,
235        },
236        mode,
237        uid: entry.uid.unwrap_or(0),
238        gid: entry.gid.unwrap_or(0),
239        content,
240        target: entry.target.clone(),
241    })
242}
243
244fn root_filesystem_protocol_lower_from_config(
245    lower: &vm_config::RootFilesystemLowerDescriptor,
246) -> ProtocolRootFilesystemLowerDescriptor {
247    match lower {
248        vm_config::RootFilesystemLowerDescriptor::Snapshot { entries } => {
249            ProtocolRootFilesystemLowerDescriptor::SnapshotRootFilesystemLower(
250                ProtocolSnapshotRootFilesystemLower {
251                    entries: entries
252                        .iter()
253                        .map(root_filesystem_protocol_entry_from_config)
254                        .collect(),
255                },
256            )
257        }
258        vm_config::RootFilesystemLowerDescriptor::BundledBaseFilesystem => {
259            ProtocolRootFilesystemLowerDescriptor::BundledBaseFilesystemLower
260        }
261    }
262}
263
264fn root_filesystem_protocol_entry_from_config(
265    entry: &vm_config::RootFilesystemEntry,
266) -> ProtocolRootFilesystemEntry {
267    ProtocolRootFilesystemEntry {
268        path: entry.path.clone(),
269        kind: match entry.kind {
270            vm_config::RootFilesystemEntryKind::File => ProtocolRootFilesystemEntryKind::File,
271            vm_config::RootFilesystemEntryKind::Directory => {
272                ProtocolRootFilesystemEntryKind::Directory
273            }
274            vm_config::RootFilesystemEntryKind::Symlink => ProtocolRootFilesystemEntryKind::Symlink,
275        },
276        mode: entry.mode,
277        uid: entry.uid,
278        gid: entry.gid,
279        content: entry.content.clone(),
280        encoding: entry.encoding.map(|encoding| match encoding {
281            vm_config::RootFilesystemEntryEncoding::Utf8 => {
282                ProtocolRootFilesystemEntryEncoding::Utf8
283            }
284            vm_config::RootFilesystemEntryEncoding::Base64 => {
285                ProtocolRootFilesystemEntryEncoding::Base64
286            }
287        }),
288        target: entry.target.clone(),
289        executable: entry.executable,
290    }
291}
292
293pub fn root_snapshot_entry(entry: &FilesystemEntry) -> ProtocolRootFilesystemEntry {
294    let (content, encoding) = entry
295        .content
296        .clone()
297        .map(snapshot_entry_content)
298        .map(|(content, encoding)| (Some(content), Some(encoding)))
299        .unwrap_or((None, None));
300
301    ProtocolRootFilesystemEntry {
302        path: entry.path.clone(),
303        kind: match entry.kind {
304            FilesystemEntryKind::File => ProtocolRootFilesystemEntryKind::File,
305            FilesystemEntryKind::Directory => ProtocolRootFilesystemEntryKind::Directory,
306            FilesystemEntryKind::Symlink => ProtocolRootFilesystemEntryKind::Symlink,
307        },
308        mode: Some(entry.mode),
309        uid: Some(entry.uid),
310        gid: Some(entry.gid),
311        content,
312        encoding,
313        target: entry.target.clone(),
314        executable: entry.mode & 0o111 != 0,
315    }
316}
317
318/// Convert a protocol root-filesystem entry into a kernel `FilesystemEntry`, decoding
319/// content (utf8/base64) and applying per-kind mode defaults. Shared by native and
320/// browser bootstrap + snapshot paths.
321pub fn convert_root_filesystem_entry(
322    entry: &ProtocolRootFilesystemEntry,
323) -> Result<FilesystemEntry, SidecarCoreError> {
324    let mode = entry.mode.unwrap_or(match entry.kind {
325        ProtocolRootFilesystemEntryKind::File => {
326            if entry.executable {
327                0o755
328            } else {
329                0o644
330            }
331        }
332        ProtocolRootFilesystemEntryKind::Directory => 0o755,
333        ProtocolRootFilesystemEntryKind::Symlink => 0o777,
334    });
335
336    let content = match entry.content.as_ref() {
337        Some(content) => match entry.encoding {
338            Some(ProtocolRootFilesystemEntryEncoding::Base64) => Some(
339                base64::engine::general_purpose::STANDARD
340                    .decode(content)
341                    .map_err(|error| {
342                        SidecarCoreError::new(format!(
343                            "invalid base64 root filesystem content for {}: {error}",
344                            entry.path
345                        ))
346                    })?,
347            ),
348            Some(ProtocolRootFilesystemEntryEncoding::Utf8) | None => {
349                Some(content.as_bytes().to_vec())
350            }
351        },
352        None => None,
353    };
354
355    Ok(FilesystemEntry {
356        path: normalize_path(&entry.path),
357        kind: match entry.kind {
358            ProtocolRootFilesystemEntryKind::File => FilesystemEntryKind::File,
359            ProtocolRootFilesystemEntryKind::Directory => FilesystemEntryKind::Directory,
360            ProtocolRootFilesystemEntryKind::Symlink => FilesystemEntryKind::Symlink,
361        },
362        mode,
363        uid: entry.uid.unwrap_or(0),
364        gid: entry.gid.unwrap_or(0),
365        content,
366        target: entry.target.clone(),
367    })
368}
369
370/// Build a kernel snapshot from protocol entries (shared by native + browser).
371pub fn root_snapshot_from_entries(
372    entries: &[ProtocolRootFilesystemEntry],
373) -> Result<RootFilesystemSnapshot, SidecarCoreError> {
374    Ok(RootFilesystemSnapshot {
375        entries: entries
376            .iter()
377            .map(convert_root_filesystem_entry)
378            .collect::<Result<Vec<_>, _>>()?,
379    })
380}
381
382/// Write one root-filesystem bootstrap entry (file/dir/symlink) into a VFS, creating
383/// parent dirs and applying deterministic mode/uid/gid defaults (chmod/chown for
384/// non-symlinks). Shared by native (raw root FS) and browser (kernel `filesystem_mut`).
385pub fn apply_root_filesystem_entry<F: VirtualFileSystem>(
386    filesystem: &mut F,
387    entry: &ProtocolRootFilesystemEntry,
388) -> Result<(), SidecarCoreError> {
389    let kernel_entry = convert_root_filesystem_entry(entry)?;
390
391    let parent = parent_directory(&kernel_entry.path);
392    if parent != "/" && !filesystem.exists(&parent) {
393        filesystem.mkdir(&parent, true).map_err(vfs_err)?;
394    }
395
396    match kernel_entry.kind {
397        FilesystemEntryKind::Directory => filesystem
398            .mkdir(&kernel_entry.path, true)
399            .map_err(vfs_err)?,
400        FilesystemEntryKind::File => filesystem
401            .write_file(&kernel_entry.path, kernel_entry.content.unwrap_or_default())
402            .map_err(vfs_err)?,
403        FilesystemEntryKind::Symlink => filesystem
404            .symlink(
405                kernel_entry.target.as_deref().ok_or_else(|| {
406                    SidecarCoreError::new(format!(
407                        "root filesystem bootstrap for symlink {} requires a target",
408                        entry.path
409                    ))
410                })?,
411                &kernel_entry.path,
412            )
413            .map_err(vfs_err)?,
414    }
415
416    if !matches!(kernel_entry.kind, FilesystemEntryKind::Symlink) {
417        filesystem
418            .chmod(&kernel_entry.path, kernel_entry.mode)
419            .map_err(vfs_err)?;
420        filesystem
421            .chown(&kernel_entry.path, kernel_entry.uid, kernel_entry.gid)
422            .map_err(vfs_err)?;
423    }
424
425    Ok(())
426}
427
428fn vfs_err<E: fmt::Display>(error: E) -> SidecarCoreError {
429    SidecarCoreError::new(error.to_string())
430}
431
432fn parent_directory(path: &str) -> String {
433    match path.rfind('/') {
434        Some(0) | None => String::from("/"),
435        Some(index) => path[..index].to_string(),
436    }
437}
438
439fn snapshot_entry_content(content: Vec<u8>) -> (String, ProtocolRootFilesystemEntryEncoding) {
440    match String::from_utf8(content) {
441        Ok(text) => (text, ProtocolRootFilesystemEntryEncoding::Utf8),
442        Err(error) => (
443            base64::engine::general_purpose::STANDARD.encode(error.into_bytes()),
444            ProtocolRootFilesystemEntryEncoding::Base64,
445        ),
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use secure_exec_kernel::resource_accounting::ResourceLimits;
453    use secure_exec_kernel::vfs::VirtualFileSystem;
454
455    #[test]
456    fn builds_root_filesystem_from_snapshot_lower_and_bootstrap_upper() {
457        let mut root = build_root_filesystem(
458            &vm_config::RootFilesystemConfig {
459                disable_default_base_layer: true,
460                lowers: vec![vm_config::RootFilesystemLowerDescriptor::Snapshot {
461                    entries: vec![vm_config::RootFilesystemEntry {
462                        path: String::from("/workspace/value.txt"),
463                        kind: vm_config::RootFilesystemEntryKind::File,
464                        mode: None,
465                        uid: None,
466                        gid: None,
467                        content: Some(String::from("lower")),
468                        encoding: Some(vm_config::RootFilesystemEntryEncoding::Utf8),
469                        target: None,
470                        executable: false,
471                    }],
472                }],
473                bootstrap_entries: vec![vm_config::RootFilesystemEntry {
474                    path: String::from("/workspace/value.txt"),
475                    kind: vm_config::RootFilesystemEntryKind::File,
476                    mode: None,
477                    uid: None,
478                    gid: None,
479                    content: Some(String::from("upper")),
480                    encoding: Some(vm_config::RootFilesystemEntryEncoding::Utf8),
481                    target: None,
482                    executable: false,
483                }],
484                ..vm_config::RootFilesystemConfig::default()
485            },
486            &ResourceLimits::default(),
487        )
488        .expect("build root filesystem");
489
490        assert_eq!(
491            root.read_file("/workspace/value.txt")
492                .expect("read merged value"),
493            b"upper".to_vec()
494        );
495    }
496
497    #[test]
498    fn decodes_base64_root_filesystem_entries() {
499        let mut root = build_root_filesystem(
500            &vm_config::RootFilesystemConfig {
501                disable_default_base_layer: true,
502                bootstrap_entries: vec![vm_config::RootFilesystemEntry {
503                    path: String::from("/bin/tool"),
504                    kind: vm_config::RootFilesystemEntryKind::File,
505                    mode: None,
506                    uid: None,
507                    gid: None,
508                    content: Some(String::from("dG9vbA==")),
509                    encoding: Some(vm_config::RootFilesystemEntryEncoding::Base64),
510                    target: None,
511                    executable: true,
512                }],
513                ..vm_config::RootFilesystemConfig::default()
514            },
515            &ResourceLimits::default(),
516        )
517        .expect("build root filesystem");
518
519        assert_eq!(root.read_file("/bin/tool").expect("read file"), b"tool");
520        assert_eq!(
521            root.stat("/bin/tool").expect("stat file").mode & 0o777,
522            0o755
523        );
524    }
525
526    #[test]
527    fn serializes_root_snapshot_entries_as_utf8_or_base64() {
528        let text = root_snapshot_entry(&FilesystemEntry {
529            path: String::from("/workspace/text.txt"),
530            kind: FilesystemEntryKind::File,
531            mode: 0o755,
532            uid: 501,
533            gid: 20,
534            content: Some(b"hello".to_vec()),
535            target: None,
536        });
537
538        assert_eq!(text.path, "/workspace/text.txt");
539        assert_eq!(text.kind, ProtocolRootFilesystemEntryKind::File);
540        assert_eq!(text.mode, Some(0o755));
541        assert_eq!(text.uid, Some(501));
542        assert_eq!(text.gid, Some(20));
543        assert_eq!(text.content.as_deref(), Some("hello"));
544        assert_eq!(
545            text.encoding,
546            Some(ProtocolRootFilesystemEntryEncoding::Utf8)
547        );
548        assert!(text.executable);
549
550        let binary = root_snapshot_entry(&FilesystemEntry {
551            path: String::from("/workspace/binary.bin"),
552            kind: FilesystemEntryKind::File,
553            mode: 0o644,
554            uid: 0,
555            gid: 0,
556            content: Some(vec![0xff, 0x00]),
557            target: None,
558        });
559
560        assert_eq!(binary.content.as_deref(), Some("/wA="));
561        assert_eq!(
562            binary.encoding,
563            Some(ProtocolRootFilesystemEntryEncoding::Base64)
564        );
565        assert!(!binary.executable);
566    }
567
568    #[test]
569    fn builds_protocol_descriptor_from_config_without_normalizing_optional_fields() {
570        let descriptor =
571            root_filesystem_protocol_descriptor_from_config(&vm_config::RootFilesystemConfig {
572                mode: vm_config::RootFilesystemMode::ReadOnly,
573                disable_default_base_layer: true,
574                lowers: vec![
575                    vm_config::RootFilesystemLowerDescriptor::BundledBaseFilesystem,
576                    vm_config::RootFilesystemLowerDescriptor::Snapshot {
577                        entries: vec![vm_config::RootFilesystemEntry {
578                            path: String::from("relative/lower.txt"),
579                            kind: vm_config::RootFilesystemEntryKind::File,
580                            mode: None,
581                            uid: None,
582                            gid: None,
583                            content: Some(String::from("lower")),
584                            encoding: None,
585                            target: None,
586                            executable: false,
587                        }],
588                    },
589                ],
590                bootstrap_entries: vec![vm_config::RootFilesystemEntry {
591                    path: String::from("/bin/tool"),
592                    kind: vm_config::RootFilesystemEntryKind::File,
593                    mode: Some(0o700),
594                    uid: Some(1000),
595                    gid: Some(1000),
596                    content: Some(String::from("dG9vbA==")),
597                    encoding: Some(vm_config::RootFilesystemEntryEncoding::Base64),
598                    target: None,
599                    executable: true,
600                }],
601            });
602
603        assert_eq!(descriptor.mode, ProtocolRootFilesystemMode::ReadOnly);
604        assert!(descriptor.disable_default_base_layer);
605        assert_eq!(descriptor.lowers.len(), 2);
606        assert!(matches!(
607            descriptor.lowers[0],
608            ProtocolRootFilesystemLowerDescriptor::BundledBaseFilesystemLower
609        ));
610        let ProtocolRootFilesystemLowerDescriptor::SnapshotRootFilesystemLower(snapshot) =
611            &descriptor.lowers[1]
612        else {
613            panic!("expected snapshot lower");
614        };
615        assert_eq!(snapshot.entries[0].path, "relative/lower.txt");
616        assert_eq!(snapshot.entries[0].mode, None);
617        assert_eq!(snapshot.entries[0].encoding, None);
618
619        let entry = &descriptor.bootstrap_entries[0];
620        assert_eq!(entry.path, "/bin/tool");
621        assert_eq!(entry.kind, ProtocolRootFilesystemEntryKind::File);
622        assert_eq!(entry.mode, Some(0o700));
623        assert_eq!(entry.uid, Some(1000));
624        assert_eq!(entry.gid, Some(1000));
625        assert_eq!(entry.content.as_deref(), Some("dG9vbA=="));
626        assert_eq!(
627            entry.encoding,
628            Some(ProtocolRootFilesystemEntryEncoding::Base64)
629        );
630        assert!(entry.executable);
631    }
632
633    #[test]
634    fn maps_root_filesystem_modes_to_kernel_modes() {
635        assert_eq!(
636            root_filesystem_mode_from_config(vm_config::RootFilesystemMode::Ephemeral),
637            KernelRootFilesystemMode::Ephemeral
638        );
639        assert_eq!(
640            root_filesystem_mode_from_config(vm_config::RootFilesystemMode::ReadOnly),
641            KernelRootFilesystemMode::ReadOnly
642        );
643        assert_eq!(
644            protocol_root_filesystem_mode(ProtocolRootFilesystemMode::Ephemeral),
645            KernelRootFilesystemMode::Ephemeral
646        );
647        assert_eq!(
648            protocol_root_filesystem_mode(ProtocolRootFilesystemMode::ReadOnly),
649            KernelRootFilesystemMode::ReadOnly
650        );
651    }
652
653    #[test]
654    fn restored_snapshot_replaces_config_lowers_but_preserves_bootstrap_entries() {
655        let restored = RootFilesystemSnapshot {
656            entries: vec![FilesystemEntry::file(
657                "/workspace/restored.txt",
658                b"restored",
659            )],
660        };
661        let loaded_snapshot = FilesystemSnapshot {
662            format: String::from(secure_exec_kernel::root_fs::ROOT_FILESYSTEM_SNAPSHOT_FORMAT),
663            bytes: secure_exec_kernel::root_fs::encode_snapshot(&restored)
664                .expect("encode restored snapshot"),
665        };
666        let mut root = build_root_filesystem_with_loaded_snapshot(
667            &vm_config::RootFilesystemConfig {
668                disable_default_base_layer: true,
669                lowers: vec![vm_config::RootFilesystemLowerDescriptor::Snapshot {
670                    entries: vec![vm_config::RootFilesystemEntry {
671                        path: String::from("/workspace/ignored-lower.txt"),
672                        kind: vm_config::RootFilesystemEntryKind::File,
673                        mode: None,
674                        uid: None,
675                        gid: None,
676                        content: Some(String::from("ignored")),
677                        encoding: Some(vm_config::RootFilesystemEntryEncoding::Utf8),
678                        target: None,
679                        executable: false,
680                    }],
681                }],
682                bootstrap_entries: vec![vm_config::RootFilesystemEntry {
683                    path: String::from("/workspace/bootstrap.txt"),
684                    kind: vm_config::RootFilesystemEntryKind::File,
685                    mode: None,
686                    uid: None,
687                    gid: None,
688                    content: Some(String::from("bootstrap")),
689                    encoding: Some(vm_config::RootFilesystemEntryEncoding::Utf8),
690                    target: None,
691                    executable: false,
692                }],
693                ..vm_config::RootFilesystemConfig::default()
694            },
695            Some(&loaded_snapshot),
696            &ResourceLimits::default(),
697        )
698        .expect("build root filesystem from restored snapshot");
699
700        assert_eq!(
701            root.read_file("/workspace/restored.txt")
702                .expect("read restored file"),
703            b"restored".to_vec()
704        );
705        assert_eq!(
706            root.read_file("/workspace/bootstrap.txt")
707                .expect("read bootstrap file"),
708            b"bootstrap".to_vec()
709        );
710        assert!(
711            root.read_file("/workspace/ignored-lower.txt").is_err(),
712            "restored snapshots should replace configured lowers"
713        );
714    }
715}