Refuse prepared-lower child mount crossings
This commit is contained in:
parent
4cf7a5e595
commit
b35582a9e6
3 changed files with 83 additions and 2 deletions
|
|
@ -859,7 +859,10 @@ fn timestamps(seconds: u64) -> Timestamps {
|
|||
}
|
||||
|
||||
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> {
|
||||
|
|
@ -867,3 +870,13 @@ fn rewound(source: &File) -> Result<File> {
|
|||
file.seek(SeekFrom::Start(0))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn retained_layer_resolution_refuses_mount_crossing() {
|
||||
assert!(secure_resolve().contains(ResolveFlags::NO_XDEV));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -693,7 +693,10 @@ fn hex(bytes: &[u8]) -> String {
|
|||
}
|
||||
|
||||
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)]
|
||||
|
|
@ -711,4 +714,9 @@ mod fixed_directory_tests {
|
|||
std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
assert!(require_owned_directory(&root).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_store_resolution_refuses_mount_crossing() {
|
||||
assert!(secure_resolve().contains(ResolveFlags::NO_XDEV));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ use sha2::{Digest, Sha256};
|
|||
use tar::{Builder, EntryType, Header};
|
||||
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_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
|
||||
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)]
|
||||
struct Entry<'a> {
|
||||
path: &'a str,
|
||||
|
|
|
|||
Loading…
Reference in a new issue