#![cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub(crate) const MAX_NAMESPACE_SOCKETS: usize = crate::egress_proxy::MAX_EGRESS_TCP_PORTS + 1;
pub(crate) const MAX_SESSION_NETNS_FDS: usize = MAX_NAMESPACE_SOCKETS + 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CreationStep {
Unshare = 1,
IdentityMaps = 2,
Loopback = 3,
LocalRoute = 4,
TcpListener = 5,
Resolver = 6,
NamespaceHandles = 7,
HandOff = 8,
}
impl CreationStep {
const ALL: [CreationStep; 8] = [
Self::Unshare,
Self::IdentityMaps,
Self::Loopback,
Self::LocalRoute,
Self::TcpListener,
Self::Resolver,
Self::NamespaceHandles,
Self::HandOff,
];
pub(crate) fn from_code(code: u32) -> Option<Self> {
Self::ALL.into_iter().find(|step| *step as u32 == code)
}
#[must_use]
pub fn describe(self) -> &'static str {
match self {
Self::Unshare => "unshare(CLONE_NEWUSER | CLONE_NEWNET)",
Self::IdentityMaps => "writing uid_map/gid_map",
Self::Loopback => "bringing lo up",
Self::LocalRoute => "installing the local default route",
Self::TcpListener => "binding a TCP listener inside the namespace",
Self::Resolver => "binding UDP 53 inside the namespace",
Self::NamespaceHandles => "opening /proc/self/ns/user and /proc/self/ns/net",
Self::HandOff => "handing the namespace to the runtime",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NamespaceAttemptError {
Step { step: CreationStep, errno: i32 },
Creator(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EgressNamespaceBlocker {
CapabilityGrantMissing,
KernelSupportMissing,
CreatorUnavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EgressNamespaceFailure {
pub blocker: EgressNamespaceBlocker,
pub failed_call: String,
pub errno: Option<i32>,
pub userns_grant: crate::sealed::UsernsGrant,
pub attachment: crate::sealed::ProfileAttachment,
}
impl EgressNamespaceFailure {
#[must_use]
pub fn summary(&self) -> String {
match self.errno {
Some(errno) => format!("{} failed with {}", self.failed_call, errno_label(errno)),
None => self.failed_call.clone(),
}
}
#[must_use]
pub fn reason(&self) -> String {
let what = match self.errno {
Some(errno) => format!(
"{} failed with {} ({})",
self.failed_call,
errno_label(errno),
errno_words(errno)
),
None => self.failed_call.clone(),
};
format!("{what}. {} {}", self.cause(), self.readings())
}
fn step(&self) -> Option<CreationStep> {
self.errno?;
CreationStep::ALL
.into_iter()
.find(|step| step.describe() == self.failed_call)
}
fn cause(&self) -> String {
match self.blocker {
EgressNamespaceBlocker::KernelSupportMissing => kernel_cause(self.errno).to_string(),
EgressNamespaceBlocker::CreatorUnavailable => {
"Likely cause: the namespace creator could not run or stopped before reporting — \
a process or descriptor limit, or memory, ran out, or a policy killed it (its \
account then reads `killed by signal`) — so whether this host can create \
the namespace is unknown, and the capsule is refused rather than run without \
it. Re-run once the limit is relieved, or allow the creator's syscalls."
.to_string()
}
EgressNamespaceBlocker::CapabilityGrantMissing => {
let cause = grant_cause(self.userns_grant, &self.attachment);
match self.step().and_then(step_context) {
Some(context) => format!("{context} {cause}"),
None => cause,
}
}
}
}
fn readings(&self) -> String {
host_readings(self.userns_grant, &self.attachment)
}
}
pub(crate) fn host_readings(
userns_grant: crate::sealed::UsernsGrant,
attachment: &crate::sealed::ProfileAttachment,
) -> String {
use crate::sealed::{ProfileAttachment, UsernsGrant};
let restriction = match userns_grant {
UsernsGrant::ApparmorAbsent => "AppArmor not enabled",
UsernsGrant::RestrictionDisabledHostWide => {
"kernel.apparmor_restrict_unprivileged_userns=0"
}
UsernsGrant::ProfileConfining | UsernsGrant::Withheld => {
"kernel.apparmor_restrict_unprivileged_userns=1"
}
};
let profile = match attachment {
ProfileAttachment::Sealed { profile } | ProfileAttachment::Other { profile } => {
profile.as_str()
}
ProfileAttachment::Unconfined => "unconfined",
ProfileAttachment::NotReported => "not reported",
};
format!("Host readings: {restriction}; AppArmor profile confining mur: {profile}.")
}
fn step_context(step: CreationStep) -> Option<&'static str> {
match step {
CreationStep::Unshare => None,
CreationStep::IdentityMaps => Some(
"The namespace was created and could not be owned: writing its identity map was \
refused.",
),
CreationStep::Loopback | CreationStep::LocalRoute => Some(
"The namespace was created and owned, and configuring it was refused — the mark of a \
confinement that withholds CAP_NET_ADMIN inside the namespace.",
),
CreationStep::TcpListener
| CreationStep::Resolver
| CreationStep::NamespaceHandles
| CreationStep::HandOff => {
Some("The namespace was created and configured, and handing it to the runtime failed.")
}
}
}
fn grant_cause(
userns_grant: crate::sealed::UsernsGrant,
attachment: &crate::sealed::ProfileAttachment,
) -> String {
use crate::sealed::{
ProfileAttachment, UsernsGrant, SEALED_APPARMOR_PROFILE_NAME, SEALED_APPARMOR_PROFILE_PATH,
};
const CONTAINER_REMEDY: &str = "Inside a container, the container runtime's seccomp \
profile is the usual blocker: it refuses unshare(CLONE_NEWUSER) and setns whatever \
capabilities the container holds. Run the container with a seccomp profile that permits \
them (Docker: `--security-opt seccomp=<file>`; Kubernetes: `seccompProfile: \
Localhost`), or create the network namespace outside the container and run mur inside \
it.";
match (userns_grant, attachment) {
(UsernsGrant::ApparmorAbsent, _) => format!(
"Likely cause: not AppArmor, which is not enabled on this host — a container \
runtime's seccomp profile, or another security module's policy. \
{CONTAINER_REMEDY} On a bare host, check the policy of the security module in use."
),
(_, ProfileAttachment::Other { profile }) => format!(
"Likely cause: the AppArmor profile `{profile}` confines mur. \
kernel.apparmor_restrict_unprivileged_userns does not govern a process another \
profile confines: that profile, and a container runtime's seccomp profile, decide \
whether it may create a user namespace. Run the container with an AppArmor profile \
and a seccomp profile that permit unshare(CLONE_NEWUSER) and setns (Docker: \
`--security-opt apparmor=<profile> --security-opt seccomp=<file>`; Kubernetes: \
`appArmorProfile` and `seccompProfile: Localhost`), or create the network namespace \
outside the container and run mur inside it."
),
(UsernsGrant::ProfileConfining, _) | (_, ProfileAttachment::Sealed { .. }) => format!(
"Likely cause: the {SEALED_APPARMOR_PROFILE_NAME} AppArmor profile confines mur and \
the creation still failed, so the profile the kernel has loaded may be stale. Reload \
it with `sudo apparmor_parser -r {SEALED_APPARMOR_PROFILE_PATH}` (for a checkout \
build, re-run `scripts/install-dev-apparmor.sh`), and check `mur doctor`'s \
comparison of the installed profile with the one this build ships."
),
(UsernsGrant::RestrictionDisabledHostWide, _) => format!(
"Likely cause: not AppArmor, whose unprivileged-userns restriction is off on this host \
— a container runtime's seccomp profile, or another security module's policy. \
{CONTAINER_REMEDY}"
),
(UsernsGrant::Withheld, _) => format!(
"Likely cause: AppArmor's unprivileged-userns restriction \
(kernel.apparmor_restrict_unprivileged_userns=1) applies to this binary, which no \
{SEALED_APPARMOR_PROFILE_NAME} profile confines. Install and load the profile shipped \
with mur, `sudo install -m 644 packaging/apparmor/{SEALED_APPARMOR_PROFILE_NAME} \
{SEALED_APPARMOR_PROFILE_PATH} && sudo apparmor_parser -r \
{SEALED_APPARMOR_PROFILE_PATH}` (or re-run the mur installer as root), then re-run. \
Building out of a checkout, where the binary sits at ./target/{{debug,release}}/mur \
and no shipped profile attaches to it: run `scripts/install-dev-apparmor.sh`, which \
generates and loads the same grant for those two paths. LAST RESORT, only where a \
profile genuinely cannot be loaded: `sudo sysctl -w \
kernel.apparmor_restrict_unprivileged_userns=0` — this removes unprivileged-userns \
hardening from every program on the machine, not just from mur, and is not the \
configuration murmur ships."
),
}
}
fn kernel_cause(errno: Option<i32>) -> &'static str {
match errno {
Some(libc::ENOSPC) => {
"Likely cause: the kernel's limit on user namespaces — user.max_user_namespaces is 0, \
or every namespace it allows is in use. Raise it where it is set, on the host or in \
the user namespace mur runs in (`sudo sysctl -w user.max_user_namespaces=10000`), \
then re-run."
}
Some(libc::EUSERS) => {
"Likely cause: the kernel's nesting limit for user namespaces — mur already runs \
inside as many nested user namespaces as the kernel allows. Run it fewer levels \
deep."
}
_ => {
"Likely cause: this kernel does not provide user namespaces (built with \
CONFIG_USER_NS=n). Run mur on a kernel built with CONFIG_USER_NS=y."
}
}
}
pub(crate) fn errno_name(errno: i32) -> Option<&'static str> {
Some(match errno {
libc::EPERM => "EPERM",
libc::ENOENT => "ENOENT",
libc::EACCES => "EACCES",
libc::EINVAL => "EINVAL",
libc::ENOSPC => "ENOSPC",
libc::ENOSYS => "ENOSYS",
libc::EUSERS => "EUSERS",
libc::ENOMEM => "ENOMEM",
libc::EAGAIN => "EAGAIN",
libc::EADDRINUSE => "EADDRINUSE",
libc::EMFILE => "EMFILE",
libc::ENFILE => "ENFILE",
libc::EROFS => "EROFS",
libc::ENOTDIR => "ENOTDIR",
libc::EBUSY => "EBUSY",
_ => return None,
})
}
pub(crate) fn errno_label(errno: i32) -> String {
errno_name(errno).map_or_else(|| format!("errno {errno}"), str::to_string)
}
pub(crate) fn errno_words(errno: i32) -> String {
let rendered = std::io::Error::from_raw_os_error(errno).to_string();
rendered
.strip_suffix(&format!(" (os error {errno})"))
.map_or_else(|| rendered.clone(), str::to_string)
}
pub(crate) fn egress_namespace_failure(
is_linux: bool,
attempt: Result<(), NamespaceAttemptError>,
userns_grant: crate::sealed::UsernsGrant,
attachment: crate::sealed::ProfileAttachment,
) -> Option<EgressNamespaceFailure> {
if !is_linux {
return None;
}
attempt
.err()
.map(|error| failure_of(error, userns_grant, attachment))
}
fn failure_of(
error: NamespaceAttemptError,
userns_grant: crate::sealed::UsernsGrant,
attachment: crate::sealed::ProfileAttachment,
) -> EgressNamespaceFailure {
let (blocker, failed_call, errno) = match error {
NamespaceAttemptError::Step { step, errno } => {
let blocker = match (step, errno) {
(
CreationStep::Unshare,
libc::ENOSPC | libc::EINVAL | libc::ENOSYS | libc::EUSERS,
) => EgressNamespaceBlocker::KernelSupportMissing,
_ => EgressNamespaceBlocker::CapabilityGrantMissing,
};
(blocker, step.describe().to_string(), Some(errno))
}
NamespaceAttemptError::Creator(detail) => {
(EgressNamespaceBlocker::CreatorUnavailable, detail, None)
}
};
EgressNamespaceFailure {
blocker,
failed_call,
errno,
userns_grant,
attachment,
}
}
#[must_use]
pub fn detect_egress_namespace_failure() -> Option<EgressNamespaceFailure> {
crate::sandbox::HostProbe::probe().egress_namespace_failure()
}
pub fn skip_without_egress_namespace(test_name: &str) -> bool {
match detect_egress_namespace_failure() {
Some(failure) => {
crate::runtime_err!("[SKIP-HOST] {test_name}: {}", failure.reason());
true
}
None => false,
}
}
pub fn check_egress_namespace(
can_spawn_subprocess: bool,
failure: Option<&EgressNamespaceFailure>,
) -> Result<(), crate::errors::RuntimeError> {
match failure {
Some(failure) if can_spawn_subprocess => Err(unavailable(failure)),
_ => Ok(()),
}
}
fn unavailable(failure: &EgressNamespaceFailure) -> crate::errors::RuntimeError {
crate::errors::RuntimeError::EgressNamespaceUnavailable {
blocker: failure.blocker,
reason: failure.reason(),
required_by: Vec::new(),
}
}
fn session_netns_creation_error(
error: NamespaceAttemptError,
userns_grant: crate::sealed::UsernsGrant,
attachment: crate::sealed::ProfileAttachment,
) -> String {
match error {
NamespaceAttemptError::Creator(detail) => {
format!("sandbox: could not create this session's network namespace: {detail}")
}
step => format!(
"sandbox: E-CAP-005: {}",
unavailable(&failure_of(step, userns_grant, attachment))
),
}
}
#[derive(Debug, Clone)]
pub(crate) struct CapsuleNetnsPlan {
pub(crate) tcp_ports: Vec<u16>,
pub(crate) uid: u32,
pub(crate) gid: u32,
}
impl CapsuleNetnsPlan {
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
pub(crate) fn resolve(tcp_ports: Vec<u16>) -> Self {
let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
Self {
tcp_ports,
uid,
gid,
}
}
pub(crate) fn socket_count(&self) -> usize {
self.tcp_ports.len() + 1
}
}
#[derive(Debug, Default)]
pub(crate) struct SessionNetns {
state: std::sync::Mutex<SessionNetnsState>,
#[cfg(test)]
creations: std::sync::atomic::AtomicUsize,
}
#[derive(Debug, Default)]
enum SessionNetnsState {
#[default]
NotYetCreated,
#[cfg(target_os = "linux")]
Established(linux::EstablishedNetns),
Ended,
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone)]
pub(crate) struct SessionNetnsJoin {
user_ns: std::sync::Arc<std::os::fd::OwnedFd>,
net_ns: std::sync::Arc<std::os::fd::OwnedFd>,
}
#[cfg(target_os = "linux")]
impl SessionNetnsJoin {
pub(crate) fn raw_fds(&self) -> (std::os::fd::RawFd, std::os::fd::RawFd) {
use std::os::fd::AsRawFd;
(self.user_ns.as_raw_fd(), self.net_ns.as_raw_fd())
}
}
impl SessionNetns {
pub(crate) fn new() -> Self {
Self::default()
}
#[cfg(target_os = "linux")]
pub(crate) fn establish(
&self,
tcp_ports: &[u16],
policy: crate::egress_proxy::EgressPolicy,
) -> Result<SessionNetnsJoin, String> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match &*state {
SessionNetnsState::Established(established) => return Ok(established.join()),
SessionNetnsState::Ended => {
return Err(
"sandbox: this session has ended, and its network namespace was released \
with it; no further command runs in it"
.to_string(),
)
}
SessionNetnsState::NotYetCreated => {}
}
let plan = CapsuleNetnsPlan::resolve(tcp_ports.to_vec());
let created = linux::create_session_netns(&plan).map_err(|error| {
session_netns_creation_error(
error,
crate::sealed::userns_grant(),
crate::sealed::inspect_profile_attachment(),
)
})?;
let established = created.serve(policy).map_err(|detail| {
format!("sandbox: could not create this session's network namespace: {detail}")
})?;
let join = established.join();
*state = SessionNetnsState::Established(established);
#[cfg(test)]
self.creations
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(join)
}
pub(crate) fn end(&self) {
let previous = std::mem::replace(
&mut *self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
SessionNetnsState::Ended,
);
#[cfg(target_os = "linux")]
if let SessionNetnsState::Established(established) = previous {
established.release();
}
#[cfg(not(target_os = "linux"))]
drop(previous);
}
#[cfg(test)]
pub(crate) fn creations(&self) -> usize {
self.creations.load(std::sync::atomic::Ordering::SeqCst)
}
}
impl Drop for SessionNetns {
fn drop(&mut self) {
self.end();
}
}
pub(crate) const STEP_RECORD_LEN: usize = 8;
pub(crate) fn encode_step_record(code: u32, errno: i32) -> [u8; STEP_RECORD_LEN] {
let mut record = [0u8; STEP_RECORD_LEN];
record[..4].copy_from_slice(&code.to_ne_bytes());
record[4..].copy_from_slice(&errno.to_ne_bytes());
record
}
pub(crate) fn decode_step_record(record: &[u8; STEP_RECORD_LEN]) -> (u32, i32) {
(
u32::from_ne_bytes([record[0], record[1], record[2], record[3]]),
i32::from_ne_bytes([record[4], record[5], record[6], record[7]]),
)
}
#[cfg(target_os = "linux")]
pub(crate) use linux::{create_session_netns, join_session_netns, reap_child};
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
mod linux {
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::sync::Arc;
use super::{
decode_step_record, encode_step_record, CapsuleNetnsPlan, CreationStep,
NamespaceAttemptError, SessionNetnsJoin, MAX_NAMESPACE_SOCKETS, MAX_SESSION_NETNS_FDS,
STEP_RECORD_LEN,
};
const RTM_NEWROUTE: u16 = 24;
const NLM_F_REQUEST: u16 = 0x001;
const NLM_F_ACK: u16 = 0x004;
const NLM_F_EXCL: u16 = 0x200;
const NLM_F_CREATE: u16 = 0x400;
const NLMSG_ERROR: u16 = 2;
const RT_TABLE_LOCAL: u8 = 255;
const RTPROT_BOOT: u8 = 3;
const RT_SCOPE_HOST: u8 = 254;
const RTN_LOCAL: u8 = 2;
const RTA_OIF: u16 = 4;
const LOOPBACK_IFINDEX: u32 = 1;
const NLMSGHDR_LEN: usize = 16;
const RTMSG_LEN: usize = 12;
const RTATTR_OIF_LEN: usize = 8;
const ROUTE_REQUEST_LEN: usize = NLMSGHDR_LEN + RTMSG_LEN + RTATTR_OIF_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct CreationFailure {
step: CreationStep,
errno: i32,
}
impl CreationFailure {
unsafe fn at(step: CreationStep) -> Self {
let errno = unsafe { *libc::__errno_location() };
Self { step, errno }
}
fn encode(self) -> [u8; STEP_RECORD_LEN] {
encode_step_record(self.step as u32, self.errno)
}
fn decode(record: &[u8; STEP_RECORD_LEN]) -> Option<Self> {
let (code, errno) = decode_step_record(record);
let step = CreationStep::from_code(code)?;
Some(Self { step, errno })
}
}
impl From<CreationFailure> for NamespaceAttemptError {
fn from(failure: CreationFailure) -> Self {
NamespaceAttemptError::Step {
step: failure.step,
errno: failure.errno,
}
}
}
unsafe fn create_capsule_netns(
plan: &CapsuleNetnsPlan,
sock_fd: RawFd,
) -> Result<(), CreationFailure> {
unsafe {
if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNET) != 0 {
return Err(CreationFailure::at(CreationStep::Unshare));
}
if write_identity_maps(plan.uid, plan.gid).is_err() {
return Err(CreationFailure::at(CreationStep::IdentityMaps));
}
if bring_loopback_up().is_err() {
return Err(CreationFailure::at(CreationStep::Loopback));
}
if add_local_default_route().is_err() {
return Err(CreationFailure::at(CreationStep::LocalRoute));
}
let mut fds = [-1 as RawFd; MAX_SESSION_NETNS_FDS];
let mut count = 0usize;
for port in &plan.tcp_ports {
let fd = bind_tcp_listener(*port);
if fd < 0 {
let failure = CreationFailure::at(CreationStep::TcpListener);
close_all(&fds[..count]);
return Err(failure);
}
fds[count] = fd;
count += 1;
}
let resolver = bind_dns_socket();
if resolver < 0 {
let failure = CreationFailure::at(CreationStep::Resolver);
close_all(&fds[..count]);
return Err(failure);
}
fds[count] = resolver;
count += 1;
for namespace in [c"/proc/self/ns/user", c"/proc/self/ns/net"] {
let fd = libc::open(namespace.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC);
if fd < 0 {
let failure = CreationFailure::at(CreationStep::NamespaceHandles);
close_all(&fds[..count]);
return Err(failure);
}
fds[count] = fd;
count += 1;
}
let sent = send_fds(sock_fd, &fds[..count]);
let failure = CreationFailure::at(CreationStep::HandOff);
close_all(&fds[..count]);
sent.map_err(|()| failure)
}
}
pub(crate) unsafe fn join_session_netns(user_fd: RawFd, net_fd: RawFd) -> io::Result<()> {
unsafe {
if libc::setns(user_fd, libc::CLONE_NEWUSER) != 0 {
return Err(io::Error::last_os_error());
}
if libc::setns(net_fd, libc::CLONE_NEWNET) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub(crate) struct CreatedNetns {
sockets: Vec<OwnedFd>,
user_ns: OwnedFd,
net_ns: OwnedFd,
}
impl CreatedNetns {
pub(crate) fn namespace_handles(&self) -> (RawFd, RawFd) {
(self.user_ns.as_raw_fd(), self.net_ns.as_raw_fd())
}
pub(super) fn serve(
self,
policy: crate::egress_proxy::EgressPolicy,
) -> Result<EstablishedNetns, String> {
let proxy = crate::egress_proxy::start_egress_proxy(self.sockets, policy)
.map_err(|error| format!("starting the session's egress proxy failed: {error}"))?;
Ok(EstablishedNetns {
user_ns: Arc::new(self.user_ns),
net_ns: Arc::new(self.net_ns),
proxy,
})
}
}
#[derive(Debug)]
pub(super) struct EstablishedNetns {
user_ns: Arc<OwnedFd>,
net_ns: Arc<OwnedFd>,
proxy: crate::egress_proxy::EgressProxyHandle,
}
impl EstablishedNetns {
pub(super) fn join(&self) -> SessionNetnsJoin {
SessionNetnsJoin {
user_ns: Arc::clone(&self.user_ns),
net_ns: Arc::clone(&self.net_ns),
}
}
pub(super) fn release(self) {
self.proxy.shutdown();
}
}
pub(crate) fn create_session_netns(
plan: &CapsuleNetnsPlan,
) -> Result<CreatedNetns, NamespaceAttemptError> {
if plan.socket_count() > MAX_NAMESPACE_SOCKETS {
return Err(NamespaceAttemptError::Creator(format!(
"{} namespace sockets requested, more than the {MAX_NAMESPACE_SOCKETS} one \
hand-off carries",
plan.socket_count()
)));
}
let mut pair = [0i32; 2];
let rc = unsafe {
libc::socketpair(
libc::AF_UNIX,
libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
0,
pair.as_mut_ptr(),
)
};
if rc != 0 {
return Err(NamespaceAttemptError::Creator(format!(
"socketpair() for the namespace hand-off failed: {}",
io::Error::last_os_error()
)));
}
let (parent_end, creator_end) =
unsafe { (OwnedFd::from_raw_fd(pair[0]), OwnedFd::from_raw_fd(pair[1])) };
let creator_fd = creator_end.as_raw_fd();
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(NamespaceAttemptError::Creator(format!(
"fork() for the namespace creator failed: {}",
io::Error::last_os_error()
)));
}
if pid == 0 {
unsafe {
match create_capsule_netns(plan, creator_fd) {
Ok(()) => libc::_exit(0),
Err(failure) => {
let record = failure.encode();
libc::send(creator_fd, record.as_ptr().cast(), record.len(), 0);
libc::_exit(1);
}
}
}
}
drop(creator_end);
let status = reap_child(pid).map_err(|error| {
NamespaceAttemptError::Creator(format!(
"waiting for the namespace creator failed: {error}"
))
})?;
match receive_session_netns(parent_end.as_raw_fd(), plan.socket_count())
.map_err(NamespaceAttemptError::Creator)?
{
Received::Handles(mut fds) => {
let net_ns = fds.pop().expect("the count was checked on receipt");
let user_ns = fds.pop().expect("the count was checked on receipt");
Ok(CreatedNetns {
sockets: fds,
user_ns,
net_ns,
})
}
Received::Failure(failure) => Err(failure.into()),
Received::Nothing => Err(NamespaceAttemptError::Creator(format!(
"the namespace creator exited without handing anything over ({status})"
))),
}
}
pub(crate) fn reap_child(pid: libc::pid_t) -> io::Result<String> {
let mut status: libc::c_int = 0;
loop {
let waited = unsafe { libc::waitpid(pid, &mut status, 0) };
if waited == pid {
break;
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
return Err(error);
}
}
Ok(if libc::WIFEXITED(status) {
format!("exit status {}", libc::WEXITSTATUS(status))
} else if libc::WIFSIGNALED(status) {
format!("killed by signal {}", libc::WTERMSIG(status))
} else {
format!("wait status {status}")
})
}
#[cfg(test)]
pub(super) fn decode_failure_record(step: u32, errno: i32) -> Option<NamespaceAttemptError> {
let record = encode_step_record(step, errno);
CreationFailure::decode(&record).map(|failure| {
assert_eq!(failure.encode(), record);
failure.into()
})
}
enum Received {
Handles(Vec<OwnedFd>),
Failure(CreationFailure),
Nothing,
}
unsafe fn write_identity_maps(uid: u32, gid: u32) -> Result<(), ()> {
unsafe {
let previous = crate::sealed::make_dumpable_for_map_writes();
let _ = crate::sealed::write_decimal_map(c"/proc/self/setgroups", None, 0);
let mapped = crate::sealed::write_decimal_map(c"/proc/self/uid_map", Some(uid), uid)
.is_ok()
&& crate::sealed::write_decimal_map(c"/proc/self/gid_map", Some(gid), gid).is_ok();
crate::sealed::restore_dumpable(previous);
if mapped {
Ok(())
} else {
Err(())
}
}
}
unsafe fn bring_loopback_up() -> Result<(), ()> {
unsafe {
let sock = libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0);
if sock < 0 {
return Err(());
}
let mut ifr: libc::ifreq = std::mem::zeroed();
ifr.ifr_name[0] = b'l' as libc::c_char;
ifr.ifr_name[1] = b'o' as libc::c_char;
if libc::ioctl(sock, libc::SIOCGIFFLAGS, &mut ifr) < 0 {
libc::close(sock);
return Err(());
}
ifr.ifr_ifru.ifru_flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short;
let rc = libc::ioctl(sock, libc::SIOCSIFFLAGS, &ifr);
libc::close(sock);
if rc < 0 {
return Err(());
}
Ok(())
}
}
unsafe fn add_local_default_route() -> Result<(), ()> {
unsafe {
let mut request = [0u8; ROUTE_REQUEST_LEN];
request[0..4].copy_from_slice(&(ROUTE_REQUEST_LEN as u32).to_ne_bytes());
request[4..6].copy_from_slice(&RTM_NEWROUTE.to_ne_bytes());
request[6..8].copy_from_slice(
&(NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL).to_ne_bytes(),
);
request[8..12].copy_from_slice(&1u32.to_ne_bytes()); request[12..16].copy_from_slice(&0u32.to_ne_bytes());
let rtm = NLMSGHDR_LEN;
request[rtm] = libc::AF_INET as u8;
request[rtm + 1] = 0; request[rtm + 2] = 0; request[rtm + 3] = 0; request[rtm + 4] = RT_TABLE_LOCAL;
request[rtm + 5] = RTPROT_BOOT;
request[rtm + 6] = RT_SCOPE_HOST;
request[rtm + 7] = RTN_LOCAL;
request[rtm + 8..rtm + 12].copy_from_slice(&0u32.to_ne_bytes());
let attr = rtm + RTMSG_LEN;
request[attr..attr + 2].copy_from_slice(&(RTATTR_OIF_LEN as u16).to_ne_bytes());
request[attr + 2..attr + 4].copy_from_slice(&RTA_OIF.to_ne_bytes());
request[attr + 4..attr + 8].copy_from_slice(&LOOPBACK_IFINDEX.to_ne_bytes());
let sock = libc::socket(
libc::AF_NETLINK,
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
libc::NETLINK_ROUTE,
);
if sock < 0 {
return Err(());
}
let mut address: libc::sockaddr_nl = std::mem::zeroed();
address.nl_family = libc::AF_NETLINK as u16;
let sent = libc::sendto(
sock,
request.as_ptr().cast(),
ROUTE_REQUEST_LEN,
0,
std::ptr::addr_of!(address).cast(),
std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
);
if sent < 0 {
libc::close(sock);
return Err(());
}
let mut response = [0u8; 256];
let received = libc::recv(sock, response.as_mut_ptr().cast(), response.len(), 0);
libc::close(sock);
if received < (NLMSGHDR_LEN + 4) as isize {
return Err(());
}
if u16::from_ne_bytes([response[4], response[5]]) == NLMSG_ERROR {
let code = i32::from_ne_bytes([
response[NLMSGHDR_LEN],
response[NLMSGHDR_LEN + 1],
response[NLMSGHDR_LEN + 2],
response[NLMSGHDR_LEN + 3],
]);
if code != 0 {
return Err(());
}
}
Ok(())
}
}
unsafe fn bind_tcp_listener(port: u16) -> RawFd {
unsafe {
let fd = libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_CLOEXEC, 0);
if fd < 0 {
return -1;
}
let mut address: libc::sockaddr_in = std::mem::zeroed();
address.sin_family = libc::AF_INET as libc::sa_family_t;
address.sin_port = port.to_be();
address.sin_addr.s_addr = libc::INADDR_ANY.to_be();
if libc::bind(
fd,
std::ptr::addr_of!(address).cast(),
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
) != 0
|| libc::listen(fd, 128) != 0
{
libc::close(fd);
return -1;
}
fd
}
}
unsafe fn bind_dns_socket() -> RawFd {
unsafe {
let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0);
if fd < 0 {
return -1;
}
let enable: libc::c_int = 1;
if libc::setsockopt(
fd,
libc::IPPROTO_IP,
libc::IP_PKTINFO,
std::ptr::addr_of!(enable).cast(),
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
) != 0
{
libc::close(fd);
return -1;
}
let mut address: libc::sockaddr_in = std::mem::zeroed();
address.sin_family = libc::AF_INET as libc::sa_family_t;
address.sin_port = super::DNS_PORT_BE;
address.sin_addr.s_addr = libc::INADDR_ANY.to_be();
if libc::bind(
fd,
std::ptr::addr_of!(address).cast(),
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
) != 0
{
libc::close(fd);
return -1;
}
fd
}
}
unsafe fn close_all(fds: &[RawFd]) {
for fd in fds {
if *fd >= 0 {
unsafe { libc::close(*fd) };
}
}
}
const CMSG_BUFFER_LEN: usize = 128;
const _: () = assert!(
unsafe {
libc::CMSG_SPACE((MAX_SESSION_NETNS_FDS * std::mem::size_of::<RawFd>()) as u32) as usize
} <= CMSG_BUFFER_LEN,
"CMSG_BUFFER_LEN is too small to hold MAX_SESSION_NETNS_FDS descriptors plus the \
cmsghdr; raise it to match"
);
#[repr(C, align(8))]
struct CmsgBuffer([u8; CMSG_BUFFER_LEN]);
unsafe fn send_fds(sock_fd: RawFd, fds: &[RawFd]) -> Result<(), ()> {
unsafe {
let mut payload = [0u8; 1];
let mut iov = libc::iovec {
iov_base: payload.as_mut_ptr().cast(),
iov_len: payload.len(),
};
let mut control = CmsgBuffer([0u8; CMSG_BUFFER_LEN]);
let bytes = std::mem::size_of_val(fds) as u32;
let mut msg: libc::msghdr = std::mem::zeroed();
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = control.0.as_mut_ptr().cast();
msg.msg_controllen = libc::CMSG_SPACE(bytes) as _;
let cmsg = libc::CMSG_FIRSTHDR(&msg);
if cmsg.is_null() {
return Err(());
}
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(bytes) as _;
std::ptr::copy_nonoverlapping(
fds.as_ptr().cast::<u8>(),
libc::CMSG_DATA(cmsg),
bytes as usize,
);
if libc::sendmsg(sock_fd, &msg, 0) < 0 {
return Err(());
}
Ok(())
}
}
fn receive_session_netns(sock_fd: RawFd, expected_sockets: usize) -> Result<Received, String> {
unsafe {
let mut payload = [0u8; STEP_RECORD_LEN];
let mut iov = libc::iovec {
iov_base: payload.as_mut_ptr().cast(),
iov_len: payload.len(),
};
let mut control = CmsgBuffer([0u8; CMSG_BUFFER_LEN]);
let mut msg: libc::msghdr = std::mem::zeroed();
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = control.0.as_mut_ptr().cast();
msg.msg_controllen = control.0.len() as _;
let received = libc::recvmsg(
sock_fd,
&mut msg,
libc::MSG_DONTWAIT | libc::MSG_CMSG_CLOEXEC,
);
if received < 0 {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::WouldBlock {
return Ok(Received::Nothing);
}
return Err(format!("receiving the namespace hand-off failed: {error}"));
}
let cmsg = libc::CMSG_FIRSTHDR(&msg);
if cmsg.is_null() {
if received as usize == STEP_RECORD_LEN {
if let Some(failure) = CreationFailure::decode(&payload) {
return Ok(Received::Failure(failure));
}
}
return Ok(Received::Nothing);
}
if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
return Err(
"the namespace hand-off did not arrive as an SCM_RIGHTS message".to_string(),
);
}
let bytes = (*cmsg).cmsg_len - libc::CMSG_LEN(0) as usize;
let count = bytes / std::mem::size_of::<RawFd>();
let data = libc::CMSG_DATA(cmsg).cast::<RawFd>();
let mut fds = Vec::with_capacity(count);
for index in 0..count {
fds.push(OwnedFd::from_raw_fd(std::ptr::read_unaligned(
data.add(index),
)));
}
if msg.msg_flags & libc::MSG_CTRUNC != 0 {
return Err("the namespace hand-off was truncated".to_string());
}
if count != expected_sockets + 2 {
return Err(format!(
"the namespace creator handed over {count} descriptors; {} were expected \
({expected_sockets} sockets and 2 namespace handles)",
expected_sockets + 2
));
}
Ok(Received::Handles(fds))
}
}
}
#[cfg(target_os = "linux")]
const DNS_PORT_BE: u16 = crate::egress_proxy::EGRESS_DNS_PORT.to_be();
#[cfg(test)]
mod tests {
use super::*;
use crate::sealed::{ProfileAttachment, UsernsGrant};
fn attachments() -> [ProfileAttachment; 4] {
[
ProfileAttachment::Sealed {
profile: "mur-sealed".to_string(),
},
ProfileAttachment::Other {
profile: "docker-default".to_string(),
},
ProfileAttachment::Unconfined,
ProfileAttachment::NotReported,
]
}
fn step(step: CreationStep, errno: i32) -> Result<(), NamespaceAttemptError> {
Err(NamespaceAttemptError::Step { step, errno })
}
fn failure(
attempt: Result<(), NamespaceAttemptError>,
grant: UsernsGrant,
attachment: ProfileAttachment,
) -> EgressNamespaceFailure {
egress_namespace_failure(true, attempt, grant, attachment)
.expect("a failed attempt is a failure")
}
#[test]
fn a_created_namespace_is_never_refused_whatever_the_readings() {
for grant in UsernsGrant::ALL {
for attachment in attachments() {
assert_eq!(
egress_namespace_failure(true, Ok(()), *grant, attachment.clone()),
None,
"{grant:?} / {attachment:?}"
);
}
}
}
#[test]
fn a_failed_creation_is_refused_whatever_the_readings() {
let attempts = [
(
step(CreationStep::Unshare, libc::EPERM),
EgressNamespaceBlocker::CapabilityGrantMissing,
),
(
step(CreationStep::Unshare, libc::ENOSPC),
EgressNamespaceBlocker::KernelSupportMissing,
),
(
Err(NamespaceAttemptError::Creator("fork() failed".to_string())),
EgressNamespaceBlocker::CreatorUnavailable,
),
];
for (attempt, blocker) in attempts {
for grant in UsernsGrant::ALL {
for attachment in attachments() {
let failure =
egress_namespace_failure(true, attempt.clone(), *grant, attachment.clone())
.unwrap_or_else(|| {
panic!("{attempt:?} under {grant:?} / {attachment:?}")
});
assert_eq!(failure.blocker, blocker, "{grant:?} / {attachment:?}");
}
}
}
}
#[test]
fn only_an_unshare_the_kernel_cannot_honour_is_missing_kernel_support() {
for errno in [libc::ENOSPC, libc::EINVAL, libc::ENOSYS, libc::EUSERS] {
assert_eq!(
failure(
step(CreationStep::Unshare, errno),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined
)
.blocker,
EgressNamespaceBlocker::KernelSupportMissing,
"errno {errno}"
);
}
for (failed, errno) in [
(CreationStep::Unshare, libc::EPERM),
(CreationStep::Unshare, libc::EACCES),
(CreationStep::IdentityMaps, libc::EPERM),
(CreationStep::Loopback, libc::EPERM),
(CreationStep::LocalRoute, libc::EINVAL),
(CreationStep::Resolver, libc::ENOSPC),
] {
assert_eq!(
failure(
step(failed, errno),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined
)
.blocker,
EgressNamespaceBlocker::CapabilityGrantMissing,
"{failed:?} errno {errno}"
);
}
}
#[test]
fn a_creator_that_could_not_run_is_unavailable_and_keeps_its_detail() {
let detail = "fork() for the namespace creator failed: Resource temporarily unavailable \
(os error 11)";
let failure = failure(
Err(NamespaceAttemptError::Creator(detail.to_string())),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined,
);
assert_eq!(failure.blocker, EgressNamespaceBlocker::CreatorUnavailable);
assert_eq!(failure.errno, None);
let reason = failure.reason();
assert!(reason.starts_with(detail), "{reason}");
assert!(!reason.contains("apparmor_parser"), "{reason}");
assert!(!reason.contains("CONFIG_USER_NS"), "{reason}");
assert!(!reason.contains("user.max_user_namespaces"), "{reason}");
}
#[test]
fn non_linux_is_never_refused_for_a_mechanism_it_never_had() {
for attempt in [
Ok(()),
step(CreationStep::Unshare, libc::EPERM),
Err(NamespaceAttemptError::Creator("fork() failed".to_string())),
] {
assert_eq!(
egress_namespace_failure(
false,
attempt,
UsernsGrant::Withheld,
ProfileAttachment::NotReported
),
None
);
}
#[cfg(not(target_os = "linux"))]
assert_eq!(detect_egress_namespace_failure(), None);
}
#[test]
fn the_check_refuses_exactly_a_capsule_that_can_spawn_on_a_failing_host() {
let failure = failure(
step(CreationStep::Unshare, libc::EPERM),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined,
);
match check_egress_namespace(true, Some(&failure)) {
Err(crate::errors::RuntimeError::EgressNamespaceUnavailable {
blocker,
reason,
..
}) => {
assert_eq!(blocker, failure.blocker);
assert_eq!(reason, failure.reason());
}
other => panic!("expected the E-CAP-005 refusal, got {other:?}"),
}
assert!(check_egress_namespace(false, Some(&failure)).is_ok());
assert!(check_egress_namespace(true, None).is_ok());
assert!(check_egress_namespace(false, None).is_ok());
}
#[test]
fn an_unconfined_binary_on_a_restricted_host_is_told_the_profile_before_the_sysctl() {
let reason = failure(
step(CreationStep::Unshare, libc::EPERM),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined,
)
.reason();
assert!(
reason.starts_with(
"unshare(CLONE_NEWUSER | CLONE_NEWNET) failed with EPERM (Operation not \
permitted)."
),
"{reason}"
);
assert!(
reason.contains("kernel.apparmor_restrict_unprivileged_userns=1"),
"{reason}"
);
let profile = reason
.find("apparmor_parser -r")
.expect("the profile install command must be named");
let sysctl = reason
.find("kernel.apparmor_restrict_unprivileged_userns=0")
.expect("the fallback must still be named for hosts that cannot load a profile");
assert!(
profile < sysctl,
"the profile install must come first: {reason}"
);
assert!(reason.contains("LAST RESORT"), "{reason}");
assert!(reason.contains("every program on the machine"), "{reason}");
assert!(
reason.contains("scripts/install-dev-apparmor.sh"),
"{reason}"
);
assert!(
reason.ends_with(
"Host readings: kernel.apparmor_restrict_unprivileged_userns=1; AppArmor profile \
confining mur: unconfined."
),
"{reason}"
);
}
#[test]
fn a_binary_another_profile_confines_is_given_the_container_remedy_not_the_sysctl() {
let reason = failure(
step(CreationStep::Unshare, libc::EPERM),
UsernsGrant::Withheld,
ProfileAttachment::Other {
profile: "docker-default".to_string(),
},
)
.reason();
assert!(reason.contains("`docker-default`"), "{reason}");
assert!(!reason.contains("--cap-add"), "{reason}");
assert!(reason.contains("seccomp profile"), "{reason}");
assert!(
reason.contains("unshare(CLONE_NEWUSER) and setns"),
"{reason}"
);
assert!(reason.contains("seccompProfile: Localhost"), "{reason}");
assert!(
!reason.contains("kernel.apparmor_restrict_unprivileged_userns=0"),
"{reason}"
);
assert!(!reason.contains("apparmor_parser"), "{reason}");
assert!(
reason.ends_with("AppArmor profile confining mur: docker-default."),
"{reason}"
);
}
#[test]
fn a_failure_under_the_sealed_profile_points_at_a_stale_load() {
let reason = failure(
step(CreationStep::Unshare, libc::EPERM),
UsernsGrant::ProfileConfining,
ProfileAttachment::Sealed {
profile: "mur-sealed".to_string(),
},
)
.reason();
assert!(
reason.contains("sudo apparmor_parser -r /etc/apparmor.d/mur-sealed"),
"{reason}"
);
assert!(reason.contains("stale"), "{reason}");
assert!(!reason.contains("LAST RESORT"), "{reason}");
}
#[test]
fn without_the_restriction_the_cause_is_not_apparmor() {
for (grant, readings) in [
(
UsernsGrant::RestrictionDisabledHostWide,
"kernel.apparmor_restrict_unprivileged_userns=0",
),
(UsernsGrant::ApparmorAbsent, "AppArmor not enabled"),
] {
for attachment in [
ProfileAttachment::Unconfined,
ProfileAttachment::NotReported,
] {
let reason =
failure(step(CreationStep::Unshare, libc::EPERM), grant, attachment).reason();
assert!(
!reason.contains("apparmor_restrict_unprivileged_userns=1"),
"{reason}"
);
assert!(!reason.contains("apparmor_parser"), "{reason}");
assert!(!reason.contains("--cap-add"), "{reason}");
assert!(!reason.contains("CAP_SYS_ADMIN"), "{reason}");
assert!(
reason.contains("seccomp profile is the usual blocker"),
"{reason}"
);
assert!(
reason.contains("unshare(CLONE_NEWUSER) and setns"),
"{reason}"
);
assert!(reason.contains("--security-opt seccomp=<file>"), "{reason}");
assert!(
reason.contains(&format!("Host readings: {readings};")),
"{reason}"
);
}
}
}
#[test]
fn another_security_modules_label_is_not_blamed_as_an_apparmor_profile() {
let reason = failure(
step(CreationStep::Unshare, libc::EPERM),
UsernsGrant::ApparmorAbsent,
ProfileAttachment::Other {
profile: "unconfined_u:unconfined_r:unconfined_t:s0".to_string(),
},
)
.reason();
assert!(reason.contains("not AppArmor"), "{reason}");
assert!(!reason.contains("the AppArmor profile `"), "{reason}");
}
#[test]
fn a_namespace_created_and_then_refused_says_which_half_failed() {
let configure = failure(
step(CreationStep::Loopback, libc::EPERM),
UsernsGrant::RestrictionDisabledHostWide,
ProfileAttachment::Unconfined,
)
.reason();
assert!(
configure.starts_with("bringing lo up failed with EPERM"),
"{configure}"
);
assert!(configure.contains("CAP_NET_ADMIN"), "{configure}");
let owned = failure(
step(CreationStep::IdentityMaps, libc::EACCES),
UsernsGrant::RestrictionDisabledHostWide,
ProfileAttachment::Unconfined,
)
.reason();
assert!(
owned.starts_with("writing uid_map/gid_map failed with EACCES (Permission denied)."),
"{owned}"
);
assert!(owned.contains("could not be owned"), "{owned}");
}
#[test]
fn missing_kernel_support_names_the_limit_or_the_config_symbol_and_no_apparmor() {
for (errno, named) in [
(libc::ENOSPC, "user.max_user_namespaces"),
(libc::EINVAL, "CONFIG_USER_NS"),
(libc::ENOSYS, "CONFIG_USER_NS"),
(libc::EUSERS, "nesting limit"),
] {
let reason = failure(
step(CreationStep::Unshare, errno),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined,
)
.reason();
assert!(reason.contains(named), "{reason}");
assert!(
reason.contains(errno_name(errno).expect("a named errno")),
"{reason}"
);
assert!(!reason.contains("apparmor_parser"), "{reason}");
assert!(!reason.contains("LAST RESORT"), "{reason}");
}
}
#[test]
fn an_unnamed_errno_is_rendered_by_number() {
assert_eq!(errno_name(libc::EPERM), Some("EPERM"));
assert_eq!(errno_name(-7), None);
let failure = failure(
step(CreationStep::HandOff, 4095),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined,
);
assert_eq!(
failure.summary(),
"handing the namespace to the runtime failed with errno 4095"
);
}
#[test]
fn the_summary_names_the_call_and_the_errno() {
let failed = failure(
step(CreationStep::Unshare, libc::EPERM),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined,
);
assert_eq!(
failed.summary(),
"unshare(CLONE_NEWUSER | CLONE_NEWNET) failed with EPERM"
);
let creator = failure(
Err(NamespaceAttemptError::Creator("fork() failed".to_string())),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined,
);
assert_eq!(creator.summary(), "fork() failed");
}
#[test]
fn every_step_round_trips_through_its_code() {
for step in CreationStep::ALL {
assert_eq!(CreationStep::from_code(step as u32), Some(step));
}
assert_eq!(CreationStep::from_code(0), None);
assert_eq!(CreationStep::from_code(99), None);
}
#[test]
fn a_session_creation_failure_is_refused_in_the_staging_words() {
for (failed, errno, grant, attachment) in [
(
CreationStep::Unshare,
libc::EPERM,
UsernsGrant::Withheld,
ProfileAttachment::Unconfined,
),
(
CreationStep::LocalRoute,
libc::EPERM,
UsernsGrant::Withheld,
ProfileAttachment::Other {
profile: "docker-default".to_string(),
},
),
(
CreationStep::Unshare,
libc::ENOSPC,
UsernsGrant::RestrictionDisabledHostWide,
ProfileAttachment::Unconfined,
),
] {
let staging =
egress_namespace_failure(true, step(failed, errno), grant, attachment.clone());
let staging_refusal = check_egress_namespace(true, staging.as_ref())
.expect_err("staging refuses")
.to_string();
let establish = session_netns_creation_error(
NamespaceAttemptError::Step {
step: failed,
errno,
},
grant,
attachment,
);
assert_eq!(establish, format!("sandbox: E-CAP-005: {staging_refusal}"));
assert!(
establish.contains(&staging.expect("a failure").reason()),
"{establish}"
);
}
}
#[test]
fn a_session_creator_that_could_not_run_keeps_the_runtime_account() {
let error = session_netns_creation_error(
NamespaceAttemptError::Creator("fork() for the namespace creator failed".to_string()),
UsernsGrant::Withheld,
ProfileAttachment::Unconfined,
);
assert_eq!(
error,
"sandbox: could not create this session's network namespace: fork() for the \
namespace creator failed"
);
}
#[test]
fn the_socket_count_is_one_per_port_plus_the_resolver() {
let plan = CapsuleNetnsPlan {
tcp_ports: vec![80, 443],
uid: 1000,
gid: 1000,
};
assert_eq!(plan.socket_count(), 3);
}
#[test]
fn a_capsule_with_no_tcp_egress_still_binds_the_resolver() {
let plan = CapsuleNetnsPlan {
tcp_ports: Vec::new(),
uid: 1000,
gid: 1000,
};
assert_eq!(plan.socket_count(), 1);
}
#[test]
fn the_widest_allowlist_still_fits_one_scm_rights_message() {
let plan = CapsuleNetnsPlan {
tcp_ports: (0..crate::egress_proxy::MAX_EGRESS_TCP_PORTS as u16).collect(),
uid: 1000,
gid: 1000,
};
assert!(plan.socket_count() <= MAX_NAMESPACE_SOCKETS);
assert_eq!(MAX_SESSION_NETNS_FDS, MAX_NAMESPACE_SOCKETS + 2);
}
#[cfg(target_os = "linux")]
#[test]
fn an_ended_session_refuses_to_create_a_namespace() {
let netns = SessionNetns::new();
netns.end();
let refusal = netns
.establish(
&[],
crate::egress_proxy::EgressPolicy::new(Vec::new(), Vec::new()),
)
.expect_err("an ended session creates nothing");
assert!(refusal.contains("session has ended"), "{refusal}");
assert_eq!(netns.creations(), 0);
}
#[cfg(target_os = "linux")]
#[test]
fn a_session_creates_its_namespace_once_and_ends_it_for_good() {
if skip_without_egress_namespace(
"a_session_creates_its_namespace_once_and_ends_it_for_good",
) {
return;
}
let policy = || crate::egress_proxy::EgressPolicy::new(Vec::new(), Vec::new());
let netns = SessionNetns::new();
assert_eq!(
netns.creations(),
0,
"nothing is created before it is needed"
);
let first = netns
.establish(&[], policy())
.expect("the namespace is created");
let second = netns
.establish(&[], policy())
.expect("the namespace is reused");
assert_eq!(netns.creations(), 1);
let inode = |fd: std::os::fd::RawFd| {
std::fs::metadata(format!("/proc/self/fd/{fd}"))
.map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
.unwrap()
};
assert_eq!(inode(first.raw_fds().1), inode(second.raw_fds().1));
let host_netns = std::fs::metadata("/proc/self/ns/net")
.map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
.unwrap();
assert_ne!(
inode(first.raw_fds().1),
host_netns,
"the session's namespace is not the runtime's own"
);
netns.end();
netns.end();
assert!(netns.establish(&[], policy()).is_err());
assert_eq!(
netns.creations(),
1,
"an ended namespace is never recreated"
);
}
#[cfg(target_os = "linux")]
#[test]
fn a_creation_failure_names_its_step_and_errno() {
assert_eq!(
linux::decode_failure_record(4, libc::EPERM),
Some(NamespaceAttemptError::Step {
step: CreationStep::LocalRoute,
errno: libc::EPERM,
})
);
assert_eq!(linux::decode_failure_record(99, 0), None);
}
#[cfg(target_os = "linux")]
#[test]
fn the_probe_and_a_session_creation_agree_about_this_host() {
let probe = detect_egress_namespace_failure();
let session = linux::create_session_netns(&CapsuleNetnsPlan::resolve(Vec::new())).map(drop);
match (&probe, &session) {
(None, Ok(())) => {}
(Some(failure), Err(NamespaceAttemptError::Step { step, errno })) => {
assert_eq!(failure.failed_call, step.describe());
assert_eq!(failure.errno, Some(*errno));
}
other => panic!("the probe and a session creation disagree: {other:?}"),
}
}
}