Refuse prepared-lower child mount crossings

This commit is contained in:
ms2k 2026-07-16 21:09:48 +09:00
commit b35582a9e6
3 changed files with 83 additions and 2 deletions

View file

@ -859,7 +859,10 @@ fn timestamps(seconds: u64) -> Timestamps {
} }
fn secure_resolve() -> ResolveFlags { fn secure_resolve() -> ResolveFlags {
ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS ResolveFlags::BENEATH
| ResolveFlags::NO_SYMLINKS
| ResolveFlags::NO_MAGICLINKS
| ResolveFlags::NO_XDEV
} }
fn rewound(source: &File) -> Result<File> { fn rewound(source: &File) -> Result<File> {
@ -867,3 +870,13 @@ fn rewound(source: &File) -> Result<File> {
file.seek(SeekFrom::Start(0))?; file.seek(SeekFrom::Start(0))?;
Ok(file) Ok(file)
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retained_layer_resolution_refuses_mount_crossing() {
assert!(secure_resolve().contains(ResolveFlags::NO_XDEV));
}
}

View file

@ -693,7 +693,10 @@ fn hex(bytes: &[u8]) -> String {
} }
fn secure_resolve() -> ResolveFlags { fn secure_resolve() -> ResolveFlags {
ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS ResolveFlags::BENEATH
| ResolveFlags::NO_SYMLINKS
| ResolveFlags::NO_MAGICLINKS
| ResolveFlags::NO_XDEV
} }
#[cfg(test)] #[cfg(test)]
@ -711,4 +714,9 @@ mod fixed_directory_tests {
std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700)).unwrap(); std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(require_owned_directory(&root).is_ok()); assert!(require_owned_directory(&root).is_ok());
} }
#[test]
fn retained_store_resolution_refuses_mount_crossing() {
assert!(secure_resolve().contains(ResolveFlags::NO_XDEV));
}
} }

View file

@ -22,6 +22,8 @@ use sha2::{Digest, Sha256};
use tar::{Builder, EntryType, Header}; use tar::{Builder, EntryType, Header};
use tempfile::TempDir; use tempfile::TempDir;
const MOUNT_CHILD_ENV: &str = "LAZARET_PREPARED_LOWER_MOUNT_CHILD";
const OCI_TAR: &str = "application/vnd.oci.image.layer.v1.tar"; const OCI_TAR: &str = "application/vnd.oci.image.layer.v1.tar";
const OCI_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip"; const OCI_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
const OCI_ZSTD: &str = "application/vnd.oci.image.layer.v1.tar+zstd"; const OCI_ZSTD: &str = "application/vnd.oci.image.layer.v1.tar+zstd";
@ -83,6 +85,64 @@ fn preexisting_fixed_children_require_exact_private_mode() {
} }
} }
#[test]
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
fn retained_fixed_child_bind_mount_is_refused_when_mount_setup_is_available() {
if std::env::var_os(MOUNT_CHILD_ENV).is_some() {
let directory = TempDir::new().unwrap();
let root = directory.path().join("storage");
let source = directory.path().join("source");
std::fs::create_dir(&root).unwrap();
std::fs::create_dir(&source).unwrap();
let catalog = Catalog::new(root.join("catalog.redb"), root.join("storage-catalog.lock"));
catalog.initialize().unwrap();
PreparedLowerStore::initialize(&root, catalog.clone()).unwrap();
let target = root.join("artifacts");
if let Err(error) = rustix::mount::mount_bind(&source, &target) {
eprintln!(
"skipping physical mount-crossing assertion: bind mount unavailable: {error}"
);
return;
}
let result = PreparedLowerStore::initialize(&root, catalog);
rustix::mount::unmount(&target, rustix::mount::UnmountFlags::DETACH).unwrap();
assert!(result.is_err());
return;
}
let output = std::process::Command::new("unshare")
.args([
"--user",
"--map-root-user",
"--mount",
"--propagation",
"private",
"--",
])
.arg(std::env::current_exe().unwrap())
.args([
"--exact",
"retained_fixed_child_bind_mount_is_refused_when_mount_setup_is_available",
])
.env(MOUNT_CHILD_ENV, "1")
.output();
let output = match output {
Ok(output) => output,
Err(error) => {
eprintln!("skipping physical mount-crossing assertion: unshare unavailable: {error}");
return;
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("Operation not permitted") {
eprintln!("skipping physical mount-crossing assertion: {stderr}");
return;
}
panic!("mount-crossing child failed: {stderr}");
}
}
#[derive(Clone)] #[derive(Clone)]
struct Entry<'a> { struct Entry<'a> {
path: &'a str, path: &'a str,