Skip to content

Commit cf2830e

Browse files
committed
fix(snapshot): narrow index lock scope and use std file locking
1 parent 590d626 commit cf2830e

4 files changed

Lines changed: 47 additions & 50 deletions

File tree

‎Cargo.lock‎

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/hyperlight_host/Cargo.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-
2121
workspace = true
2222

2323
[dependencies]
24-
fs2 = "0.4"
2524
gdbstub = { version = "0.7.10", optional = true }
2625
gdbstub_arch = { version = "0.3.3", optional = true }
2726
goblin = { version = "0.10", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] }

‎src/hyperlight_host/src/sandbox/snapshot/file/mod.rs‎

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ mod transport;
1414
use std::fs::OpenOptions;
1515
use std::path::{Path, PathBuf};
1616

17-
use fs2::FileExt;
18-
1917
use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;
2018
use hyperlight_common::vmem::PAGE_SIZE;
2119
use oci_spec::image::{
@@ -149,6 +147,22 @@ fn read_layout_marker(path: &Path) -> crate::Result<()> {
149147
Ok(())
150148
}
151149

150+
151+
/// Validate an existing OCI layout marker, if present.
152+
/// Returns whether the marker already exists.
153+
fn validate_existing_layout(path: &Path) -> crate::Result<bool> {
154+
let marker = path.join("oci-layout");
155+
let exists = marker
156+
.try_exists()
157+
.map_err(|e| crate::new_error!("save: failed to stat {:?}: {}", marker, e))?;
158+
159+
if exists {
160+
read_layout_marker(path)?;
161+
}
162+
163+
Ok(exists)
164+
}
165+
152166
fn load_manifest(
153167
path: &Path,
154168
blobs_dir: &Path,
@@ -400,6 +414,14 @@ impl Snapshot {
400414
}
401415
}
402416

417+
// Reject unsupported layouts before writing snapshot data.
418+
validate_existing_layout(path)?;
419+
let layout_marker = path.join("oci-layout");
420+
421+
// Generate content-addressed snapshot blobs outside the index lock.
422+
let new_desc = self.write_blobs_and_build_descriptor(path, tag, &cfg, &cfg_bytes)?;
423+
let written_digest = OciDigest::from_oci_spec_digest(new_desc.digest());
424+
403425
// Serialize layout validation and the index read-modify-write
404426
// transaction across cooperating processes.
405427
// Use a stable lock file, not index.json, which is atomically replaced.
@@ -412,40 +434,13 @@ impl Snapshot {
412434
.map_err(|e| {
413435
crate::new_error!("save: failed to open index lock {:?}: {}", lock_path, e)
414436
})?;
415-
lock_file.lock_exclusive().map_err(|e| {
437+
lock_file.lock().map_err(|e| {
416438
crate::new_error!("save: failed to acquire index lock {:?}: {}", lock_path, e)
417439
})?;
418440
// File lock is released when lock_file is dropped on all exit paths.
419441

420-
// Validate the existing marker while holding the lock.
421-
// Invalid layouts are rejected before writing snapshot data.
422-
let layout_marker = path.join("oci-layout");
423-
let marker_existed = layout_marker
424-
.try_exists()
425-
.map_err(|e| crate::new_error!("save: failed to stat {:?}: {}", layout_marker, e))?;
426-
if marker_existed {
427-
let bytes = read_bounded(&layout_marker, MAX_JSON_BLOB_SIZE).map_err(|e| {
428-
crate::new_error!("save: failed to read existing oci-layout: {}", e)
429-
})?;
430-
let v: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| {
431-
crate::new_error!("save: existing oci-layout is not valid JSON: {}", e)
432-
})?;
433-
match v.get("imageLayoutVersion").and_then(|s| s.as_str()) {
434-
Some(s) if s == OCI_LAYOUT_VERSION => {}
435-
Some(other) => {
436-
return Err(crate::new_error!(
437-
"save: existing imageLayoutVersion {:?} is unsupported (expected {:?})",
438-
other,
439-
OCI_LAYOUT_VERSION
440-
));
441-
}
442-
None => {
443-
return Err(crate::new_error!(
444-
"save: existing oci-layout is missing imageLayoutVersion"
445-
));
446-
}
447-
}
448-
}
442+
// Revalidate under the lock in case the layout changed.
443+
let marker_existed = validate_existing_layout(path)?;
449444

450445
let index_path = path.join("index.json");
451446
let index_existed = index_path
@@ -466,9 +461,6 @@ impl Snapshot {
466461
Vec::new()
467462
};
468463

469-
let new_desc = self.write_blobs_and_build_descriptor(path, tag, &cfg, &cfg_bytes)?;
470-
let written_digest = OciDigest::from_oci_spec_digest(new_desc.digest());
471-
472464
// Replacement is by tag, not by digest: a new snapshot may
473465
// hash to a different value but still claim the same logical
474466
// ref. Blobs from the replaced manifest become orphans.

‎src/hyperlight_host/src/sandbox/snapshot/file_tests.rs‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,15 @@ fn save_rejects_unsupported_existing_layout_version() {
15971597
br#"{"imageLayoutVersion":"99.0.0"}"#,
15981598
)
15991599
.unwrap();
1600+
// Capture every existing entry and its bytes before the failed save.
1601+
let before: Vec<_> = std::fs::read_dir(&path)
1602+
.unwrap()
1603+
.map(|entry| {
1604+
let entry = entry.unwrap();
1605+
(entry.file_name(), std::fs::read(entry.path()).unwrap())
1606+
})
1607+
.collect();
1608+
16001609
let err = snap
16011610
.save(&path, &OciTag::new("latest").unwrap())
16021611
.unwrap_err();
@@ -1605,9 +1614,17 @@ fn save_rejects_unsupported_existing_layout_version() {
16051614
msg.contains("imageLayoutVersion") || msg.contains("unsupported"),
16061615
"expected unsupported-version error, got: {msg}"
16071616
);
1608-
assert!(
1609-
!path.join("index.json").exists(),
1610-
"save must not have written index.json"
1617+
let after: Vec<_> = std::fs::read_dir(&path)
1618+
.unwrap()
1619+
.map(|entry| {
1620+
let entry = entry.unwrap();
1621+
(entry.file_name(), std::fs::read(entry.path()).unwrap())
1622+
})
1623+
.collect();
1624+
1625+
assert_eq!(
1626+
before, after,
1627+
"unsupported layout must leave directory contents unchanged"
16111628
);
16121629
}
16131630

0 commit comments

Comments
 (0)