Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/hyperlight_host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-
workspace = true

[dependencies]
fs2 = "0.4"
gdbstub = { version = "0.7.10", optional = true }
gdbstub_arch = { version = "0.3.3", optional = true }
goblin = { version = "0.10", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] }
Expand Down
26 changes: 22 additions & 4 deletions src/hyperlight_host/src/sandbox/snapshot/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ mod media_types;
pub(crate) mod reference;
mod transport;

use std::fs::OpenOptions;
use std::path::{Path, PathBuf};

use fs2::FileExt;

use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;
use hyperlight_common::vmem::PAGE_SIZE;
use oci_spec::image::{
Expand Down Expand Up @@ -397,10 +400,25 @@ impl Snapshot {
}
}

// Validate any pre-existing `oci-layout` marker before
// touching anything else, so a foreign layout (future
// version, hand-edited file) is reported without altering
// the directory.
// Serialize layout validation and the index read-modify-write
// transaction across cooperating processes.
// Use a stable lock file, not index.json, which is atomically replaced.
let lock_path = path.join(".hyperlight-index.lock");
let lock_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&lock_path)
.map_err(|e| {
crate::new_error!("save: failed to open index lock {:?}: {}", lock_path, e)
})?;
lock_file.lock_exclusive().map_err(|e| {
crate::new_error!("save: failed to acquire index lock {:?}: {}", lock_path, e)
})?;
// File lock is released when lock_file is dropped on all exit paths.

// Validate the existing marker while holding the lock.
// Invalid layouts are rejected before writing snapshot data.
let layout_marker = path.join("oci-layout");
let marker_existed = layout_marker
.try_exists()
Expand Down
56 changes: 56 additions & 0 deletions src/hyperlight_host/src/sandbox/snapshot/file_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,62 @@ fn snapshot_metadata_namespaces_are_independent() {

// Round-trip via OCI layout on disk.

#[test]
fn concurrent_snapshot_saves_preserve_all_tags() {
use std::collections::HashSet;
use std::sync::Barrier;
use std::thread;

const WRITERS: usize = 8;

let snapshot = create_snapshot();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("concurrent");
let barrier = Arc::new(Barrier::new(WRITERS));

let handles: Vec<_> = (0..WRITERS)
.map(|i| {
let snapshot = Arc::clone(&snapshot);
let barrier = Arc::clone(&barrier);
let path = path.clone();

thread::spawn(move || {
let tag = format!("writer-{i}");
barrier.wait();
snapshot.save(&path, &OciTag::new(&tag).unwrap()).unwrap();
})
})
.collect();

for handle in handles {
handle.join().unwrap();
}

let index: Value =
serde_json::from_slice(&std::fs::read(path.join("index.json")).unwrap()).unwrap();

let tags: HashSet<String> = index["manifests"]
.as_array()
.unwrap()
.iter()
.map(|manifest| {
manifest["annotations"]["org.opencontainers.image.ref.name"]
.as_str()
.unwrap()
.to_owned()
})
.collect();

assert_eq!(tags.len(), WRITERS);

for i in 0..WRITERS {
assert!(
tags.contains(&format!("writer-{i}")),
"missing snapshot tag writer-{i}"
);
}
}

#[test]
fn round_trip_save_load_call() {
let snapshot = create_snapshot();
Expand Down