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 {
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue