Generalize safe host-to-isolated rename migration
This commit is contained in:
parent
aaee63a6e3
commit
35ab7f17c0
13 changed files with 589 additions and 81 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1787,6 +1787,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ tar = "=0.4.46"
|
|||
time = "=0.3.53"
|
||||
tokio = { version = "=1.52.3", features = ["fs", "io-util", "process", "rt"] }
|
||||
toml = { version = "=1.1.3", default-features = false, features = ["parse", "serde", "std"] }
|
||||
xattr = "=1.6.1"
|
||||
x509-parser = "=0.18.1"
|
||||
zstd = "=0.13.3"
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ rustix.workspace = true
|
|||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
xattr.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@ use std::fs::File;
|
|||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::os::fd::AsFd;
|
||||
use std::os::fd::{AsFd, AsRawFd};
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
use std::path::{Component, Path};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use rustix::fs::{
|
||||
AtFlags, Mode, OFlags, RawDir, RenameFlags, ResolveFlags, Stat, Timestamps, linkat, mkdirat,
|
||||
openat, openat2, readlinkat, renameat_with, statat, symlinkat, unlinkat, utimensat,
|
||||
AtFlags, Mode, OFlags, RawDir, RenameFlags, ResolveFlags, Stat, Timestamps, chownat, linkat,
|
||||
mkdirat, openat, openat2, readlinkat, renameat_with, statat, symlinkat, unlinkat, utimensat,
|
||||
};
|
||||
use rustix::process::{Gid, Uid};
|
||||
|
||||
const RESOLVE: ResolveFlags = ResolveFlags::BENEATH
|
||||
.union(ResolveFlags::NO_MAGICLINKS)
|
||||
|
|
@ -196,6 +197,14 @@ impl BackingRoot {
|
|||
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(
|
||||
&self,
|
||||
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>> {
|
||||
Ok(self
|
||||
.typed_entries(relative)?
|
||||
|
|
|
|||
|
|
@ -351,7 +351,6 @@ impl Filesystem for IgnoredWorkspace {
|
|||
if source_entry.backing != destination_backing {
|
||||
let supported_flags =
|
||||
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
|
||||
|| destination_backing != Classification::Isolated
|
||||
{
|
||||
|
|
@ -364,10 +363,13 @@ impl Filesystem for IgnoredWorkspace {
|
|||
);
|
||||
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!(
|
||||
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));
|
||||
}
|
||||
|
|
@ -388,6 +390,7 @@ impl Filesystem for IgnoredWorkspace {
|
|||
&provenance,
|
||||
&source,
|
||||
&destination,
|
||||
flags.is_empty(),
|
||||
)?;
|
||||
provenance.remove_tree(&source);
|
||||
return Ok(());
|
||||
|
|
@ -492,6 +495,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
_lock_owner: Option<LockOwner>,
|
||||
reply: ReplyWrite,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = self.with_file(handle, |open| {
|
||||
open.migration_quiesced.store(false, Ordering::Release);
|
||||
let offset = if flags.0 & libc::O_APPEND != 0 {
|
||||
|
|
@ -1005,6 +1012,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
reply.error(Errno::EINVAL);
|
||||
return;
|
||||
}
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let files = self
|
||||
.files
|
||||
.lock()
|
||||
|
|
|
|||
|
|
@ -321,6 +321,75 @@ fn physical_router_migrates_cargo_target_only_toward_isolated_state() {
|
|||
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) {
|
||||
fs::create_dir_all(root.join(".git/info")).unwrap();
|
||||
fs::create_dir_all(root.join(".git/hooks")).unwrap();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const TRANSACTIONS: &str = ".lazaret-ignored-workspace-transactions";
|
|||
const INTENT_SUFFIX: &str = ".json";
|
||||
static TRANSACTION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub(crate) struct ObjectStamp {
|
||||
device: u64,
|
||||
inode: u64,
|
||||
|
|
@ -44,6 +44,10 @@ impl ObjectStamp {
|
|||
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)]
|
||||
|
|
@ -99,6 +103,12 @@ struct Intent {
|
|||
destination: Vec<u8>,
|
||||
quarantine: Vec<u8>,
|
||||
staging: Vec<u8>,
|
||||
#[serde(default)]
|
||||
backup: Vec<u8>,
|
||||
#[serde(default)]
|
||||
destination_existed: bool,
|
||||
#[serde(default)]
|
||||
destination_stamp: Option<ObjectStamp>,
|
||||
manifest: Vec<ObjectRecord>,
|
||||
}
|
||||
|
||||
|
|
@ -117,12 +127,15 @@ struct ObjectRecord {
|
|||
change_seconds: i64,
|
||||
change_nanoseconds: u64,
|
||||
digest: Option<[u8; 32]>,
|
||||
#[serde(default)]
|
||||
link_target: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
enum ObjectKind {
|
||||
Directory,
|
||||
Regular,
|
||||
Symlink,
|
||||
}
|
||||
|
||||
pub(crate) fn reserved(path: &Path) -> bool {
|
||||
|
|
@ -147,8 +160,10 @@ pub(crate) fn migrate(
|
|||
provenance: &Provenance,
|
||||
source: &Path,
|
||||
destination: &Path,
|
||||
replace_existing: bool,
|
||||
) -> 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));
|
||||
}
|
||||
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));
|
||||
}
|
||||
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| {
|
||||
let path = source.join(path_from_bytes(&record.path));
|
||||
!provenance.matches(&path, &record.stamp())
|
||||
|
|
@ -189,13 +207,17 @@ pub(crate) fn migrate(
|
|||
let destination_parent = destination.parent().unwrap_or_else(|| Path::new(""));
|
||||
let quarantine = source_parent.join(format!(".lazaret-quarantine-{id}"));
|
||||
let staging = destination_parent.join(format!(".lazaret-staging-{id}"));
|
||||
let backup = destination_parent.join(format!(".lazaret-backup-{id}"));
|
||||
let intent = Intent {
|
||||
schema: 1,
|
||||
schema: 3,
|
||||
id,
|
||||
source: path_bytes(source),
|
||||
destination: path_bytes(destination),
|
||||
quarantine: path_bytes(&quarantine),
|
||||
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,
|
||||
};
|
||||
if let Err(error) = persist_intent(isolated, &intent) {
|
||||
|
|
@ -214,12 +236,20 @@ pub(crate) fn migrate(
|
|||
isolated.sync_directory(destination_parent)?;
|
||||
require_exact_tree(host, &quarantine, &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.sync_directory(destination_parent)?;
|
||||
require_equivalent_tree(isolated, destination, &intent.manifest)?;
|
||||
require_exact_tree(host, &quarantine, &intent.manifest)?;
|
||||
remove_tree(host, &quarantine)?;
|
||||
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)
|
||||
})();
|
||||
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 intent: Intent = serde_json::from_slice(&bytes)
|
||||
.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));
|
||||
}
|
||||
reconcile(host, isolated, &intent)?;
|
||||
|
|
@ -270,32 +304,65 @@ fn reconcile(
|
|||
let destination = path_from_bytes(&intent.destination);
|
||||
let quarantine = path_from_bytes(&intent.quarantine);
|
||||
let staging = path_from_bytes(&intent.staging);
|
||||
let source_exists = host.stat(&source).is_ok();
|
||||
let quarantine_exists = host.stat(&quarantine).is_ok();
|
||||
let destination_exists = isolated.stat(&destination).is_ok();
|
||||
let backup = intent_backup(intent, &destination);
|
||||
let source_exists = stat_if_exists(host, &source)?.is_some();
|
||||
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 {
|
||||
require_equivalent_tree(isolated, &destination, &intent.manifest)?;
|
||||
if quarantine_exists {
|
||||
require_exact_tree(host, &quarantine, &intent.manifest)?;
|
||||
remove_tree(host, &quarantine)?;
|
||||
host.sync_directory(quarantine.parent().unwrap_or_else(|| Path::new("")))?;
|
||||
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)?;
|
||||
if quarantine_exists {
|
||||
require_exact_tree(host, &quarantine, &intent.manifest)?;
|
||||
remove_tree(host, &quarantine)?;
|
||||
host.sync_directory(quarantine.parent().unwrap_or_else(|| Path::new("")))?;
|
||||
}
|
||||
if source_exists {
|
||||
return Err(io::Error::from_raw_os_error(libc::EIO));
|
||||
}
|
||||
if stat_if_exists(isolated, &staging)?.is_some() {
|
||||
remove_tree(isolated, &staging)?;
|
||||
}
|
||||
if backup_exists {
|
||||
remove_tree(isolated, &backup)?;
|
||||
}
|
||||
isolated.sync_directory(staging.parent().unwrap_or_else(|| Path::new("")))?;
|
||||
clear_intent(isolated, intent)?;
|
||||
return Ok(Reconciled::Isolated);
|
||||
}
|
||||
if source_exists {
|
||||
if !intent.destination_existed || backup_exists {
|
||||
return Err(io::Error::from_raw_os_error(libc::EIO));
|
||||
}
|
||||
if isolated.stat(&staging).is_ok() {
|
||||
remove_tree(isolated, &staging)?;
|
||||
isolated.sync_directory(staging.parent().unwrap_or_else(|| Path::new("")))?;
|
||||
}
|
||||
clear_intent(isolated, intent)?;
|
||||
return Ok(Reconciled::Isolated);
|
||||
}
|
||||
|
||||
if isolated.stat(&staging).is_ok() {
|
||||
if stat_if_exists(isolated, &staging)?.is_some() {
|
||||
remove_tree(isolated, &staging)?;
|
||||
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) {
|
||||
(true, false) => {
|
||||
require_exact_tree(host, &source, &intent.manifest)?;
|
||||
|
|
@ -311,6 +378,43 @@ fn reconcile(
|
|||
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<()> {
|
||||
let final_path = intent_path(intent);
|
||||
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>> {
|
||||
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));
|
||||
Ok(records)
|
||||
}
|
||||
|
|
@ -340,6 +445,7 @@ fn scan_entry(
|
|||
root: &BackingRoot,
|
||||
tree: &Path,
|
||||
relative: &Path,
|
||||
device: u64,
|
||||
records: &mut Vec<ObjectRecord>,
|
||||
) -> io::Result<()> {
|
||||
let path = if relative.as_os_str().is_empty() {
|
||||
|
|
@ -348,26 +454,35 @@ fn scan_entry(
|
|||
tree.join(relative)
|
||||
};
|
||||
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 {
|
||||
libc::S_IFDIR => ObjectKind::Directory,
|
||||
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)),
|
||||
};
|
||||
let file = root.open_entry(&path, OFlags::RDONLY | OFlags::NONBLOCK, Mode::empty())?;
|
||||
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(|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));
|
||||
let file = if kind == ObjectKind::Symlink {
|
||||
if root.symlink_xattrs(&path)?.into_iter().any(|name| {
|
||||
let name = name.as_bytes();
|
||||
unsupported_xattr_name(name)
|
||||
}) {
|
||||
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
||||
}
|
||||
None
|
||||
} 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 = if kind == ObjectKind::Regular {
|
||||
Some(digest(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 {
|
||||
None
|
||||
};
|
||||
|
|
@ -385,15 +500,34 @@ fn scan_entry(
|
|||
change_seconds: stat.st_ctime,
|
||||
change_nanoseconds: stat.st_ctime_nsec,
|
||||
digest,
|
||||
link_target,
|
||||
});
|
||||
if kind == ObjectKind::Directory {
|
||||
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(())
|
||||
}
|
||||
|
||||
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]> {
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = vec![0; 64 * 1024];
|
||||
|
|
@ -437,6 +571,16 @@ fn copy_tree(
|
|||
set_metadata(&output, &stat)?;
|
||||
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, ×tamps(&stat))
|
||||
}
|
||||
_ => Err(io::Error::from_raw_os_error(libc::EXDEV)),
|
||||
}
|
||||
}
|
||||
|
|
@ -448,22 +592,23 @@ fn set_metadata(file: &File, stat: &rustix::fs::Stat) -> io::Result<()> {
|
|||
Some(Uid::from_raw(stat.st_uid)),
|
||||
Some(Gid::from_raw(stat.st_gid)),
|
||||
)?;
|
||||
futimens(
|
||||
file.as_fd(),
|
||||
&Timestamps {
|
||||
last_access: Timespec {
|
||||
tv_sec: stat.st_atime,
|
||||
tv_nsec: i64::try_from(stat.st_atime_nsec).unwrap_or(999_999_999),
|
||||
},
|
||||
last_modification: Timespec {
|
||||
tv_sec: stat.st_mtime,
|
||||
tv_nsec: i64::try_from(stat.st_mtime_nsec).unwrap_or(999_999_999),
|
||||
},
|
||||
},
|
||||
)?;
|
||||
futimens(file.as_fd(), ×tamps(stat))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn timestamps(stat: &rustix::fs::Stat) -> Timestamps {
|
||||
Timestamps {
|
||||
last_access: Timespec {
|
||||
tv_sec: stat.st_atime,
|
||||
tv_nsec: i64::try_from(stat.st_atime_nsec).unwrap_or(999_999_999),
|
||||
},
|
||||
last_modification: Timespec {
|
||||
tv_sec: stat.st_mtime,
|
||||
tv_nsec: i64::try_from(stat.st_mtime_nsec).unwrap_or(999_999_999),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_tree(root: &BackingRoot, path: &Path) -> io::Result<()> {
|
||||
let stat = root.stat(path)?;
|
||||
if stat.st_mode & libc::S_IFMT == libc::S_IFDIR {
|
||||
|
|
@ -540,6 +685,7 @@ impl ObjectRecord {
|
|||
&& self.modification_seconds == expected.modification_seconds
|
||||
&& self.modification_nanoseconds == expected.modification_nanoseconds
|
||||
&& self.digest == expected.digest
|
||||
&& self.link_target == expected.link_target
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -554,10 +700,11 @@ fn path_from_bytes(bytes: &[u8]) -> PathBuf {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use super::{
|
||||
BackingRoot, Intent, Mode, Provenance, initialize, migrate, path_bytes, path_from_bytes,
|
||||
persist_intent, scan_tree,
|
||||
BackingRoot, Intent, Mode, ObjectStamp, Provenance, copy_tree, initialize, migrate,
|
||||
path_bytes, path_from_bytes, persist_intent, scan_tree,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -597,6 +744,7 @@ mod tests {
|
|||
&provenance,
|
||||
std::path::Path::new("target-temporary"),
|
||||
std::path::Path::new("target"),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -653,6 +801,7 @@ mod tests {
|
|||
&provenance,
|
||||
std::path::Path::new("target-temporary"),
|
||||
std::path::Path::new("target"),
|
||||
true,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.raw_os_error(), Some(libc::EXDEV));
|
||||
|
|
@ -687,6 +836,9 @@ mod tests {
|
|||
destination: path_bytes(destination),
|
||||
quarantine: path_bytes(quarantine),
|
||||
staging: path_bytes(staging),
|
||||
backup: Vec::new(),
|
||||
destination_existed: false,
|
||||
destination_stamp: None,
|
||||
manifest: scan_tree(&host, source).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 {
|
||||
root: std::path::PathBuf,
|
||||
host: std::path::PathBuf,
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@ use validation::{
|
|||
};
|
||||
|
||||
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] = [
|
||||
185, 173, 111, 147, 168, 232, 144, 181, 147, 80, 97, 88, 43, 26, 18, 134, 130, 243, 253, 65,
|
||||
200, 5, 60, 84, 208, 228, 79, 244, 93, 128, 153, 113,
|
||||
91, 68, 29, 2, 212, 7, 146, 207, 226, 40, 178, 148, 251, 104, 138, 128, 207, 97, 97, 251, 104,
|
||||
118, 212, 158, 240, 49, 56, 97, 138, 226, 86, 46,
|
||||
];
|
||||
pub const RUNSC_EXECUTABLE_NAME: &str =
|
||||
"lazaret-runsc-b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971";
|
||||
"lazaret-runsc-5b441d02d40792cfe228b294fb688a80cf6161fb6876d49ef03138618ae2562e";
|
||||
pub const DOMAIN_OPEN_FILE_LIMIT: u64 = 524_288;
|
||||
|
||||
const OCI_VERSION: &str = "1.2.1";
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
`RENAME_WHITEOUT` returns `EOPNOTSUPP` without mutation.
|
||||
|
||||
A plain or `RENAME_NOREPLACE` host-to-isolated directory rename is qualified
|
||||
only when the destination is absent and the complete source tree was created
|
||||
through the current router instance. Every retained source identity must still
|
||||
match, and every descendant must be a regular file or directory with supported
|
||||
metadata. A writable description beneath the source must be read-only or
|
||||
quiesced by a completed flush; any later mutation clears quiescence. Pinned
|
||||
read-only file and directory descriptions remain attached to their original
|
||||
host objects. Host mutation detected before quarantine refuses the rename.
|
||||
A plain or `RENAME_NOREPLACE` host-to-isolated rename is qualified when the
|
||||
complete source was created through the current router instance, every retained
|
||||
source identity still matches, and the source has defined copy semantics. The
|
||||
copyable set is single-link regular files, symbolic links, and trees containing
|
||||
only those objects and directories with supported metadata. FIFOs are not
|
||||
copyable because their live endpoint state is not observable through the FUSE
|
||||
protocol. Hard-linked regular files, sockets, devices, and mounted trees are not
|
||||
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
|
||||
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
|
||||
isolated staging tree, verifies the staged tree and quarantined source, and
|
||||
synchronizes the isolated tree before publishing it with a same-backing rename.
|
||||
Only a qualified isolated destination permits deletion of the exact
|
||||
quarantined tree. The router synchronizes the host parent before clearing
|
||||
intent.
|
||||
For replacement, it first moves the exact old isolated destination to a
|
||||
same-backing backup and synchronizes the destination parent. Only a qualified
|
||||
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
|
||||
unresolved. Startup reconciles every retained intent before publishing router
|
||||
readiness. Recovery exposes either the complete host source or the qualified
|
||||
isolated destination; partial staging and changed quarantine remain
|
||||
unpublished and fail closed. The transaction never copies isolated bytes into
|
||||
the host 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.
|
||||
unpublished and fail closed. Before publication, recovery restores an exact old
|
||||
destination from its backup; after publication, it retains only the qualified
|
||||
new destination. The transaction never copies isolated bytes into the host
|
||||
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
|
||||
to `EXDEV` with their own visible copy and unlink operations.
|
||||
|
|
|
|||
28
patches/gvisor-9f8f6c917-per-description-file-handles.patch
Normal file
28
patches/gvisor-9f8f6c917-per-description-file-handles.patch
Normal 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)
|
||||
+ }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import importlib.machinery
|
|||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
|
@ -406,7 +407,7 @@ class VerificationTests(unittest.TestCase):
|
|||
)
|
||||
|
||||
def test_runsc_materializer_and_install_identity_are_exact(self) -> None:
|
||||
build_id = "b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971"
|
||||
build_id = "5b441d02d40792cfe228b294fb688a80cf6161fb6876d49ef03138618ae2562e"
|
||||
materializer = (ROOT / "tools/materialize-runsc").read_text()
|
||||
installer = (ROOT / "tools/install-to-host").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"runsc_build_id={build_id}", installer)
|
||||
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.assertTrue(
|
||||
(ROOT / "patches/gvisor-9f8f6c917-directory-handles.patch").is_file()
|
||||
|
|
@ -423,6 +432,12 @@ class VerificationTests(unittest.TestCase):
|
|||
self.assertTrue(
|
||||
(ROOT / "patches/gvisor-9f8f6c917-private-ownership.patch").is_file()
|
||||
)
|
||||
self.assertTrue(
|
||||
(
|
||||
ROOT
|
||||
/ "patches/gvisor-9f8f6c917-per-description-file-handles.patch"
|
||||
).is_file()
|
||||
)
|
||||
self.assertTrue(
|
||||
(ROOT / "patches/gvisor-9f8f6c917-MODULE.bazel.lock").is_file()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ if readelf -l "$sandbox_init" | grep -q 'Requesting program interpreter' ||
|
|||
fi
|
||||
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_source="$root/.lazaret/bin/$runsc_name"
|
||||
runsc_destination="$install_dir/$runsc_name"
|
||||
|
|
|
|||
|
|
@ -7,16 +7,18 @@ archive_checksum=9d96426115f1bf54ef09ba41964c73dc3c809743519c8442016b8dd3fce0889
|
|||
patch_files=(
|
||||
"$root/patches/gvisor-9f8f6c917-directory-handles.patch"
|
||||
"$root/patches/gvisor-9f8f6c917-private-ownership.patch"
|
||||
"$root/patches/gvisor-9f8f6c917-per-description-file-handles.patch"
|
||||
)
|
||||
patch_checksums=(
|
||||
541420eece9ec7efbef156bdb726744ead6b4d991d41c5560f4fdfe83fb2357a
|
||||
79a8c98502d3bb7c254ffb0885dbd26afffac227d04a0091ff91383df0842561
|
||||
2403efb81df3185145d18ebdf8d511252fbc2fb414bae4746d7fe471ec4ea9d9
|
||||
)
|
||||
patch_digest=5622a44af2d9e7561f5372844dc21cf6fb93b4fcd3fe70da7f545d403a84c612
|
||||
patch_digest=cb9126927fc45e4b0c4ec82e65b4244472b1427d850477432b62183b40e7443f
|
||||
bazel_version=8.3.1
|
||||
module_lock="$root/patches/gvisor-9f8f6c917-MODULE.bazel.lock"
|
||||
module_checksum=8402c7beb4baf2c666f4b78e400ea3f15514b56117598c2cb5411d6a35208d34
|
||||
build_id=b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971
|
||||
build_id=5b441d02d40792cfe228b294fb688a80cf6161fb6876d49ef03138618ae2562e
|
||||
archive="$root/.lazaret/cache/gvisor-$revision.tar.gz"
|
||||
source="$root/.lazaret/vendor/gvisor-$build_id"
|
||||
binary="$root/.lazaret/bin/lazaret-runsc-$build_id"
|
||||
|
|
|
|||
Loading…
Reference in a new issue