Confine package ownership changes to private mounts
This commit is contained in:
parent
2c420012bd
commit
8334fd379f
8 changed files with 402 additions and 25 deletions
|
|
@ -275,8 +275,23 @@ impl IgnoredWorkspace {
|
|||
let stat = match backing {
|
||||
Classification::Host => host,
|
||||
Classification::Isolated => isolated,
|
||||
}
|
||||
.ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
|
||||
};
|
||||
let (backing, stat) = if let Some(stat) = stat {
|
||||
(backing, stat)
|
||||
} else if backing == Classification::Isolated
|
||||
&& host.as_ref().is_some_and(|stat| {
|
||||
stat.st_mode & libc::S_IFMT == libc::S_IFDIR
|
||||
&& self
|
||||
.provenance
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.matches(path, &crate::migration::ObjectStamp::from_stat(stat))
|
||||
})
|
||||
{
|
||||
(Classification::Host, host.expect("checked host directory"))
|
||||
} else {
|
||||
return Err(io::Error::from_raw_os_error(libc::ENOENT));
|
||||
};
|
||||
Ok(ResolvedEntry {
|
||||
path: path.to_path_buf(),
|
||||
backing,
|
||||
|
|
|
|||
|
|
@ -81,6 +81,63 @@ fn disabled_isolation_routes_ignored_paths_to_the_host_backing() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newly_ignored_created_directory_remains_a_safe_traversal_shell() {
|
||||
let fixture = Fixture::new();
|
||||
repository(&fixture.workspace);
|
||||
let filesystem = IgnoredWorkspace::open(&fixture.workspace, &fixture.isolated, None).unwrap();
|
||||
for path in ["environment", "environment/bin"] {
|
||||
fs::create_dir(fixture.workspace.join(path)).unwrap();
|
||||
filesystem
|
||||
.record_created(Classification::Host, std::path::Path::new(path))
|
||||
.unwrap();
|
||||
}
|
||||
fs::write(fixture.workspace.join("environment/.gitignore"), b"*\n").unwrap();
|
||||
filesystem
|
||||
.record_created(
|
||||
Classification::Host,
|
||||
std::path::Path::new("environment/.gitignore"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let path = std::path::Path::new("environment/bin");
|
||||
let snapshot = filesystem.classifier.snapshot_for(path);
|
||||
let entry = filesystem.resolve(&snapshot, path).unwrap();
|
||||
assert_eq!(entry.backing, Classification::Host);
|
||||
assert_eq!(
|
||||
filesystem.classify(&snapshot, &path.join("python3"), false),
|
||||
Classification::Isolated
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignored_host_directory_requires_current_creation_provenance() {
|
||||
let fixture = Fixture::new();
|
||||
repository(&fixture.workspace);
|
||||
fs::write(fixture.workspace.join(".gitignore"), b"hidden/\nchanged/\n").unwrap();
|
||||
fs::create_dir(fixture.workspace.join("hidden")).unwrap();
|
||||
fs::create_dir(fixture.workspace.join("changed")).unwrap();
|
||||
let filesystem = IgnoredWorkspace::open(&fixture.workspace, &fixture.isolated, None).unwrap();
|
||||
|
||||
let hidden = std::path::Path::new("hidden");
|
||||
let snapshot = filesystem.classifier.snapshot_for(hidden);
|
||||
let Err(error) = filesystem.resolve(&snapshot, hidden) else {
|
||||
panic!("unproven host directory became visible");
|
||||
};
|
||||
assert_eq!(error.raw_os_error(), Some(libc::ENOENT));
|
||||
|
||||
filesystem
|
||||
.record_created(Classification::Host, std::path::Path::new("changed"))
|
||||
.unwrap();
|
||||
fs::write(fixture.workspace.join("changed/external"), b"outside").unwrap();
|
||||
let changed = std::path::Path::new("changed");
|
||||
let snapshot = filesystem.classifier.snapshot_for(changed);
|
||||
let Err(error) = filesystem.resolve(&snapshot, changed) else {
|
||||
panic!("changed host directory retained creation provenance");
|
||||
};
|
||||
assert_eq!(error.raw_os_error(), Some(libc::ENOENT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a usable host FUSE device"]
|
||||
fn physical_router_preserves_lock_domains_across_transition() {
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ impl Provenance {
|
|||
self.objects.extend(moved);
|
||||
}
|
||||
|
||||
fn matches(&self, path: &Path, stamp: &ObjectStamp) -> bool {
|
||||
pub(crate) fn matches(&self, path: &Path, stamp: &ObjectStamp) -> bool {
|
||||
self.objects.get(path) == Some(stamp)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@ pub(super) fn create_regular_file(parent: impl AsFd, name: &str) -> Result<(), S
|
|||
parent.as_fd(),
|
||||
name,
|
||||
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::from_raw_mode(0o600),
|
||||
Mode::from_raw_mode(0o644),
|
||||
confined_resolve_flags(),
|
||||
) {
|
||||
Ok(descriptor) => File::from(descriptor).sync_all()?,
|
||||
|
|
@ -440,7 +440,7 @@ pub(super) fn atomic_replace_at(
|
|||
parent.as_fd(),
|
||||
Path::new(&candidate),
|
||||
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::from_raw_mode(0o600),
|
||||
Mode::from_raw_mode(0o644),
|
||||
confined_resolve_flags(),
|
||||
) {
|
||||
Ok(descriptor) => {
|
||||
|
|
@ -514,6 +514,7 @@ pub(super) fn overlay_options(lower: &Path, upper: &Path, work: &Path) -> OsStri
|
|||
append_fuse_option_path(&mut bytes, upper);
|
||||
bytes.extend_from_slice(b",workdir=");
|
||||
append_fuse_option_path(&mut bytes, work);
|
||||
bytes.extend_from_slice(b",xattr_permissions=2,noacl");
|
||||
OsString::from_vec(bytes)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ fn run() -> io::Result<()> {
|
|||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
enter_runtime_user_namespace()?;
|
||||
let tree = TestTree::new().map_err(|error| io::Error::other(format!("test tree: {error}")))?;
|
||||
let configuration = configuration(&tree)
|
||||
.map_err(|error| io::Error::other(format!("configuration: {error}")))?;
|
||||
|
|
@ -251,6 +252,25 @@ fn run() -> io::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn enter_runtime_user_namespace() -> io::Result<()> {
|
||||
let uid = rustix::process::getuid().as_raw();
|
||||
let gid = rustix::process::getgid().as_raw();
|
||||
rustix::thread::unshare(rustix::thread::UnshareFlags::NEWUSER)
|
||||
.map_err(|error| io::Error::other(format!("fixture user namespace: {error}")))?;
|
||||
fs::write("/proc/self/setgroups", "deny")?;
|
||||
fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))?;
|
||||
fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))?;
|
||||
rustix::thread::unshare(rustix::thread::UnshareFlags::NEWNS)
|
||||
.map_err(|error| io::Error::other(format!("fixture mount namespace: {error}")))?;
|
||||
rustix::mount::mount_change(
|
||||
"/",
|
||||
rustix::mount::MountPropagationFlags::PRIVATE | rustix::mount::MountPropagationFlags::REC,
|
||||
)
|
||||
.map_err(|error| io::Error::other(format!("fixture private propagation: {error}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn run_private_recovery_holder(arguments: &[OsString]) -> io::Result<()> {
|
||||
if arguments.len() != 2 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue