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
318pub 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
370pub 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
382pub 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}