Generalize safe host-to-isolated rename migration

This commit is contained in:
ms2k 2026-07-29 22:20:04 +09:00
commit 35ab7f17c0
13 changed files with 589 additions and 81 deletions

1
Cargo.lock generated
View file

@ -1787,6 +1787,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.11.0", "sha2 0.11.0",
"xattr",
] ]
[[package]] [[package]]

View file

@ -53,6 +53,7 @@ tar = "=0.4.46"
time = "=0.3.53" time = "=0.3.53"
tokio = { version = "=1.52.3", features = ["fs", "io-util", "process", "rt"] } tokio = { version = "=1.52.3", features = ["fs", "io-util", "process", "rt"] }
toml = { version = "=1.1.3", default-features = false, features = ["parse", "serde", "std"] } toml = { version = "=1.1.3", default-features = false, features = ["parse", "serde", "std"] }
xattr = "=1.6.1"
x509-parser = "=0.18.1" x509-parser = "=0.18.1"
zstd = "=0.13.3" zstd = "=0.13.3"

View file

@ -16,6 +16,7 @@ rustix.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
sha2.workspace = true sha2.workspace = true
xattr.workspace = true
[dev-dependencies] [dev-dependencies]

View file

@ -3,14 +3,15 @@ use std::fs::File;
use std::io; use std::io;
use std::io::Read; use std::io::Read;
use std::io::Write; use std::io::Write;
use std::os::fd::AsFd; use std::os::fd::{AsFd, AsRawFd};
use std::os::unix::ffi::{OsStrExt, OsStringExt}; use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Component, Path}; use std::path::{Component, Path, PathBuf};
use rustix::fs::{ use rustix::fs::{
AtFlags, Mode, OFlags, RawDir, RenameFlags, ResolveFlags, Stat, Timestamps, linkat, mkdirat, AtFlags, Mode, OFlags, RawDir, RenameFlags, ResolveFlags, Stat, Timestamps, chownat, linkat,
openat, openat2, readlinkat, renameat_with, statat, symlinkat, unlinkat, utimensat, mkdirat, openat, openat2, readlinkat, renameat_with, statat, symlinkat, unlinkat, utimensat,
}; };
use rustix::process::{Gid, Uid};
const RESOLVE: ResolveFlags = ResolveFlags::BENEATH const RESOLVE: ResolveFlags = ResolveFlags::BENEATH
.union(ResolveFlags::NO_MAGICLINKS) .union(ResolveFlags::NO_MAGICLINKS)
@ -196,6 +197,14 @@ impl BackingRoot {
Ok(readlinkat(parent.as_fd(), name, Vec::new())?.into_bytes()) Ok(readlinkat(parent.as_fd(), name, Vec::new())?.into_bytes())
} }
pub(crate) fn symlink_xattrs(&self, relative: &Path) -> io::Result<Vec<OsString>> {
let (parent, name) = split(relative)?;
let parent = self.open_directory(parent)?;
let mut path = format!("/proc/self/fd/{}/", parent.as_raw_fd()).into_bytes();
path.extend_from_slice(name.as_bytes());
xattr::list(PathBuf::from(OsString::from_vec(path))).map(Iterator::collect)
}
pub(crate) fn set_symlink_times( pub(crate) fn set_symlink_times(
&self, &self,
relative: &Path, relative: &Path,
@ -211,6 +220,18 @@ impl BackingRoot {
)?) )?)
} }
pub(crate) fn set_symlink_owner(&self, relative: &Path, uid: Uid, gid: Gid) -> io::Result<()> {
let (parent, name) = split(relative)?;
let parent = self.open_directory(parent)?;
Ok(chownat(
parent.as_fd(),
name,
Some(uid),
Some(gid),
AtFlags::SYMLINK_NOFOLLOW,
)?)
}
pub(crate) fn entries(&self, relative: &Path) -> io::Result<Vec<OsString>> { pub(crate) fn entries(&self, relative: &Path) -> io::Result<Vec<OsString>> {
Ok(self Ok(self
.typed_entries(relative)? .typed_entries(relative)?

View file

@ -351,7 +351,6 @@ impl Filesystem for IgnoredWorkspace {
if source_entry.backing != destination_backing { if source_entry.backing != destination_backing {
let supported_flags = let supported_flags =
flags.is_empty() || flags == FuseRenameFlags::RENAME_NOREPLACE; flags.is_empty() || flags == FuseRenameFlags::RENAME_NOREPLACE;
let directory = source_entry.stat.st_mode & libc::S_IFMT == libc::S_IFDIR;
if source_entry.backing != Classification::Host if source_entry.backing != Classification::Host
|| destination_backing != Classification::Isolated || destination_backing != Classification::Isolated
{ {
@ -364,10 +363,13 @@ impl Filesystem for IgnoredWorkspace {
); );
return Err(io::Error::from_raw_os_error(libc::EXDEV)); return Err(io::Error::from_raw_os_error(libc::EXDEV));
} }
if !directory { if !matches!(
source_entry.stat.st_mode & libc::S_IFMT,
libc::S_IFREG | libc::S_IFDIR | libc::S_IFLNK
) {
let _ = writeln!( let _ = writeln!(
io::stderr().lock(), io::stderr().lock(),
"lazaret ignored-workspace refused host-to-isolated migration: source is not a directory" "lazaret ignored-workspace refused host-to-isolated migration: source type has no copy semantics"
); );
return Err(io::Error::from_raw_os_error(libc::EXDEV)); return Err(io::Error::from_raw_os_error(libc::EXDEV));
} }
@ -388,6 +390,7 @@ impl Filesystem for IgnoredWorkspace {
&provenance, &provenance,
&source, &source,
&destination, &destination,
flags.is_empty(),
)?; )?;
provenance.remove_tree(&source); provenance.remove_tree(&source);
return Ok(()); return Ok(());
@ -492,6 +495,10 @@ impl Filesystem for IgnoredWorkspace {
_lock_owner: Option<LockOwner>, _lock_owner: Option<LockOwner>,
reply: ReplyWrite, reply: ReplyWrite,
) { ) {
let _namespace = self
.namespace
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let result = self.with_file(handle, |open| { let result = self.with_file(handle, |open| {
open.migration_quiesced.store(false, Ordering::Release); open.migration_quiesced.store(false, Ordering::Release);
let offset = if flags.0 & libc::O_APPEND != 0 { let offset = if flags.0 & libc::O_APPEND != 0 {
@ -1005,6 +1012,10 @@ impl Filesystem for IgnoredWorkspace {
reply.error(Errno::EINVAL); reply.error(Errno::EINVAL);
return; return;
} }
let _namespace = self
.namespace
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let files = self let files = self
.files .files
.lock() .lock()

View file

@ -321,6 +321,75 @@ fn physical_router_migrates_cargo_target_only_toward_isolated_state() {
session.umount_and_join().unwrap(); session.umount_and_join().unwrap();
} }
#[test]
#[ignore = "requires a usable host FUSE device"]
fn physical_router_replaces_isolated_regular_file_only_toward_isolated_state() {
let fixture = Fixture::new();
repository(&fixture.workspace);
fs::write(fixture.workspace.join(".gitignore"), b"dump.rdb\n").unwrap();
let filesystem = IgnoredWorkspace::open(&fixture.workspace, &fixture.isolated, None).unwrap();
let mut config = Config::default();
config.mount_options.extend([
MountOption::RW,
MountOption::DefaultPermissions,
MountOption::NoDev,
MountOption::NoSuid,
]);
let session = fuser::spawn_mount2(filesystem, &fixture.mountpoint, &config).unwrap();
fs::write(fixture.mountpoint.join("dump.rdb"), b"old").unwrap();
fs::write(fixture.mountpoint.join("temporary.rdb"), b"new").unwrap();
fs::rename(
fixture.mountpoint.join("temporary.rdb"),
fixture.mountpoint.join("dump.rdb"),
)
.unwrap();
assert!(!fixture.workspace.join("temporary.rdb").exists());
assert!(!fixture.workspace.join("dump.rdb").exists());
assert_eq!(fs::read(fixture.isolated.join("dump.rdb")).unwrap(), b"new");
let error = fs::rename(
fixture.mountpoint.join("dump.rdb"),
fixture.mountpoint.join("exported.rdb"),
)
.unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::EXDEV));
assert!(!fixture.workspace.join("exported.rdb").exists());
fs::write(fixture.mountpoint.join("open-source"), b"open").unwrap();
let open = OpenOptions::new()
.read(true)
.write(true)
.open(fixture.mountpoint.join("open-source"))
.unwrap();
let error = fs::rename(
fixture.mountpoint.join("open-source"),
fixture.mountpoint.join("dump.rdb"),
)
.unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::EXDEV));
drop(open);
rustix::fs::mknodat(
rustix::fs::CWD,
fixture.mountpoint.join("fifo-source"),
rustix::fs::FileType::Fifo,
rustix::fs::Mode::from_raw_mode(0o600),
0,
)
.unwrap();
let error = fs::rename(
fixture.mountpoint.join("fifo-source"),
fixture.mountpoint.join("dump.rdb"),
)
.unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::EXDEV));
session.umount_and_join().unwrap();
}
fn repository(root: &std::path::Path) { fn repository(root: &std::path::Path) {
fs::create_dir_all(root.join(".git/info")).unwrap(); fs::create_dir_all(root.join(".git/info")).unwrap();
fs::create_dir_all(root.join(".git/hooks")).unwrap(); fs::create_dir_all(root.join(".git/hooks")).unwrap();

View file

@ -19,7 +19,7 @@ const TRANSACTIONS: &str = ".lazaret-ignored-workspace-transactions";
const INTENT_SUFFIX: &str = ".json"; const INTENT_SUFFIX: &str = ".json";
static TRANSACTION_SEQUENCE: AtomicU64 = AtomicU64::new(0); static TRANSACTION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct ObjectStamp { pub(crate) struct ObjectStamp {
device: u64, device: u64,
inode: u64, inode: u64,
@ -44,6 +44,10 @@ impl ObjectStamp {
change_nanoseconds: stat.st_ctime_nsec, change_nanoseconds: stat.st_ctime_nsec,
} }
} }
fn same_object(&self, stat: &rustix::fs::Stat) -> bool {
self.device == stat.st_dev && self.inode == stat.st_ino && self.mode == stat.st_mode
}
} }
#[derive(Default)] #[derive(Default)]
@ -99,6 +103,12 @@ struct Intent {
destination: Vec<u8>, destination: Vec<u8>,
quarantine: Vec<u8>, quarantine: Vec<u8>,
staging: Vec<u8>, staging: Vec<u8>,
#[serde(default)]
backup: Vec<u8>,
#[serde(default)]
destination_existed: bool,
#[serde(default)]
destination_stamp: Option<ObjectStamp>,
manifest: Vec<ObjectRecord>, manifest: Vec<ObjectRecord>,
} }
@ -117,12 +127,15 @@ struct ObjectRecord {
change_seconds: i64, change_seconds: i64,
change_nanoseconds: u64, change_nanoseconds: u64,
digest: Option<[u8; 32]>, digest: Option<[u8; 32]>,
#[serde(default)]
link_target: Option<Vec<u8>>,
} }
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
enum ObjectKind { enum ObjectKind {
Directory, Directory,
Regular, Regular,
Symlink,
} }
pub(crate) fn reserved(path: &Path) -> bool { pub(crate) fn reserved(path: &Path) -> bool {
@ -147,8 +160,10 @@ pub(crate) fn migrate(
provenance: &Provenance, provenance: &Provenance,
source: &Path, source: &Path,
destination: &Path, destination: &Path,
replace_existing: bool,
) -> io::Result<()> { ) -> io::Result<()> {
if isolated.stat(destination).is_ok() { let destination_stat = stat_if_exists(isolated, destination)?;
if destination_stat.is_some() && !replace_existing {
return Err(io::Error::from_raw_os_error(libc::EEXIST)); return Err(io::Error::from_raw_os_error(libc::EEXIST));
} }
let manifest = match scan_tree(host, source) { let manifest = match scan_tree(host, source) {
@ -168,6 +183,9 @@ pub(crate) fn migrate(
); );
return Err(io::Error::from_raw_os_error(libc::EXDEV)); return Err(io::Error::from_raw_os_error(libc::EXDEV));
} }
if let Some(destination_stat) = destination_stat.as_ref() {
require_compatible_destination(isolated, destination, &manifest[0], destination_stat)?;
}
if let Some(record) = manifest.iter().find(|record| { if let Some(record) = manifest.iter().find(|record| {
let path = source.join(path_from_bytes(&record.path)); let path = source.join(path_from_bytes(&record.path));
!provenance.matches(&path, &record.stamp()) !provenance.matches(&path, &record.stamp())
@ -189,13 +207,17 @@ pub(crate) fn migrate(
let destination_parent = destination.parent().unwrap_or_else(|| Path::new("")); let destination_parent = destination.parent().unwrap_or_else(|| Path::new(""));
let quarantine = source_parent.join(format!(".lazaret-quarantine-{id}")); let quarantine = source_parent.join(format!(".lazaret-quarantine-{id}"));
let staging = destination_parent.join(format!(".lazaret-staging-{id}")); let staging = destination_parent.join(format!(".lazaret-staging-{id}"));
let backup = destination_parent.join(format!(".lazaret-backup-{id}"));
let intent = Intent { let intent = Intent {
schema: 1, schema: 3,
id, id,
source: path_bytes(source), source: path_bytes(source),
destination: path_bytes(destination), destination: path_bytes(destination),
quarantine: path_bytes(&quarantine), quarantine: path_bytes(&quarantine),
staging: path_bytes(&staging), staging: path_bytes(&staging),
backup: path_bytes(&backup),
destination_existed: destination_stat.is_some(),
destination_stamp: destination_stat.as_ref().map(ObjectStamp::from_stat),
manifest, manifest,
}; };
if let Err(error) = persist_intent(isolated, &intent) { if let Err(error) = persist_intent(isolated, &intent) {
@ -214,12 +236,20 @@ pub(crate) fn migrate(
isolated.sync_directory(destination_parent)?; isolated.sync_directory(destination_parent)?;
require_exact_tree(host, &quarantine, &intent.manifest)?; require_exact_tree(host, &quarantine, &intent.manifest)?;
require_equivalent_tree(isolated, &staging, &intent.manifest)?; require_equivalent_tree(isolated, &staging, &intent.manifest)?;
if intent.destination_existed {
isolated.rename(destination, &backup, RenameFlags::NOREPLACE)?;
isolated.sync_directory(destination_parent)?;
}
isolated.rename(&staging, destination, RenameFlags::NOREPLACE)?; isolated.rename(&staging, destination, RenameFlags::NOREPLACE)?;
isolated.sync_directory(destination_parent)?; isolated.sync_directory(destination_parent)?;
require_equivalent_tree(isolated, destination, &intent.manifest)?; require_equivalent_tree(isolated, destination, &intent.manifest)?;
require_exact_tree(host, &quarantine, &intent.manifest)?; require_exact_tree(host, &quarantine, &intent.manifest)?;
remove_tree(host, &quarantine)?; remove_tree(host, &quarantine)?;
host.sync_directory(source_parent)?; host.sync_directory(source_parent)?;
if stat_if_exists(isolated, &backup)?.is_some() {
remove_tree(isolated, &backup)?;
isolated.sync_directory(destination_parent)?;
}
clear_intent(isolated, &intent) clear_intent(isolated, &intent)
})(); })();
if let Err(error) = result { if let Err(error) = result {
@ -246,7 +276,11 @@ fn recover(host: &BackingRoot, isolated: &BackingRoot) -> io::Result<()> {
let bytes = isolated.read_file(&path, 16 * 1024 * 1024)?; let bytes = isolated.read_file(&path, 16 * 1024 * 1024)?;
let intent: Intent = serde_json::from_slice(&bytes) let intent: Intent = serde_json::from_slice(&bytes)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if intent.schema != 1 || intent_path(&intent) != path { if !matches!(intent.schema, 1..=3)
|| intent_path(&intent) != path
|| (intent.schema >= 2
&& intent.destination_existed != intent.destination_stamp.is_some())
{
return Err(io::Error::from_raw_os_error(libc::EIO)); return Err(io::Error::from_raw_os_error(libc::EIO));
} }
reconcile(host, isolated, &intent)?; reconcile(host, isolated, &intent)?;
@ -270,11 +304,21 @@ fn reconcile(
let destination = path_from_bytes(&intent.destination); let destination = path_from_bytes(&intent.destination);
let quarantine = path_from_bytes(&intent.quarantine); let quarantine = path_from_bytes(&intent.quarantine);
let staging = path_from_bytes(&intent.staging); let staging = path_from_bytes(&intent.staging);
let source_exists = host.stat(&source).is_ok(); let backup = intent_backup(intent, &destination);
let quarantine_exists = host.stat(&quarantine).is_ok(); let source_exists = stat_if_exists(host, &source)?.is_some();
let destination_exists = isolated.stat(&destination).is_ok(); let quarantine_exists = stat_if_exists(host, &quarantine)?.is_some();
let destination_stat = stat_if_exists(isolated, &destination)?;
let destination_exists = destination_stat.is_some();
let backup_stat = stat_if_exists(isolated, &backup)?;
let backup_exists = backup_stat.is_some();
if destination_exists { if destination_exists {
let destination_is_previous = intent.destination_stamp.as_ref().is_some_and(|stamp| {
destination_stat
.as_ref()
.is_some_and(|stat| stamp.same_object(stat))
});
if !destination_is_previous {
require_equivalent_tree(isolated, &destination, &intent.manifest)?; require_equivalent_tree(isolated, &destination, &intent.manifest)?;
if quarantine_exists { if quarantine_exists {
require_exact_tree(host, &quarantine, &intent.manifest)?; require_exact_tree(host, &quarantine, &intent.manifest)?;
@ -284,18 +328,41 @@ fn reconcile(
if source_exists { if source_exists {
return Err(io::Error::from_raw_os_error(libc::EIO)); return Err(io::Error::from_raw_os_error(libc::EIO));
} }
if isolated.stat(&staging).is_ok() { if stat_if_exists(isolated, &staging)?.is_some() {
remove_tree(isolated, &staging)?; remove_tree(isolated, &staging)?;
isolated.sync_directory(staging.parent().unwrap_or_else(|| Path::new("")))?;
} }
if backup_exists {
remove_tree(isolated, &backup)?;
}
isolated.sync_directory(staging.parent().unwrap_or_else(|| Path::new("")))?;
clear_intent(isolated, intent)?; clear_intent(isolated, intent)?;
return Ok(Reconciled::Isolated); return Ok(Reconciled::Isolated);
} }
if !intent.destination_existed || backup_exists {
return Err(io::Error::from_raw_os_error(libc::EIO));
}
}
if isolated.stat(&staging).is_ok() { if stat_if_exists(isolated, &staging)?.is_some() {
remove_tree(isolated, &staging)?; remove_tree(isolated, &staging)?;
isolated.sync_directory(staging.parent().unwrap_or_else(|| Path::new("")))?; isolated.sync_directory(staging.parent().unwrap_or_else(|| Path::new("")))?;
} }
if backup_exists {
if destination_exists {
return Err(io::Error::from_raw_os_error(libc::EIO));
}
if !intent.destination_stamp.as_ref().is_some_and(|stamp| {
backup_stat
.as_ref()
.is_some_and(|stat| stamp.same_object(stat))
}) {
return Err(io::Error::from_raw_os_error(libc::EIO));
}
isolated.rename(&backup, &destination, RenameFlags::NOREPLACE)?;
isolated.sync_directory(destination.parent().unwrap_or_else(|| Path::new("")))?;
} else if intent.destination_existed && !destination_exists {
return Err(io::Error::from_raw_os_error(libc::EIO));
}
match (source_exists, quarantine_exists) { match (source_exists, quarantine_exists) {
(true, false) => { (true, false) => {
require_exact_tree(host, &source, &intent.manifest)?; require_exact_tree(host, &source, &intent.manifest)?;
@ -311,6 +378,43 @@ fn reconcile(
Ok(Reconciled::Host) Ok(Reconciled::Host)
} }
fn stat_if_exists(root: &BackingRoot, path: &Path) -> io::Result<Option<rustix::fs::Stat>> {
match root.stat(path) {
Ok(stat) => Ok(Some(stat)),
Err(error) if error.raw_os_error() == Some(libc::ENOENT) => Ok(None),
Err(error) => Err(error),
}
}
fn intent_backup(intent: &Intent, destination: &Path) -> PathBuf {
if intent.backup.is_empty() {
destination
.parent()
.unwrap_or_else(|| Path::new(""))
.join(format!(".lazaret-backup-{}", intent.id))
} else {
path_from_bytes(&intent.backup)
}
}
fn require_compatible_destination(
isolated: &BackingRoot,
destination: &Path,
source: &ObjectRecord,
destination_stat: &rustix::fs::Stat,
) -> io::Result<()> {
let source_directory = source.kind == ObjectKind::Directory;
let destination_directory = destination_stat.st_mode & libc::S_IFMT == libc::S_IFDIR;
match (source_directory, destination_directory) {
(true, false) => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
(false, true) => Err(io::Error::from_raw_os_error(libc::EISDIR)),
(true, true) if !isolated.typed_entries(destination)?.is_empty() => {
Err(io::Error::from_raw_os_error(libc::ENOTEMPTY))
}
_ => Ok(()),
}
}
fn persist_intent(isolated: &BackingRoot, intent: &Intent) -> io::Result<()> { fn persist_intent(isolated: &BackingRoot, intent: &Intent) -> io::Result<()> {
let final_path = intent_path(intent); let final_path = intent_path(intent);
let temporary_path = Path::new(TRANSACTIONS).join(format!("{}.tmp", intent.id)); let temporary_path = Path::new(TRANSACTIONS).join(format!("{}.tmp", intent.id));
@ -331,7 +435,8 @@ fn intent_path(intent: &Intent) -> PathBuf {
fn scan_tree(root: &BackingRoot, tree: &Path) -> io::Result<Vec<ObjectRecord>> { fn scan_tree(root: &BackingRoot, tree: &Path) -> io::Result<Vec<ObjectRecord>> {
let mut records = Vec::new(); let mut records = Vec::new();
scan_entry(root, tree, Path::new(""), &mut records)?; let device = root.stat(tree)?.st_dev;
scan_entry(root, tree, Path::new(""), device, &mut records)?;
records.sort_by(|left, right| left.path.cmp(&right.path)); records.sort_by(|left, right| left.path.cmp(&right.path));
Ok(records) Ok(records)
} }
@ -340,6 +445,7 @@ fn scan_entry(
root: &BackingRoot, root: &BackingRoot,
tree: &Path, tree: &Path,
relative: &Path, relative: &Path,
device: u64,
records: &mut Vec<ObjectRecord>, records: &mut Vec<ObjectRecord>,
) -> io::Result<()> { ) -> io::Result<()> {
let path = if relative.as_os_str().is_empty() { let path = if relative.as_os_str().is_empty() {
@ -348,26 +454,35 @@ fn scan_entry(
tree.join(relative) tree.join(relative)
}; };
let stat = root.stat(&path)?; let stat = root.stat(&path)?;
if stat.st_dev != device {
return Err(io::Error::from_raw_os_error(libc::EXDEV));
}
let kind = match stat.st_mode & libc::S_IFMT { let kind = match stat.st_mode & libc::S_IFMT {
libc::S_IFDIR => ObjectKind::Directory, libc::S_IFDIR => ObjectKind::Directory,
libc::S_IFREG if stat.st_nlink == 1 => ObjectKind::Regular, libc::S_IFREG if stat.st_nlink == 1 => ObjectKind::Regular,
libc::S_IFLNK => ObjectKind::Symlink,
_ => return Err(io::Error::from_raw_os_error(libc::EXDEV)), _ => return Err(io::Error::from_raw_os_error(libc::EXDEV)),
}; };
let file = root.open_entry(&path, OFlags::RDONLY | OFlags::NONBLOCK, Mode::empty())?; let file = if kind == ObjectKind::Symlink {
let mut xattrs = vec![0; 64 * 1024]; if root.symlink_xattrs(&path)?.into_iter().any(|name| {
let xattr_length = flistxattr(file.as_fd(), &mut xattrs)?; let name = name.as_bytes();
xattrs.truncate(xattr_length); unsupported_xattr_name(name)
if xattrs.split(|byte| *byte == 0).any(|name| {
name.starts_with(b"user.")
|| matches!(
name,
b"system.posix_acl_access" | b"system.posix_acl_default"
)
}) { }) {
return Err(io::Error::from_raw_os_error(libc::EXDEV)); return Err(io::Error::from_raw_os_error(libc::EXDEV));
} }
let digest = if kind == ObjectKind::Regular { None
Some(digest(file)?) } else {
Some(root.open_entry(&path, OFlags::RDONLY | OFlags::NONBLOCK, Mode::empty())?)
};
if let Some(file) = file.as_ref() {
require_no_unsupported_xattrs(file)?;
}
let digest = match (kind, file) {
(ObjectKind::Regular, Some(file)) => Some(digest(file)?),
_ => None,
};
let link_target = if kind == ObjectKind::Symlink {
Some(root.readlink(&path)?)
} else { } else {
None None
}; };
@ -385,15 +500,34 @@ fn scan_entry(
change_seconds: stat.st_ctime, change_seconds: stat.st_ctime,
change_nanoseconds: stat.st_ctime_nsec, change_nanoseconds: stat.st_ctime_nsec,
digest, digest,
link_target,
}); });
if kind == ObjectKind::Directory { if kind == ObjectKind::Directory {
for (name, _) in root.typed_entries(&path)? { for (name, _) in root.typed_entries(&path)? {
scan_entry(root, tree, &relative.join(name), records)?; scan_entry(root, tree, &relative.join(name), device, records)?;
} }
} }
Ok(()) Ok(())
} }
fn require_no_unsupported_xattrs(file: &File) -> io::Result<()> {
let mut xattrs = vec![0; 64 * 1024];
let xattr_length = flistxattr(file.as_fd(), &mut xattrs)?;
xattrs.truncate(xattr_length);
if xattrs.split(|byte| *byte == 0).any(unsupported_xattr_name) {
return Err(io::Error::from_raw_os_error(libc::EXDEV));
}
Ok(())
}
fn unsupported_xattr_name(name: &[u8]) -> bool {
name.starts_with(b"user.")
|| matches!(
name,
b"system.posix_acl_access" | b"system.posix_acl_default"
)
}
fn digest(mut file: File) -> io::Result<[u8; 32]> { fn digest(mut file: File) -> io::Result<[u8; 32]> {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
let mut buffer = vec![0; 64 * 1024]; let mut buffer = vec![0; 64 * 1024];
@ -437,6 +571,16 @@ fn copy_tree(
set_metadata(&output, &stat)?; set_metadata(&output, &stat)?;
output.sync_all() output.sync_all()
} }
libc::S_IFLNK => {
let target = PathBuf::from(OsString::from_vec(source_root.readlink(source)?));
destination_root.symlink(&target, destination)?;
destination_root.set_symlink_owner(
destination,
Uid::from_raw(stat.st_uid),
Gid::from_raw(stat.st_gid),
)?;
destination_root.set_symlink_times(destination, &timestamps(&stat))
}
_ => Err(io::Error::from_raw_os_error(libc::EXDEV)), _ => Err(io::Error::from_raw_os_error(libc::EXDEV)),
} }
} }
@ -448,9 +592,12 @@ fn set_metadata(file: &File, stat: &rustix::fs::Stat) -> io::Result<()> {
Some(Uid::from_raw(stat.st_uid)), Some(Uid::from_raw(stat.st_uid)),
Some(Gid::from_raw(stat.st_gid)), Some(Gid::from_raw(stat.st_gid)),
)?; )?;
futimens( futimens(file.as_fd(), &timestamps(stat))?;
file.as_fd(), Ok(())
&Timestamps { }
fn timestamps(stat: &rustix::fs::Stat) -> Timestamps {
Timestamps {
last_access: Timespec { last_access: Timespec {
tv_sec: stat.st_atime, tv_sec: stat.st_atime,
tv_nsec: i64::try_from(stat.st_atime_nsec).unwrap_or(999_999_999), tv_nsec: i64::try_from(stat.st_atime_nsec).unwrap_or(999_999_999),
@ -459,9 +606,7 @@ fn set_metadata(file: &File, stat: &rustix::fs::Stat) -> io::Result<()> {
tv_sec: stat.st_mtime, tv_sec: stat.st_mtime,
tv_nsec: i64::try_from(stat.st_mtime_nsec).unwrap_or(999_999_999), tv_nsec: i64::try_from(stat.st_mtime_nsec).unwrap_or(999_999_999),
}, },
}, }
)?;
Ok(())
} }
fn remove_tree(root: &BackingRoot, path: &Path) -> io::Result<()> { fn remove_tree(root: &BackingRoot, path: &Path) -> io::Result<()> {
@ -540,6 +685,7 @@ impl ObjectRecord {
&& self.modification_seconds == expected.modification_seconds && self.modification_seconds == expected.modification_seconds
&& self.modification_nanoseconds == expected.modification_nanoseconds && self.modification_nanoseconds == expected.modification_nanoseconds
&& self.digest == expected.digest && self.digest == expected.digest
&& self.link_target == expected.link_target
} }
} }
@ -554,10 +700,11 @@ fn path_from_bytes(bytes: &[u8]) -> PathBuf {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io::Write; use std::io::Write;
use std::os::unix::ffi::OsStrExt;
use super::{ use super::{
BackingRoot, Intent, Mode, Provenance, initialize, migrate, path_bytes, path_from_bytes, BackingRoot, Intent, Mode, ObjectStamp, Provenance, copy_tree, initialize, migrate,
persist_intent, scan_tree, path_bytes, path_from_bytes, persist_intent, scan_tree,
}; };
#[test] #[test]
@ -597,6 +744,7 @@ mod tests {
&provenance, &provenance,
std::path::Path::new("target-temporary"), std::path::Path::new("target-temporary"),
std::path::Path::new("target"), std::path::Path::new("target"),
true,
) )
.unwrap(); .unwrap();
@ -653,6 +801,7 @@ mod tests {
&provenance, &provenance,
std::path::Path::new("target-temporary"), std::path::Path::new("target-temporary"),
std::path::Path::new("target"), std::path::Path::new("target"),
true,
) )
.unwrap_err(); .unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::EXDEV)); assert_eq!(error.raw_os_error(), Some(libc::EXDEV));
@ -687,6 +836,9 @@ mod tests {
destination: path_bytes(destination), destination: path_bytes(destination),
quarantine: path_bytes(quarantine), quarantine: path_bytes(quarantine),
staging: path_bytes(staging), staging: path_bytes(staging),
backup: Vec::new(),
destination_existed: false,
destination_stamp: None,
manifest: scan_tree(&host, source).unwrap(), manifest: scan_tree(&host, source).unwrap(),
}; };
persist_intent(&isolated, &intent).unwrap(); persist_intent(&isolated, &intent).unwrap();
@ -711,6 +863,197 @@ mod tests {
); );
} }
#[test]
fn qualified_regular_file_replaces_isolated_destination() {
let fixture = Fixture::new("regular-replacement");
let host = BackingRoot::open(&fixture.host).unwrap();
let isolated = BackingRoot::open(&fixture.isolated).unwrap();
initialize(&host, &isolated).unwrap();
let source = std::path::Path::new("temporary.rdb");
let destination = std::path::Path::new("dump.rdb");
host.write_file(source, b"new", Mode::from_raw_mode(0o640))
.unwrap();
isolated
.write_file(destination, b"old", Mode::from_raw_mode(0o600))
.unwrap();
let mut provenance = Provenance::default();
provenance.record(source.to_path_buf(), &host.stat(source).unwrap());
migrate(&host, &isolated, &provenance, source, destination, true).unwrap();
assert!(host.stat(source).is_err());
assert_eq!(isolated.read_file(destination, 16).unwrap(), b"new");
assert!(
isolated
.typed_entries(std::path::Path::new(
".lazaret-ignored-workspace-transactions"
))
.unwrap()
.is_empty()
);
assert!(
isolated
.typed_entries(std::path::Path::new(""))
.unwrap()
.into_iter()
.all(|(name, _)| !name.as_bytes().starts_with(b".lazaret-backup-"))
);
}
#[test]
fn qualified_symlink_migrates_without_host_export() {
let fixture = Fixture::new("special");
let host = BackingRoot::open(&fixture.host).unwrap();
let isolated = BackingRoot::open(&fixture.isolated).unwrap();
initialize(&host, &isolated).unwrap();
host.symlink(
std::path::Path::new("relative-target"),
std::path::Path::new("temporary-link"),
)
.unwrap();
let mut provenance = Provenance::default();
let link = std::path::Path::new("temporary-link");
provenance.record(link.to_path_buf(), &host.stat(link).unwrap());
migrate(
&host,
&isolated,
&provenance,
link,
std::path::Path::new("isolated-link"),
true,
)
.unwrap();
assert!(host.stat(link).is_err());
assert_eq!(
isolated
.readlink(std::path::Path::new("isolated-link"))
.unwrap(),
b"relative-target"
);
}
#[test]
fn noreplace_preserves_regular_source_and_destination() {
let fixture = Fixture::new("regular-noreplace");
let host = BackingRoot::open(&fixture.host).unwrap();
let isolated = BackingRoot::open(&fixture.isolated).unwrap();
initialize(&host, &isolated).unwrap();
let source = std::path::Path::new("temporary.rdb");
let destination = std::path::Path::new("dump.rdb");
host.write_file(source, b"new", Mode::from_raw_mode(0o600))
.unwrap();
isolated
.write_file(destination, b"old", Mode::from_raw_mode(0o600))
.unwrap();
let mut provenance = Provenance::default();
provenance.record(source.to_path_buf(), &host.stat(source).unwrap());
let error = migrate(&host, &isolated, &provenance, source, destination, false).unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::EEXIST));
assert_eq!(host.read_file(source, 16).unwrap(), b"new");
assert_eq!(isolated.read_file(destination, 16).unwrap(), b"old");
}
#[test]
fn startup_restores_replaced_destination_before_publication() {
let fixture = Fixture::new("replacement-rollback");
let host = BackingRoot::open(&fixture.host).unwrap();
let isolated = BackingRoot::open(&fixture.isolated).unwrap();
initialize(&host, &isolated).unwrap();
let source = std::path::Path::new("temporary.rdb");
let destination = std::path::Path::new("dump.rdb");
let quarantine = std::path::Path::new(".lazaret-quarantine-rollback");
let staging = std::path::Path::new(".lazaret-staging-rollback");
let backup = std::path::Path::new(".lazaret-backup-rollback");
host.write_file(source, b"new", Mode::from_raw_mode(0o600))
.unwrap();
isolated
.write_file(destination, b"old", Mode::from_raw_mode(0o600))
.unwrap();
let intent = Intent {
schema: 2,
id: "rollback".to_owned(),
source: path_bytes(source),
destination: path_bytes(destination),
quarantine: path_bytes(quarantine),
staging: path_bytes(staging),
backup: path_bytes(backup),
destination_existed: true,
destination_stamp: Some(ObjectStamp::from_stat(&isolated.stat(destination).unwrap())),
manifest: scan_tree(&host, source).unwrap(),
};
persist_intent(&isolated, &intent).unwrap();
host.rename(source, quarantine, rustix::fs::RenameFlags::NOREPLACE)
.unwrap();
host.sync_directory(std::path::Path::new("")).unwrap();
copy_tree(&host, quarantine, &isolated, staging).unwrap();
isolated.sync_directory(std::path::Path::new("")).unwrap();
isolated
.rename(destination, backup, rustix::fs::RenameFlags::NOREPLACE)
.unwrap();
isolated.sync_directory(std::path::Path::new("")).unwrap();
initialize(&host, &isolated).unwrap();
assert_eq!(host.read_file(source, 16).unwrap(), b"new");
assert_eq!(isolated.read_file(destination, 16).unwrap(), b"old");
assert!(isolated.stat(staging).is_err());
assert!(isolated.stat(backup).is_err());
}
#[test]
fn startup_completes_published_replacement() {
let fixture = Fixture::new("replacement-published");
let host = BackingRoot::open(&fixture.host).unwrap();
let isolated = BackingRoot::open(&fixture.isolated).unwrap();
initialize(&host, &isolated).unwrap();
let source = std::path::Path::new("temporary.rdb");
let destination = std::path::Path::new("dump.rdb");
let quarantine = std::path::Path::new(".lazaret-quarantine-published");
let staging = std::path::Path::new(".lazaret-staging-published");
let backup = std::path::Path::new(".lazaret-backup-published");
host.write_file(source, b"new", Mode::from_raw_mode(0o600))
.unwrap();
isolated
.write_file(destination, b"old", Mode::from_raw_mode(0o600))
.unwrap();
let intent = Intent {
schema: 2,
id: "published".to_owned(),
source: path_bytes(source),
destination: path_bytes(destination),
quarantine: path_bytes(quarantine),
staging: path_bytes(staging),
backup: path_bytes(backup),
destination_existed: true,
destination_stamp: Some(ObjectStamp::from_stat(&isolated.stat(destination).unwrap())),
manifest: scan_tree(&host, source).unwrap(),
};
persist_intent(&isolated, &intent).unwrap();
host.rename(source, quarantine, rustix::fs::RenameFlags::NOREPLACE)
.unwrap();
host.sync_directory(std::path::Path::new("")).unwrap();
copy_tree(&host, quarantine, &isolated, staging).unwrap();
isolated.sync_directory(std::path::Path::new("")).unwrap();
isolated
.rename(destination, backup, rustix::fs::RenameFlags::NOREPLACE)
.unwrap();
isolated.sync_directory(std::path::Path::new("")).unwrap();
isolated
.rename(staging, destination, rustix::fs::RenameFlags::NOREPLACE)
.unwrap();
isolated.sync_directory(std::path::Path::new("")).unwrap();
initialize(&host, &isolated).unwrap();
assert!(host.stat(source).is_err());
assert!(host.stat(quarantine).is_err());
assert!(isolated.stat(backup).is_err());
assert_eq!(isolated.read_file(destination, 16).unwrap(), b"new");
}
struct Fixture { struct Fixture {
root: std::path::PathBuf, root: std::path::PathBuf,
host: std::path::PathBuf, host: std::path::PathBuf,

View file

@ -41,13 +41,13 @@ use validation::{
}; };
pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net"; pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
pub const RUNSC_BUILD_ID: &str = "b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971"; pub const RUNSC_BUILD_ID: &str = "5b441d02d40792cfe228b294fb688a80cf6161fb6876d49ef03138618ae2562e";
pub const RUNSC_BUILD_ID_BYTES: [u8; 32] = [ pub const RUNSC_BUILD_ID_BYTES: [u8; 32] = [
185, 173, 111, 147, 168, 232, 144, 181, 147, 80, 97, 88, 43, 26, 18, 134, 130, 243, 253, 65, 91, 68, 29, 2, 212, 7, 146, 207, 226, 40, 178, 148, 251, 104, 138, 128, 207, 97, 97, 251, 104,
200, 5, 60, 84, 208, 228, 79, 244, 93, 128, 153, 113, 118, 212, 158, 240, 49, 56, 97, 138, 226, 86, 46,
]; ];
pub const RUNSC_EXECUTABLE_NAME: &str = pub const RUNSC_EXECUTABLE_NAME: &str =
"lazaret-runsc-b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971"; "lazaret-runsc-5b441d02d40792cfe228b294fb688a80cf6161fb6876d49ef03138618ae2562e";
pub const DOMAIN_OPEN_FILE_LIMIT: u64 = 524_288; pub const DOMAIN_OPEN_FILE_LIMIT: u64 = 524_288;
const OCI_VERSION: &str = "1.2.1"; const OCI_VERSION: &str = "1.2.1";

View file

@ -745,14 +745,27 @@ error behavior. An isolated-to-host rename, cross-backing hard link,
host-to-isolated class returns `EXDEV` before either backing is changed. host-to-isolated class returns `EXDEV` before either backing is changed.
`RENAME_WHITEOUT` returns `EOPNOTSUPP` without mutation. `RENAME_WHITEOUT` returns `EOPNOTSUPP` without mutation.
A plain or `RENAME_NOREPLACE` host-to-isolated directory rename is qualified A plain or `RENAME_NOREPLACE` host-to-isolated rename is qualified when the
only when the destination is absent and the complete source tree was created complete source was created through the current router instance, every retained
through the current router instance. Every retained source identity must still source identity still matches, and the source has defined copy semantics. The
match, and every descendant must be a regular file or directory with supported copyable set is single-link regular files, symbolic links, and trees containing
metadata. A writable description beneath the source must be read-only or only those objects and directories with supported metadata. FIFOs are not
quiesced by a completed flush; any later mutation clears quiescence. Pinned copyable because their live endpoint state is not observable through the FUSE
read-only file and directory descriptions remain attached to their original protocol. Hard-linked regular files, sockets, devices, and mounted trees are not
host objects. Host mutation detected before quarantine refuses the rename. copyable.
The destination may be absent or, for a plain rename, an existing object that
the backing filesystem permits native rename to replace. `RENAME_NOREPLACE`
returns `EEXIST` for an existing destination. A directory may replace only an
empty directory; directory and nondirectory mismatches retain their native
errors.
A writable description beneath the source must be read-only or quiesced by a
completed flush; any later mutation clears quiescence. The gVisor composition
uses per-description handles for the routed mount, so closing a guest file
description releases its router handle even while pathname dentries remain
cached. Pinned read-only file and directory descriptions remain attached to
their original host objects. Host mutation detected before quarantine refuses
the rename.
A qualified rename is a durable migration transaction rather than a A qualified rename is a durable migration transaction rather than a
cross-filesystem `rename(2)`. The router records and synchronizes intent before cross-filesystem `rename(2)`. The router records and synchronizes intent before
@ -760,18 +773,21 @@ renaming the source to a reserved same-backing host quarantine. It synchronizes
that parent, copies through descriptor-confined traversal into a reserved that parent, copies through descriptor-confined traversal into a reserved
isolated staging tree, verifies the staged tree and quarantined source, and isolated staging tree, verifies the staged tree and quarantined source, and
synchronizes the isolated tree before publishing it with a same-backing rename. synchronizes the isolated tree before publishing it with a same-backing rename.
Only a qualified isolated destination permits deletion of the exact For replacement, it first moves the exact old isolated destination to a
quarantined tree. The router synchronizes the host parent before clearing same-backing backup and synchronizes the destination parent. Only a qualified
intent. isolated destination permits deletion of the exact quarantined tree and backup.
The router synchronizes both affected parents before clearing intent.
The affected paths do not admit new observations while the transaction is The affected paths do not admit new observations while the transaction is
unresolved. Startup reconciles every retained intent before publishing router unresolved. Startup reconciles every retained intent before publishing router
readiness. Recovery exposes either the complete host source or the qualified readiness. Recovery exposes either the complete host source or the qualified
isolated destination; partial staging and changed quarantine remain isolated destination; partial staging and changed quarantine remain
unpublished and fail closed. The transaction never copies isolated bytes into unpublished and fail closed. Before publication, recovery restores an exact old
the host backing. Its success refines the sandbox namespace to one complete destination from its backup; after publication, it retains only the qualified
endpoint but does not promise cross-filesystem inode identity or atomic new destination. The transaction never copies isolated bytes into the host
observation by host processes that bypass FUSE. backing. Its success refines the sandbox namespace to one complete endpoint but
does not promise cross-filesystem inode identity or atomic observation by host
processes that bypass FUSE.
All other cross-backing renames retain `EXDEV` behavior. Applications may react All other cross-backing renames retain `EXDEV` behavior. Applications may react
to `EXDEV` with their own visible copy and unlink operations. to `EXDEV` with their own visible copy and unlink operations.

View file

@ -0,0 +1,28 @@
diff --git a/runsc/boot/vfs.go b/runsc/boot/vfs.go
index b34854d4c..b250b78c8 100644
--- a/runsc/boot/vfs.go
+++ b/runsc/boot/vfs.go
@@ -330,6 +330,9 @@ func goferMountData(fd int, fa config.FileAccessType, conf *config.Config, suppre
if fa == config.FileAccessShared {
opts = append(opts, "cache=remote_revalidating")
}
+ if suppressDirectFS {
+ opts = append(opts, "disable_file_handle_sharing")
+ }
if conf.DirectFS && !suppressDirectFS {
opts = append(opts, "directfs")
}
diff --git a/runsc/boot/vfs_test.go b/runsc/boot/vfs_test.go
index 17ec62bdd..31a103d28 100644
--- a/runsc/boot/vfs_test.go
+++ b/runsc/boot/vfs_test.go
@@ -108,6 +108,9 @@ func TestGoferMountDataDirectFS(t *testing.T) {
if gotEnabled != tc.wantEnabled {
t.Errorf("directfs option present = %t, want %t (opts=%v)", gotEnabled, tc.wantEnabled, opts)
}
+ if got := slices.Contains(opts, "disable_file_handle_sharing"); got != tc.suppressDirectFS {
+ t.Errorf("disable_file_handle_sharing option present = %t, want %t (opts=%v)", got, tc.suppressDirectFS, opts)
+ }
})
}
}

View file

@ -5,6 +5,7 @@ import importlib.machinery
import importlib.util import importlib.util
import json import json
from pathlib import Path from pathlib import Path
import re
import tempfile import tempfile
import unittest import unittest
from unittest import mock from unittest import mock
@ -406,7 +407,7 @@ class VerificationTests(unittest.TestCase):
) )
def test_runsc_materializer_and_install_identity_are_exact(self) -> None: def test_runsc_materializer_and_install_identity_are_exact(self) -> None:
build_id = "b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971" build_id = "5b441d02d40792cfe228b294fb688a80cf6161fb6876d49ef03138618ae2562e"
materializer = (ROOT / "tools/materialize-runsc").read_text() materializer = (ROOT / "tools/materialize-runsc").read_text()
installer = (ROOT / "tools/install-to-host").read_text() installer = (ROOT / "tools/install-to-host").read_text()
runtime = (ROOT / "crates/lazaret-runtime-supervisor/src/sandbox.rs").read_text() runtime = (ROOT / "crates/lazaret-runtime-supervisor/src/sandbox.rs").read_text()
@ -416,6 +417,14 @@ class VerificationTests(unittest.TestCase):
self.assertIn(f"build_id={build_id}", materializer) self.assertIn(f"build_id={build_id}", materializer)
self.assertIn(f"runsc_build_id={build_id}", installer) self.assertIn(f"runsc_build_id={build_id}", installer)
self.assertIn(f'"lazaret-runsc-{build_id}"', runtime) self.assertIn(f'"lazaret-runsc-{build_id}"', runtime)
encoded = re.search(
r"RUNSC_BUILD_ID_BYTES: \[u8; 32\] = \[(.*?)\];", runtime, re.DOTALL
)
self.assertIsNotNone(encoded)
self.assertEqual(
bytes(int(value.strip()) for value in encoded.group(1).split(",") if value.strip()),
bytes.fromhex(build_id),
)
self.assertIn("cmp --silent", installer) self.assertIn("cmp --silent", installer)
self.assertTrue( self.assertTrue(
(ROOT / "patches/gvisor-9f8f6c917-directory-handles.patch").is_file() (ROOT / "patches/gvisor-9f8f6c917-directory-handles.patch").is_file()
@ -423,6 +432,12 @@ class VerificationTests(unittest.TestCase):
self.assertTrue( self.assertTrue(
(ROOT / "patches/gvisor-9f8f6c917-private-ownership.patch").is_file() (ROOT / "patches/gvisor-9f8f6c917-private-ownership.patch").is_file()
) )
self.assertTrue(
(
ROOT
/ "patches/gvisor-9f8f6c917-per-description-file-handles.patch"
).is_file()
)
self.assertTrue( self.assertTrue(
(ROOT / "patches/gvisor-9f8f6c917-MODULE.bazel.lock").is_file() (ROOT / "patches/gvisor-9f8f6c917-MODULE.bazel.lock").is_file()
) )

View file

@ -59,7 +59,7 @@ if readelf -l "$sandbox_init" | grep -q 'Requesting program interpreter' ||
fi fi
install -m 755 "$sandbox_init" "$install_dir/lazaret-sandbox-init" install -m 755 "$sandbox_init" "$install_dir/lazaret-sandbox-init"
runsc_build_id=b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971 runsc_build_id=5b441d02d40792cfe228b294fb688a80cf6161fb6876d49ef03138618ae2562e
runsc_name="lazaret-runsc-$runsc_build_id" runsc_name="lazaret-runsc-$runsc_build_id"
runsc_source="$root/.lazaret/bin/$runsc_name" runsc_source="$root/.lazaret/bin/$runsc_name"
runsc_destination="$install_dir/$runsc_name" runsc_destination="$install_dir/$runsc_name"

View file

@ -7,16 +7,18 @@ archive_checksum=9d96426115f1bf54ef09ba41964c73dc3c809743519c8442016b8dd3fce0889
patch_files=( patch_files=(
"$root/patches/gvisor-9f8f6c917-directory-handles.patch" "$root/patches/gvisor-9f8f6c917-directory-handles.patch"
"$root/patches/gvisor-9f8f6c917-private-ownership.patch" "$root/patches/gvisor-9f8f6c917-private-ownership.patch"
"$root/patches/gvisor-9f8f6c917-per-description-file-handles.patch"
) )
patch_checksums=( patch_checksums=(
541420eece9ec7efbef156bdb726744ead6b4d991d41c5560f4fdfe83fb2357a 541420eece9ec7efbef156bdb726744ead6b4d991d41c5560f4fdfe83fb2357a
79a8c98502d3bb7c254ffb0885dbd26afffac227d04a0091ff91383df0842561 79a8c98502d3bb7c254ffb0885dbd26afffac227d04a0091ff91383df0842561
2403efb81df3185145d18ebdf8d511252fbc2fb414bae4746d7fe471ec4ea9d9
) )
patch_digest=5622a44af2d9e7561f5372844dc21cf6fb93b4fcd3fe70da7f545d403a84c612 patch_digest=cb9126927fc45e4b0c4ec82e65b4244472b1427d850477432b62183b40e7443f
bazel_version=8.3.1 bazel_version=8.3.1
module_lock="$root/patches/gvisor-9f8f6c917-MODULE.bazel.lock" module_lock="$root/patches/gvisor-9f8f6c917-MODULE.bazel.lock"
module_checksum=8402c7beb4baf2c666f4b78e400ea3f15514b56117598c2cb5411d6a35208d34 module_checksum=8402c7beb4baf2c666f4b78e400ea3f15514b56117598c2cb5411d6a35208d34
build_id=b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971 build_id=5b441d02d40792cfe228b294fb688a80cf6161fb6876d49ef03138618ae2562e
archive="$root/.lazaret/cache/gvisor-$revision.tar.gz" archive="$root/.lazaret/cache/gvisor-$revision.tar.gz"
source="$root/.lazaret/vendor/gvisor-$build_id" source="$root/.lazaret/vendor/gvisor-$build_id"
binary="$root/.lazaret/bin/lazaret-runsc-$build_id" binary="$root/.lazaret/bin/lazaret-runsc-$build_id"