Confine package ownership changes to private mounts

This commit is contained in:
ms2k 2026-07-27 03:31:58 +09:00
commit 8334fd379f
3 changed files with 402 additions and 25 deletions

View file

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::fs::File;
@ -40,17 +41,19 @@ use validation::{
};
pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
pub const RUNSC_BUILD_ID: &str = "904ab3c9bd4c99a27181076f2c9ea2fecc041db5a728c999540d6664174d2cd4";
pub const RUNSC_BUILD_ID: &str = "b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971";
pub const RUNSC_BUILD_ID_BYTES: [u8; 32] = [
144, 74, 179, 201, 189, 76, 153, 162, 113, 129, 7, 111, 44, 158, 162, 254, 204, 4, 29, 181,
167, 40, 201, 153, 84, 13, 102, 100, 23, 77, 44, 212,
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,
];
pub const RUNSC_EXECUTABLE_NAME: &str =
"lazaret-runsc-904ab3c9bd4c99a27181076f2c9ea2fecc041db5a728c999540d6664174d2cd4";
"lazaret-runsc-b9ad6f93a8e890b5935061582b1a128682f3fd41c8053c54d0e44ff45d809971";
pub const DOMAIN_OPEN_FILE_LIMIT: u64 = 524_288;
const OCI_VERSION: &str = "1.2.1";
const MAX_TERMINFO_BYTES: usize = 1_048_576;
const GVISOR_MOUNT_ANNOTATION_PREFIX: &str = "dev.gvisor.spec.mount.";
const OWNER_CHANGE_MOUNTS_ANNOTATION: &str = "dev.lazaret.runsc.owner-change-mounts";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SandboxBuildErrorKind {
@ -691,8 +694,10 @@ fn build_sandbox_bundle_from_source(
.readonly(false)
.build()
.map_err(oci_error)?;
let annotations = build_mount_annotations(request.configuration, &mounts)?;
let specification = SpecBuilder::default()
.version(OCI_VERSION)
.annotations(annotations)
.root(root)
.mounts(mounts)
.process(process)
@ -723,6 +728,54 @@ fn build_sandbox_bundle_from_source(
})
}
fn build_mount_annotations(
configuration: &ResolvedDomainConfiguration,
mounts: &[Mount],
) -> Result<HashMap<String, String>, SandboxBuildError> {
let owner_change_mounts = std::iter::once("/".to_owned())
.chain(
configuration
.fingerprint_record()
.persistent_dirs
.iter()
.cloned(),
)
.collect::<Vec<_>>();
let mut annotations = HashMap::from([(
OWNER_CHANGE_MOUNTS_ANNOTATION.to_owned(),
serde_json::to_string(&owner_change_mounts).map_err(|error| {
SandboxBuildError::new(SandboxBuildErrorKind::JsonEncoding, error.to_string())
})?,
)]);
for (index, mount) in mounts.iter().enumerate() {
let destination = mount.destination().to_str().ok_or_else(|| {
SandboxBuildError::new(
SandboxBuildErrorKind::InvalidPath,
"mount destination is not UTF-8",
)
})?;
if mount.typ().as_deref() != Some("bind")
|| owner_change_mounts
.iter()
.any(|allowed| allowed == destination)
{
continue;
}
let Some(source) = mount.source().as_ref().and_then(|path| path.to_str()) else {
return Err(SandboxBuildError::new(
SandboxBuildErrorKind::InvalidPath,
"bind mount source is not UTF-8",
));
};
let prefix = format!("{GVISOR_MOUNT_ANNOTATION_PREFIX}lazaret{index}.");
annotations.insert(format!("{prefix}source"), source.to_owned());
annotations.insert(format!("{prefix}type"), "bind".to_owned());
annotations.insert(format!("{prefix}share"), "shared".to_owned());
annotations.insert(format!("{prefix}directfs"), "off".to_owned());
}
Ok(annotations)
}
fn canonical_specification_json(specification: &Spec) -> Result<Vec<u8>, SandboxBuildError> {
let mut value = serde_json::to_value(specification).map_err(|error| {
SandboxBuildError::new(SandboxBuildErrorKind::JsonEncoding, error.to_string())

View file

@ -338,8 +338,15 @@ pub(super) fn build_process(
root_process: RootProcess,
_terminal: Option<&StagedHostTerminal>,
) -> Result<oci_spec::runtime::Process, SandboxBuildError> {
static IDENTITY_TRANSITION_CAPABILITIES: LazyLock<Capabilities> =
LazyLock::new(|| Capabilities::from([Capability::Setgid, Capability::Setuid]));
static IDENTITY_TRANSITION_CAPABILITIES: LazyLock<Capabilities> = LazyLock::new(|| {
Capabilities::from([
Capability::Chown,
Capability::DacOverride,
Capability::Fowner,
Capability::Setgid,
Capability::Setuid,
])
});
let empty = Capabilities::new();
let identity = IDENTITY_TRANSITION_CAPABILITIES.clone();
let capabilities = LinuxCapabilitiesBuilder::default()

View file

@ -303,14 +303,42 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
assert_eq!(
specification["process"]["capabilities"],
serde_json::json!({
"bounding": ["CAP_SETGID", "CAP_SETUID"],
"effective": ["CAP_SETGID", "CAP_SETUID"],
"bounding": ["CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_SETGID", "CAP_SETUID"],
"effective": ["CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_SETGID", "CAP_SETUID"],
"inheritable": [],
"permitted": ["CAP_SETGID", "CAP_SETUID"],
"permitted": ["CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_SETGID", "CAP_SETUID"],
"ambient": []
})
);
let annotations = specification["annotations"].as_object().unwrap();
assert_eq!(
serde_json::from_str::<Vec<String>>(
annotations["dev.lazaret.runsc.owner-change-mounts"]
.as_str()
.unwrap()
)
.unwrap(),
["/", "/root", "/usr", "/var", "/etc", "/opt"]
);
let owner_change_mounts = BTreeSet::from(["/", "/root", "/usr", "/var", "/etc", "/opt"]);
for (index, mount) in specification["mounts"]
.as_array()
.unwrap()
.iter()
.enumerate()
{
let destination = mount["destination"].as_str().unwrap();
if mount["type"] != "bind" || owner_change_mounts.contains(destination) {
continue;
}
let prefix = format!("dev.gvisor.spec.mount.lazaret{index}.");
assert_eq!(annotations[&format!("{prefix}source")], mount["source"]);
assert_eq!(annotations[&format!("{prefix}type")], "bind");
assert_eq!(annotations[&format!("{prefix}share")], "shared");
assert_eq!(annotations[&format!("{prefix}directfs")], "off");
}
let namespaces = specification["linux"]["namespaces"].as_array().unwrap();
assert_eq!(
namespaces