1#![cfg_attr(not(target_os = "linux"), allow(dead_code))]
117
118pub(crate) const MAX_NAMESPACE_SOCKETS: usize = crate::egress_proxy::MAX_EGRESS_TCP_PORTS + 1;
120
121pub(crate) const BRIDGE_SOCKET_COUNT: usize = 1;
125
126pub(crate) const NAMESPACE_HANDLE_COUNT: usize = 3;
129
130pub(crate) const MAX_SESSION_NETNS_FDS: usize =
136 MAX_NAMESPACE_SOCKETS + BRIDGE_SOCKET_COUNT + NAMESPACE_HANDLE_COUNT;
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum CreationStep {
146 Unshare = 1,
147 IdentityMaps = 2,
148 Loopback = 3,
149 LocalRoute = 4,
150 TcpListener = 5,
151 Resolver = 6,
152 NamespaceHandles = 7,
153 HandOff = 8,
154 PidNamespace = 9,
156 Holder = 10,
158 BridgeListener = 11,
160}
161
162impl CreationStep {
163 const ALL: [CreationStep; 11] = [
164 Self::Unshare,
165 Self::IdentityMaps,
166 Self::Loopback,
167 Self::LocalRoute,
168 Self::TcpListener,
169 Self::Resolver,
170 Self::NamespaceHandles,
171 Self::HandOff,
172 Self::PidNamespace,
173 Self::Holder,
174 Self::BridgeListener,
175 ];
176
177 pub(crate) fn from_code(code: u32) -> Option<Self> {
179 Self::ALL.into_iter().find(|step| *step as u32 == code)
180 }
181
182 #[must_use]
184 pub fn describe(self) -> &'static str {
185 match self {
186 Self::Unshare => "unshare(CLONE_NEWUSER | CLONE_NEWNET)",
187 Self::IdentityMaps => "writing uid_map/gid_map",
188 Self::Loopback => "bringing lo up",
189 Self::LocalRoute => "installing the local default route",
190 Self::TcpListener => "binding a TCP listener inside the namespace",
191 Self::Resolver => "binding UDP 53 inside the namespace",
192 Self::NamespaceHandles => {
193 "opening /proc/self/ns/user, /proc/self/ns/net and /proc/self/ns/pid_for_children"
194 }
195 Self::HandOff => "handing the namespace to the runtime",
196 Self::PidNamespace => "unshare(CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWPID)",
197 Self::Holder => "forking the PID namespace's holder process",
198 Self::BridgeListener => "binding the tool bridge's TCP listener inside the namespace",
199 }
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub enum NamespaceAttemptError {
206 Step { step: CreationStep, errno: i32 },
208 Creator(String),
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
222#[serde(rename_all = "snake_case")]
223pub enum EgressNamespaceBlocker {
224 CapabilityGrantMissing,
229 KernelSupportMissing,
233 CreatorUnavailable,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct EgressNamespaceFailure {
247 pub blocker: EgressNamespaceBlocker,
249 pub failed_call: String,
254 pub errno: Option<i32>,
257 pub userns_grant: crate::sealed::UsernsGrant,
260 pub attachment: crate::sealed::ProfileAttachment,
262 pub profile_gap: crate::sealed::ProfileGap,
265}
266
267impl EgressNamespaceFailure {
268 #[must_use]
271 pub fn summary(&self) -> String {
272 match self.errno {
273 Some(errno) => format!("{} failed with {}", self.failed_call, errno_label(errno)),
274 None => self.failed_call.clone(),
275 }
276 }
277
278 #[must_use]
282 pub fn reason(&self) -> String {
283 let what = match self.errno {
284 Some(errno) => format!(
285 "{} failed with {} ({})",
286 self.failed_call,
287 errno_label(errno),
288 errno_words(errno)
289 ),
290 None => self.failed_call.clone(),
291 };
292 format!("{what}. {} {}", self.cause(), self.readings())
293 }
294
295 fn step(&self) -> Option<CreationStep> {
297 self.errno?;
298 CreationStep::ALL
299 .into_iter()
300 .find(|step| step.describe() == self.failed_call)
301 }
302
303 fn cause(&self) -> String {
304 match self.blocker {
305 EgressNamespaceBlocker::KernelSupportMissing => {
306 kernel_cause(self.step(), self.errno).to_string()
307 }
308 EgressNamespaceBlocker::CreatorUnavailable => {
309 "Likely cause: the namespace creator could not run or stopped before reporting — \
310 a process or descriptor limit, or memory, ran out, or a policy killed it (its \
311 account then reads `killed by signal`) — so whether this host can create \
312 the namespace is unknown, and the capsule is refused rather than run without \
313 it. Re-run once the limit is relieved, or allow the creator's syscalls."
314 .to_string()
315 }
316 EgressNamespaceBlocker::CapabilityGrantMissing
317 if self.step() == Some(CreationStep::Holder) =>
318 {
319 format!(
320 "{} Likely cause: a process limit (RLIMIT_NPROC, or the pids.max of the cgroup \
321 mur runs in) or memory ran out. Re-run once the limit is relieved.",
322 step_context(CreationStep::Holder).unwrap_or_default()
323 )
324 }
325 EgressNamespaceBlocker::CapabilityGrantMissing => {
326 let cause = grant_cause(self.userns_grant, &self.attachment, &self.profile_gap);
327 match self.step().and_then(step_context) {
328 Some(context) => format!("{context} {cause}"),
329 None => cause,
330 }
331 }
332 }
333 }
334
335 fn readings(&self) -> String {
336 host_readings(self.userns_grant, &self.attachment)
337 }
338}
339
340pub(crate) fn host_readings(
344 userns_grant: crate::sealed::UsernsGrant,
345 attachment: &crate::sealed::ProfileAttachment,
346) -> String {
347 use crate::sealed::{ProfileAttachment, UsernsGrant};
348 let restriction = match userns_grant {
349 UsernsGrant::ApparmorAbsent => "AppArmor not enabled",
350 UsernsGrant::RestrictionDisabledHostWide => {
351 "kernel.apparmor_restrict_unprivileged_userns=0"
352 }
353 UsernsGrant::ProfileConfining | UsernsGrant::Withheld => {
354 "kernel.apparmor_restrict_unprivileged_userns=1"
355 }
356 };
357 let profile = match attachment {
358 ProfileAttachment::Sealed { profile } | ProfileAttachment::Other { profile } => {
359 profile.as_str()
360 }
361 ProfileAttachment::Unconfined => "unconfined",
362 ProfileAttachment::NotReported => "not reported",
363 };
364 format!("Host readings: {restriction}; AppArmor profile confining mur: {profile}.")
365}
366
367fn step_context(step: CreationStep) -> Option<&'static str> {
369 match step {
370 CreationStep::Unshare => None,
371 CreationStep::IdentityMaps => Some(
372 "The namespace was created and could not be owned: writing its identity map was \
373 refused.",
374 ),
375 CreationStep::Loopback | CreationStep::LocalRoute => Some(
376 "The namespace was created and owned, and configuring it was refused — the mark of a \
377 confinement that withholds CAP_NET_ADMIN inside the namespace.",
378 ),
379 CreationStep::PidNamespace => Some(
380 "The user and network namespaces were created without CLONE_NEWPID, and creating a \
381 PID namespace with them was refused — the mark of a seccomp profile or confinement \
382 that permits unshare but not CLONE_NEWPID.",
383 ),
384 CreationStep::Holder => Some(
385 "The namespaces were created, and forking the PID namespace's first process failed.",
386 ),
387 CreationStep::TcpListener
388 | CreationStep::Resolver
389 | CreationStep::BridgeListener
390 | CreationStep::NamespaceHandles
391 | CreationStep::HandOff => {
392 Some("The namespace was created and configured, and handing it to the runtime failed.")
393 }
394 }
395}
396
397fn grant_cause(
407 userns_grant: crate::sealed::UsernsGrant,
408 attachment: &crate::sealed::ProfileAttachment,
409 profile_gap: &crate::sealed::ProfileGap,
410) -> String {
411 use crate::sealed::{
412 profile_gap_remedy, ProfileAttachment, ProfileGap, UsernsGrant,
413 SEALED_APPARMOR_PROFILE_NAME, SEALED_APPARMOR_PROFILE_PATH,
414 };
415 const CONTAINER_REMEDY: &str = "Inside a container, the container runtime's seccomp \
416 profile is the usual blocker: it refuses unshare(CLONE_NEWUSER) and setns whatever \
417 capabilities the container holds. Run the container with a seccomp profile that permits \
418 them (Docker: `--security-opt seccomp=<file>`; Kubernetes: `seccompProfile: \
419 Localhost`), or create the network namespace outside the container and run mur inside \
420 it.";
421 match (userns_grant, attachment) {
422 (UsernsGrant::ApparmorAbsent, _) => format!(
423 "Likely cause: not AppArmor, which is not enabled on this host — a container \
424 runtime's seccomp profile, or another security module's policy. \
425 {CONTAINER_REMEDY} On a bare host, check the policy of the security module in use."
426 ),
427 (_, ProfileAttachment::Other { profile }) => format!(
428 "Likely cause: the AppArmor profile `{profile}` confines mur. \
429 kernel.apparmor_restrict_unprivileged_userns does not govern a process another \
430 profile confines: that profile, and a container runtime's seccomp profile, decide \
431 whether it may create a user namespace. Run the container with an AppArmor profile \
432 and a seccomp profile that permit unshare(CLONE_NEWUSER) and setns (Docker: \
433 `--security-opt apparmor=<profile> --security-opt seccomp=<file>`; Kubernetes: \
434 `appArmorProfile` and `seccompProfile: Localhost`), or create the network namespace \
435 outside the container and run mur inside it."
436 ),
437 (UsernsGrant::ProfileConfining, _) | (_, ProfileAttachment::Sealed { .. }) => format!(
438 "Likely cause: the {SEALED_APPARMOR_PROFILE_NAME} AppArmor profile confines mur and \
439 the creation still failed, so the profile the kernel has loaded may be stale. Reload \
440 it with `sudo apparmor_parser -r {SEALED_APPARMOR_PROFILE_PATH}` (for a checkout \
441 build, re-run `scripts/install-dev-apparmor.sh`), and check `mur doctor`'s \
442 comparison of the installed profile with the one this build ships."
443 ),
444 (UsernsGrant::RestrictionDisabledHostWide, _) => format!(
445 "Likely cause: not AppArmor, whose unprivileged-userns restriction is off on this host \
446 — a container runtime's seccomp profile, or another security module's policy. \
447 {CONTAINER_REMEDY}"
448 ),
449 (UsernsGrant::Withheld, attachment) => {
450 let gap = match attachment {
451 ProfileAttachment::Unconfined => profile_gap,
452 _ => &ProfileGap::Undetermined,
453 };
454 format!(
455 "Likely cause: AppArmor's unprivileged-userns restriction \
456 (kernel.apparmor_restrict_unprivileged_userns=1) applies to this binary, which no \
457 {SEALED_APPARMOR_PROFILE_NAME} profile confines. {}",
458 profile_gap_remedy(gap, env!("CARGO_PKG_VERSION"))
459 )
460 }
461 }
462}
463
464fn kernel_cause(step: Option<CreationStep>, errno: Option<i32>) -> &'static str {
466 if step == Some(CreationStep::PidNamespace) {
467 return match errno {
468 Some(libc::ENOSPC) => {
469 "Likely cause: the kernel's limit on PID namespaces — user.max_pid_namespaces is \
470 0, or every namespace it allows is in use. Raise it where it is set \
471 (`sudo sysctl -w user.max_pid_namespaces=10000`), then re-run."
472 }
473 _ => {
474 "Likely cause: this kernel does not provide PID namespaces (built with \
475 CONFIG_PID_NS=n). Run mur on a kernel built with CONFIG_PID_NS=y."
476 }
477 };
478 }
479 match errno {
480 Some(libc::ENOSPC) => {
481 "Likely cause: the kernel's limit on user namespaces — user.max_user_namespaces is 0, \
482 or every namespace it allows is in use. Raise it where it is set, on the host or in \
483 the user namespace mur runs in (`sudo sysctl -w user.max_user_namespaces=10000`), \
484 then re-run."
485 }
486 Some(libc::EUSERS) => {
487 "Likely cause: the kernel's nesting limit for user namespaces — mur already runs \
488 inside as many nested user namespaces as the kernel allows. Run it fewer levels \
489 deep."
490 }
491 _ => {
492 "Likely cause: this kernel does not provide user namespaces (built with \
493 CONFIG_USER_NS=n). Run mur on a kernel built with CONFIG_USER_NS=y."
494 }
495 }
496}
497
498pub(crate) fn errno_name(errno: i32) -> Option<&'static str> {
501 Some(match errno {
502 libc::EPERM => "EPERM",
503 libc::ENOENT => "ENOENT",
504 libc::EBADF => "EBADF",
505 libc::EACCES => "EACCES",
506 libc::EFAULT => "EFAULT",
507 libc::EEXIST => "EEXIST",
508 libc::EXDEV => "EXDEV",
509 libc::ELOOP => "ELOOP",
510 libc::EAFNOSUPPORT => "EAFNOSUPPORT",
511 libc::ENETUNREACH => "ENETUNREACH",
512 libc::ECONNREFUSED => "ECONNREFUSED",
513 libc::ETIMEDOUT => "ETIMEDOUT",
514 libc::EHOSTUNREACH => "EHOSTUNREACH",
515 libc::EINVAL => "EINVAL",
516 libc::ENOSPC => "ENOSPC",
517 libc::ENOSYS => "ENOSYS",
518 libc::EUSERS => "EUSERS",
519 libc::ENOMEM => "ENOMEM",
520 libc::EAGAIN => "EAGAIN",
521 libc::EADDRINUSE => "EADDRINUSE",
522 libc::EMFILE => "EMFILE",
523 libc::ENFILE => "ENFILE",
524 libc::EROFS => "EROFS",
525 libc::ENOTDIR => "ENOTDIR",
526 libc::EBUSY => "EBUSY",
527 _ => return None,
528 })
529}
530
531pub(crate) fn errno_label(errno: i32) -> String {
533 errno_name(errno).map_or_else(|| format!("errno {errno}"), str::to_string)
534}
535
536pub(crate) fn errno_words(errno: i32) -> String {
538 let rendered = std::io::Error::from_raw_os_error(errno).to_string();
539 rendered
540 .strip_suffix(&format!(" (os error {errno})"))
541 .map_or_else(|| rendered.clone(), str::to_string)
542}
543
544pub(crate) fn egress_namespace_failure(
550 is_linux: bool,
551 attempt: Result<(), NamespaceAttemptError>,
552 userns_grant: crate::sealed::UsernsGrant,
553 attachment: crate::sealed::ProfileAttachment,
554 profile_gap: crate::sealed::ProfileGap,
555) -> Option<EgressNamespaceFailure> {
556 if !is_linux {
561 return None;
562 }
563 attempt
564 .err()
565 .map(|error| failure_of(error, userns_grant, attachment, profile_gap))
566}
567
568fn failure_of(
570 error: NamespaceAttemptError,
571 userns_grant: crate::sealed::UsernsGrant,
572 attachment: crate::sealed::ProfileAttachment,
573 profile_gap: crate::sealed::ProfileGap,
574) -> EgressNamespaceFailure {
575 let (blocker, failed_call, errno) = match error {
576 NamespaceAttemptError::Step { step, errno } => {
577 let blocker = match (step, errno) {
578 (
579 CreationStep::Unshare,
580 libc::ENOSPC | libc::EINVAL | libc::ENOSYS | libc::EUSERS,
581 )
582 | (CreationStep::PidNamespace, libc::ENOSPC | libc::EINVAL) => {
583 EgressNamespaceBlocker::KernelSupportMissing
584 }
585 _ => EgressNamespaceBlocker::CapabilityGrantMissing,
586 };
587 (blocker, step.describe().to_string(), Some(errno))
588 }
589 NamespaceAttemptError::Creator(detail) => {
590 (EgressNamespaceBlocker::CreatorUnavailable, detail, None)
591 }
592 };
593 EgressNamespaceFailure {
594 blocker,
595 failed_call,
596 errno,
597 userns_grant,
598 attachment,
599 profile_gap,
600 }
601}
602
603#[must_use]
611pub fn detect_egress_namespace_failure() -> Option<EgressNamespaceFailure> {
612 crate::sandbox::HostProbe::probe().egress_namespace_failure()
613}
614
615pub fn skip_without_egress_namespace(test_name: &str) -> bool {
626 match detect_egress_namespace_failure() {
627 Some(failure) => {
628 crate::runtime_err!("[SKIP-HOST] {test_name}: {}", failure.reason());
629 true
630 }
631 None => false,
632 }
633}
634
635#[cfg(target_os = "linux")]
645#[must_use]
646pub fn host_pid_in_session(runtime_pid: u32, namespace_pid: i32) -> Option<i32> {
647 host_pids_in_sessions(runtime_pid, namespace_pid)
648 .into_iter()
649 .next()
650}
651
652#[cfg(not(target_os = "linux"))]
656#[must_use]
657pub fn host_pid_in_session(_runtime_pid: u32, namespace_pid: i32) -> Option<i32> {
658 (namespace_pid > 0).then_some(namespace_pid)
659}
660
661#[cfg(target_os = "linux")]
664#[must_use]
665pub fn host_pids_in_sessions(runtime_pid: u32, namespace_pid: i32) -> Vec<i32> {
666 let Ok(fds) = std::fs::read_dir(format!("/proc/{runtime_pid}/fd")) else {
667 return Vec::new();
668 };
669 let held: Vec<std::path::PathBuf> = fds
670 .filter_map(|entry| std::fs::read_link(entry.ok()?.path()).ok())
671 .filter(|target| target.to_string_lossy().starts_with("pid:["))
672 .collect();
673 if held.is_empty() {
674 return Vec::new();
675 }
676 let Ok(entries) = std::fs::read_dir("/proc") else {
677 return Vec::new();
678 };
679 entries
680 .filter_map(Result::ok)
681 .filter_map(|entry| entry.file_name().to_str()?.parse::<i32>().ok())
682 .filter(|pid| {
683 let in_session = std::fs::read_link(format!("/proc/{pid}/ns/pid"))
684 .is_ok_and(|namespace| held.contains(&namespace));
685 in_session
686 && std::fs::read_to_string(format!("/proc/{pid}/status")).is_ok_and(|status| {
687 status
688 .lines()
689 .find_map(|line| line.strip_prefix("NSpid:"))
690 .and_then(|fields| fields.split_whitespace().last())
691 .and_then(|last| last.parse::<i32>().ok())
692 == Some(namespace_pid)
693 })
694 })
695 .collect()
696}
697
698#[cfg(target_os = "linux")]
709#[must_use]
710pub fn host_pid_in_workdir(workdir: &std::path::Path, namespace_pid: i32) -> Option<i32> {
711 let workdir = std::fs::canonicalize(workdir).unwrap_or_else(|_| workdir.to_path_buf());
712 std::fs::read_dir("/proc")
713 .ok()?
714 .filter_map(Result::ok)
715 .filter_map(|entry| entry.file_name().to_str()?.parse::<i32>().ok())
716 .find(|pid| {
717 std::fs::read_link(format!("/proc/{pid}/cwd")).is_ok_and(|cwd| cwd == workdir)
718 && std::fs::read_to_string(format!("/proc/{pid}/status")).is_ok_and(|status| {
719 let fields: Vec<&str> = status
720 .lines()
721 .find_map(|line| line.strip_prefix("NSpid:"))
722 .map(|fields| fields.split_whitespace().collect())
723 .unwrap_or_default();
724 fields.len() >= 2
725 && fields.last().and_then(|last| last.parse::<i32>().ok())
726 == Some(namespace_pid)
727 })
728 })
729}
730
731#[cfg(not(target_os = "linux"))]
735#[must_use]
736pub fn host_pid_in_workdir(_workdir: &std::path::Path, namespace_pid: i32) -> Option<i32> {
737 (namespace_pid > 0).then_some(namespace_pid)
738}
739
740pub fn check_egress_namespace(
751 can_spawn_subprocess: bool,
752 failure: Option<&EgressNamespaceFailure>,
753) -> Result<(), crate::errors::RuntimeError> {
754 match failure {
755 Some(failure) if can_spawn_subprocess => Err(unavailable(failure)),
756 _ => Ok(()),
757 }
758}
759
760fn unavailable(failure: &EgressNamespaceFailure) -> crate::errors::RuntimeError {
762 crate::errors::RuntimeError::EgressNamespaceUnavailable {
763 blocker: failure.blocker,
764 reason: failure.reason(),
765 required_by: Vec::new(),
766 }
767}
768
769fn session_netns_creation_error(
775 error: NamespaceAttemptError,
776 userns_grant: crate::sealed::UsernsGrant,
777 attachment: crate::sealed::ProfileAttachment,
778 profile_gap: crate::sealed::ProfileGap,
779) -> String {
780 match error {
781 NamespaceAttemptError::Creator(detail) => {
782 format!("sandbox: could not create this session's network namespace: {detail}")
783 }
784 step => format!(
785 "sandbox: E-CAP-005: {}",
786 unavailable(&failure_of(step, userns_grant, attachment, profile_gap))
787 ),
788 }
789}
790
791#[derive(Debug, Clone)]
800pub(crate) struct CapsuleNetnsPlan {
801 pub(crate) tcp_ports: Vec<u16>,
808 pub(crate) uid: u32,
814 pub(crate) gid: u32,
815 #[cfg(test)]
818 pub(crate) force_pid_namespace_failure: bool,
819}
820
821#[cfg(test)]
822thread_local! {
823 pub(crate) static FORCE_PID_NAMESPACE_FAILURE: std::cell::Cell<bool> =
826 const { std::cell::Cell::new(false) };
827}
828
829#[cfg(test)]
831pub(crate) struct ForcePidNamespaceFailure;
832
833#[cfg(test)]
834impl ForcePidNamespaceFailure {
835 pub(crate) fn arm() -> Self {
836 FORCE_PID_NAMESPACE_FAILURE.with(|flag| flag.set(true));
837 Self
838 }
839}
840
841#[cfg(test)]
842impl Drop for ForcePidNamespaceFailure {
843 fn drop(&mut self) {
844 FORCE_PID_NAMESPACE_FAILURE.with(|flag| flag.set(false));
845 }
846}
847
848impl CapsuleNetnsPlan {
849 #[cfg(target_os = "linux")]
851 #[allow(unsafe_code)]
852 pub(crate) fn resolve(tcp_ports: Vec<u16>) -> Self {
853 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
855 Self {
856 tcp_ports,
857 uid,
858 gid,
859 #[cfg(test)]
860 force_pid_namespace_failure: FORCE_PID_NAMESPACE_FAILURE.with(std::cell::Cell::get),
861 }
862 }
863
864 pub(crate) fn socket_count(&self) -> usize {
866 self.tcp_ports.len() + 1
867 }
868}
869
870#[derive(Debug, Default)]
883pub(crate) struct SessionNetns {
884 state: std::sync::Mutex<SessionNetnsState>,
885 bind_observer: std::sync::Mutex<Option<crate::held_ports::BindObserver>>,
888 #[cfg(test)]
890 creations: std::sync::atomic::AtomicUsize,
891}
892
893#[derive(Debug, Default)]
894enum SessionNetnsState {
895 #[default]
896 NotYetCreated,
897 #[cfg(target_os = "linux")]
898 Established(linux::EstablishedNetns),
899 Ended,
900}
901
902#[cfg(target_os = "linux")]
909#[derive(Debug, Clone)]
910pub(crate) struct SessionNetnsJoin {
911 user_ns: std::sync::Arc<std::os::fd::OwnedFd>,
912 net_ns: std::sync::Arc<std::os::fd::OwnedFd>,
913 pid_ns: std::sync::Arc<std::os::fd::OwnedFd>,
914}
915
916#[cfg(target_os = "linux")]
917impl SessionNetnsJoin {
918 pub(crate) fn raw_fds(&self) -> (std::os::fd::RawFd, std::os::fd::RawFd) {
920 use std::os::fd::AsRawFd;
921 (self.user_ns.as_raw_fd(), self.net_ns.as_raw_fd())
922 }
923
924 pub(crate) fn pid_ns_fd(&self) -> std::os::fd::RawFd {
927 use std::os::fd::AsRawFd;
928 self.pid_ns.as_raw_fd()
929 }
930}
931
932impl SessionNetns {
933 pub(crate) fn new() -> Self {
935 Self::default()
936 }
937
938 #[cfg(target_os = "linux")]
955 pub(crate) fn establish(
956 &self,
957 tcp_ports: &[u16],
958 policy: crate::egress_proxy::EgressPolicy,
959 bind_observation: Option<&crate::held_ports::HeldPorts>,
960 ) -> Result<SessionNetnsJoin, String> {
961 let mut state = self
962 .state
963 .lock()
964 .unwrap_or_else(std::sync::PoisonError::into_inner);
965 if !matches!(&*state, SessionNetnsState::Ended) {
966 if let Some(held) = bind_observation {
967 self.bind_observer
968 .lock()
969 .unwrap_or_else(std::sync::PoisonError::into_inner)
970 .get_or_insert_with(|| crate::held_ports::BindObserver::new(held.clone()));
971 }
972 }
973 match &*state {
974 SessionNetnsState::Established(established) => return Ok(established.join()),
975 SessionNetnsState::Ended => {
976 return Err(
977 "sandbox: this session has ended, and its network namespace was released \
978 with it; no further command runs in it"
979 .to_string(),
980 )
981 }
982 SessionNetnsState::NotYetCreated => {}
983 }
984 let plan = CapsuleNetnsPlan::resolve(tcp_ports.to_vec());
985 let created = linux::create_session_netns(&plan).map_err(|error| {
988 session_netns_creation_error(
989 error,
990 crate::sealed::userns_grant(),
991 crate::sealed::inspect_profile_attachment(),
992 crate::sealed::inspect_profile_gap(),
993 )
994 })?;
995 let established = created.serve(policy).map_err(|detail| {
996 format!("sandbox: could not create this session's network namespace: {detail}")
997 })?;
998 let join = established.join();
999 *state = SessionNetnsState::Established(established);
1000 #[cfg(test)]
1001 self.creations
1002 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1003 Ok(join)
1004 }
1005
1006 pub(crate) fn end(&self) {
1015 let previous = std::mem::replace(
1016 &mut *self
1017 .state
1018 .lock()
1019 .unwrap_or_else(std::sync::PoisonError::into_inner),
1020 SessionNetnsState::Ended,
1021 );
1022 #[cfg(target_os = "linux")]
1023 if let SessionNetnsState::Established(established) = previous {
1024 established.release();
1025 }
1026 #[cfg(not(target_os = "linux"))]
1027 drop(previous);
1028 if let Some(observer) = self
1029 .bind_observer
1030 .lock()
1031 .unwrap_or_else(std::sync::PoisonError::into_inner)
1032 .take()
1033 {
1034 observer.stop();
1035 }
1036 }
1037
1038 #[cfg(target_os = "linux")]
1043 pub(crate) fn bridge_listener_dup(&self) -> Option<std::net::TcpListener> {
1044 match &*self
1045 .state
1046 .lock()
1047 .unwrap_or_else(std::sync::PoisonError::into_inner)
1048 {
1049 SessionNetnsState::Established(established) => established.bridge_listener_dup().ok(),
1050 _ => None,
1051 }
1052 }
1053
1054 #[cfg(target_os = "linux")]
1057 pub(crate) fn observe_bind_listener(&self, listener: std::os::fd::OwnedFd) {
1058 match self
1059 .bind_observer
1060 .lock()
1061 .unwrap_or_else(std::sync::PoisonError::into_inner)
1062 .as_ref()
1063 {
1064 Some(observer) => observer.add_listener(listener),
1065 None => drop(listener),
1066 }
1067 }
1068
1069 pub(crate) fn take_held_port_lines(&self) -> Vec<String> {
1072 self.bind_observer
1073 .lock()
1074 .unwrap_or_else(std::sync::PoisonError::into_inner)
1075 .as_ref()
1076 .map(crate::held_ports::BindObserver::take_lines)
1077 .unwrap_or_default()
1078 }
1079
1080 #[cfg(test)]
1083 pub(crate) fn record_held_port_for_test(
1084 &self,
1085 held: &crate::held_ports::HeldPorts,
1086 port: u16,
1087 ) -> bool {
1088 self.bind_observer
1089 .lock()
1090 .unwrap_or_else(std::sync::PoisonError::into_inner)
1091 .get_or_insert_with(|| crate::held_ports::BindObserver::new(held.clone()))
1092 .record_for_test(port)
1093 }
1094
1095 #[cfg(all(test, target_os = "linux"))]
1097 pub(crate) fn bind_observer_running(&self) -> bool {
1098 self.bind_observer
1099 .lock()
1100 .unwrap_or_else(std::sync::PoisonError::into_inner)
1101 .as_ref()
1102 .is_some_and(crate::held_ports::BindObserver::thread_running)
1103 }
1104
1105 #[cfg(test)]
1107 pub(crate) fn creations(&self) -> usize {
1108 self.creations.load(std::sync::atomic::Ordering::SeqCst)
1109 }
1110}
1111
1112impl Drop for SessionNetns {
1113 fn drop(&mut self) {
1116 self.end();
1117 }
1118}
1119
1120pub(crate) const STEP_RECORD_LEN: usize = 8;
1123
1124pub(crate) fn encode_step_record(code: u32, errno: i32) -> [u8; STEP_RECORD_LEN] {
1126 let mut record = [0u8; STEP_RECORD_LEN];
1127 record[..4].copy_from_slice(&code.to_ne_bytes());
1128 record[4..].copy_from_slice(&errno.to_ne_bytes());
1129 record
1130}
1131
1132pub(crate) fn decode_step_record(record: &[u8; STEP_RECORD_LEN]) -> (u32, i32) {
1134 (
1135 u32::from_ne_bytes([record[0], record[1], record[2], record[3]]),
1136 i32::from_ne_bytes([record[4], record[5], record[6], record[7]]),
1137 )
1138}
1139
1140#[cfg(target_os = "linux")]
1141pub(crate) use linux::{create_session_netns, join_session_netns, reap_child, send_fds};
1142
1143#[cfg(target_os = "linux")]
1144#[allow(unsafe_code)]
1145mod linux {
1146 use std::io;
1147 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
1148 use std::sync::Arc;
1149
1150 use super::{
1151 decode_step_record, encode_step_record, CapsuleNetnsPlan, CreationStep,
1152 NamespaceAttemptError, SessionNetnsJoin, BRIDGE_SOCKET_COUNT, MAX_NAMESPACE_SOCKETS,
1153 MAX_SESSION_NETNS_FDS, NAMESPACE_HANDLE_COUNT, STEP_RECORD_LEN,
1154 };
1155
1156 const RTM_NEWROUTE: u16 = 24;
1163 const NLM_F_REQUEST: u16 = 0x001;
1164 const NLM_F_ACK: u16 = 0x004;
1165 const NLM_F_EXCL: u16 = 0x200;
1166 const NLM_F_CREATE: u16 = 0x400;
1167 const NLMSG_ERROR: u16 = 2;
1168 const RT_TABLE_LOCAL: u8 = 255;
1171 const RTPROT_BOOT: u8 = 3;
1172 const RT_SCOPE_HOST: u8 = 254;
1173 const RTN_LOCAL: u8 = 2;
1174 const RTA_OIF: u16 = 4;
1175 const LOOPBACK_IFINDEX: u32 = 1;
1178
1179 const NLMSGHDR_LEN: usize = 16;
1180 const RTMSG_LEN: usize = 12;
1181 const RTATTR_OIF_LEN: usize = 8;
1182 const ROUTE_REQUEST_LEN: usize = NLMSGHDR_LEN + RTMSG_LEN + RTATTR_OIF_LEN;
1183
1184 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1190 pub(super) struct CreationFailure {
1191 step: CreationStep,
1192 errno: i32,
1193 }
1194
1195 impl CreationFailure {
1196 unsafe fn at(step: CreationStep) -> Self {
1201 let errno = unsafe { *libc::__errno_location() };
1204 Self { step, errno }
1205 }
1206
1207 fn encode(self) -> [u8; STEP_RECORD_LEN] {
1208 encode_step_record(self.step as u32, self.errno)
1209 }
1210
1211 fn decode(record: &[u8; STEP_RECORD_LEN]) -> Option<Self> {
1212 let (code, errno) = decode_step_record(record);
1213 let step = CreationStep::from_code(code)?;
1214 Some(Self { step, errno })
1215 }
1216 }
1217
1218 impl From<CreationFailure> for NamespaceAttemptError {
1219 fn from(failure: CreationFailure) -> Self {
1220 NamespaceAttemptError::Step {
1221 step: failure.step,
1222 errno: failure.errno,
1223 }
1224 }
1225 }
1226
1227 unsafe fn create_capsule_netns(
1253 plan: &CapsuleNetnsPlan,
1254 sock_fd: RawFd,
1255 lifeline_fd: RawFd,
1256 lifeline_write_fd: RawFd,
1257 ) -> Result<(), CreationFailure> {
1258 unsafe {
1260 #[cfg(test)]
1261 let forced = plan.force_pid_namespace_failure;
1262 #[cfg(not(test))]
1263 let forced = false;
1264 let pid_namespace_refused = if forced {
1265 Some(libc::EPERM)
1266 } else if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNET | libc::CLONE_NEWPID)
1267 == 0
1268 {
1269 None
1270 } else {
1271 Some(*libc::__errno_location())
1272 };
1273 if let Some(errno) = pid_namespace_refused {
1274 if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNET) != 0 {
1275 return Err(CreationFailure::at(CreationStep::Unshare));
1276 }
1277 return Err(CreationFailure {
1278 step: CreationStep::PidNamespace,
1279 errno,
1280 });
1281 }
1282 if write_identity_maps(plan.uid, plan.gid).is_err() {
1283 return Err(CreationFailure::at(CreationStep::IdentityMaps));
1284 }
1285 libc::close(lifeline_write_fd);
1291 let holder = libc::fork();
1292 if holder < 0 {
1293 return Err(CreationFailure::at(CreationStep::Holder));
1294 }
1295 if holder == 0 {
1296 run_holder(lifeline_fd);
1297 }
1298 if bring_loopback_up().is_err() {
1299 return Err(CreationFailure::at(CreationStep::Loopback));
1300 }
1301 if add_local_default_route().is_err() {
1302 return Err(CreationFailure::at(CreationStep::LocalRoute));
1303 }
1304
1305 let mut fds = [-1 as RawFd; MAX_SESSION_NETNS_FDS];
1309 let mut count = 0usize;
1310
1311 for port in &plan.tcp_ports {
1312 let fd = bind_tcp_listener(*port);
1313 if fd < 0 {
1314 let failure = CreationFailure::at(CreationStep::TcpListener);
1315 close_all(&fds[..count]);
1316 return Err(failure);
1317 }
1318 fds[count] = fd;
1319 count += 1;
1320 }
1321
1322 let resolver = bind_dns_socket();
1323 if resolver < 0 {
1324 let failure = CreationFailure::at(CreationStep::Resolver);
1325 close_all(&fds[..count]);
1326 return Err(failure);
1327 }
1328 fds[count] = resolver;
1329 count += 1;
1330
1331 let bridge = bind_bridge_listener();
1336 if bridge < 0 {
1337 let failure = CreationFailure::at(CreationStep::BridgeListener);
1338 close_all(&fds[..count]);
1339 return Err(failure);
1340 }
1341 fds[count] = bridge;
1342 count += 1;
1343
1344 for namespace in [
1348 c"/proc/self/ns/user",
1349 c"/proc/self/ns/net",
1350 c"/proc/self/ns/pid_for_children",
1351 ] {
1352 let fd = libc::open(namespace.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC);
1353 if fd < 0 {
1354 let failure = CreationFailure::at(CreationStep::NamespaceHandles);
1355 close_all(&fds[..count]);
1356 return Err(failure);
1357 }
1358 fds[count] = fd;
1359 count += 1;
1360 }
1361
1362 let sent = send_fds(sock_fd, &fds[..count]);
1363 let failure = CreationFailure::at(CreationStep::HandOff);
1364 close_all(&fds[..count]);
1365 sent.map_err(|()| failure)
1366 }
1367 }
1368
1369 unsafe fn run_holder(lifeline_fd: RawFd) -> ! {
1386 unsafe {
1388 let mut ignore: libc::sigaction = std::mem::zeroed();
1389 ignore.sa_sigaction = libc::SIG_IGN;
1390 libc::sigaction(libc::SIGCHLD, &ignore, std::ptr::null_mut());
1391 libc::prctl(libc::PR_SET_NAME, c"mur-pidns-init".as_ptr());
1392 libc::chdir(c"/".as_ptr());
1394 let lifeline = lifeline_fd as libc::c_uint;
1395 if lifeline > 0 {
1396 crate::cgroup::close_range(0, lifeline - 1);
1397 }
1398 crate::cgroup::close_range(lifeline + 1, libc::c_uint::MAX);
1399 let mut byte = 0u8;
1400 loop {
1401 let read = libc::read(lifeline_fd, (&raw mut byte).cast(), 1);
1402 if read < 0 && *libc::__errno_location() == libc::EINTR {
1403 continue;
1404 }
1405 if read > 0 {
1406 continue;
1407 }
1408 break;
1409 }
1410 libc::_exit(0)
1411 }
1412 }
1413
1414 pub(crate) unsafe fn join_session_netns(user_fd: RawFd, net_fd: RawFd) -> io::Result<()> {
1426 unsafe {
1429 if libc::setns(user_fd, libc::CLONE_NEWUSER) != 0 {
1430 return Err(io::Error::last_os_error());
1431 }
1432 if libc::setns(net_fd, libc::CLONE_NEWNET) != 0 {
1433 return Err(io::Error::last_os_error());
1434 }
1435 }
1436 Ok(())
1437 }
1438
1439 pub(crate) struct CreatedNetns {
1444 sockets: Vec<OwnedFd>,
1445 bridge_listener: OwnedFd,
1448 user_ns: OwnedFd,
1449 net_ns: OwnedFd,
1450 pid_ns: OwnedFd,
1451 lifeline: OwnedFd,
1452 }
1453
1454 impl CreatedNetns {
1455 pub(crate) fn namespace_handles(&self) -> (RawFd, RawFd) {
1457 (self.user_ns.as_raw_fd(), self.net_ns.as_raw_fd())
1458 }
1459
1460 pub(crate) fn pid_namespace_handle(&self) -> RawFd {
1462 self.pid_ns.as_raw_fd()
1463 }
1464
1465 pub(super) fn serve(
1470 self,
1471 policy: crate::egress_proxy::EgressPolicy,
1472 ) -> Result<EstablishedNetns, String> {
1473 let proxy = crate::egress_proxy::start_egress_proxy(self.sockets, policy)
1474 .map_err(|error| format!("starting the session's egress proxy failed: {error}"))?;
1475 Ok(EstablishedNetns {
1476 user_ns: Arc::new(self.user_ns),
1477 net_ns: Arc::new(self.net_ns),
1478 pid_ns: Arc::new(self.pid_ns),
1479 bridge_listener: Arc::new(self.bridge_listener),
1480 lifeline: self.lifeline,
1481 proxy,
1482 })
1483 }
1484 }
1485
1486 #[derive(Debug)]
1492 pub(super) struct EstablishedNetns {
1493 user_ns: Arc<OwnedFd>,
1494 net_ns: Arc<OwnedFd>,
1495 pid_ns: Arc<OwnedFd>,
1496 bridge_listener: Arc<OwnedFd>,
1497 lifeline: OwnedFd,
1498 proxy: crate::egress_proxy::EgressProxyHandle,
1499 }
1500
1501 impl EstablishedNetns {
1502 pub(super) fn join(&self) -> SessionNetnsJoin {
1503 SessionNetnsJoin {
1504 user_ns: Arc::clone(&self.user_ns),
1505 net_ns: Arc::clone(&self.net_ns),
1506 pid_ns: Arc::clone(&self.pid_ns),
1507 }
1508 }
1509
1510 pub(super) fn bridge_listener_dup(&self) -> std::io::Result<std::net::TcpListener> {
1515 let dup = self.bridge_listener.try_clone()?;
1516 Ok(std::net::TcpListener::from(dup))
1517 }
1518
1519 pub(super) fn release(self) {
1522 self.proxy.shutdown();
1523 drop(self.lifeline);
1524 }
1525 }
1526
1527 pub(crate) fn create_session_netns(
1537 plan: &CapsuleNetnsPlan,
1538 ) -> Result<CreatedNetns, NamespaceAttemptError> {
1539 if plan.socket_count() > MAX_NAMESPACE_SOCKETS {
1542 return Err(NamespaceAttemptError::Creator(format!(
1543 "{} namespace sockets requested, more than the {MAX_NAMESPACE_SOCKETS} one \
1544 hand-off carries",
1545 plan.socket_count()
1546 )));
1547 }
1548
1549 let mut pair = [0i32; 2];
1550 let rc = unsafe {
1552 libc::socketpair(
1553 libc::AF_UNIX,
1554 libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
1555 0,
1556 pair.as_mut_ptr(),
1557 )
1558 };
1559 if rc != 0 {
1560 return Err(NamespaceAttemptError::Creator(format!(
1561 "socketpair() for the namespace hand-off failed: {}",
1562 io::Error::last_os_error()
1563 )));
1564 }
1565 let (parent_end, creator_end) =
1568 unsafe { (OwnedFd::from_raw_fd(pair[0]), OwnedFd::from_raw_fd(pair[1])) };
1569 let creator_fd = creator_end.as_raw_fd();
1570
1571 let mut lifeline = [0i32; 2];
1574 if unsafe { libc::pipe2(lifeline.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
1576 return Err(NamespaceAttemptError::Creator(format!(
1577 "pipe2() for the PID namespace holder's lifeline failed: {}",
1578 io::Error::last_os_error()
1579 )));
1580 }
1581 let (lifeline_read, lifeline_write) = unsafe {
1584 (
1585 OwnedFd::from_raw_fd(lifeline[0]),
1586 OwnedFd::from_raw_fd(lifeline[1]),
1587 )
1588 };
1589 let lifeline_fd = lifeline_read.as_raw_fd();
1590 let lifeline_write_fd = lifeline_write.as_raw_fd();
1591
1592 let pid = unsafe { libc::fork() };
1599 if pid < 0 {
1600 return Err(NamespaceAttemptError::Creator(format!(
1601 "fork() for the namespace creator failed: {}",
1602 io::Error::last_os_error()
1603 )));
1604 }
1605 if pid == 0 {
1606 unsafe {
1609 match create_capsule_netns(plan, creator_fd, lifeline_fd, lifeline_write_fd) {
1610 Ok(()) => libc::_exit(0),
1611 Err(failure) => {
1612 let record = failure.encode();
1613 libc::send(creator_fd, record.as_ptr().cast(), record.len(), 0);
1614 libc::_exit(1);
1615 }
1616 }
1617 }
1618 }
1619 drop(creator_end);
1620 drop(lifeline_read);
1621
1622 let status = reap_child(pid).map_err(|error| {
1626 NamespaceAttemptError::Creator(format!(
1627 "waiting for the namespace creator failed: {error}"
1628 ))
1629 })?;
1630 match receive_session_netns(parent_end.as_raw_fd(), plan.socket_count())
1631 .map_err(NamespaceAttemptError::Creator)?
1632 {
1633 Received::Handles(mut fds) => {
1634 let pid_ns = fds.pop().expect("the count was checked on receipt");
1635 let net_ns = fds.pop().expect("the count was checked on receipt");
1636 let user_ns = fds.pop().expect("the count was checked on receipt");
1637 let bridge_listener = fds.pop().expect("the count was checked on receipt");
1640 Ok(CreatedNetns {
1641 sockets: fds,
1642 bridge_listener,
1643 user_ns,
1644 net_ns,
1645 pid_ns,
1646 lifeline: lifeline_write,
1647 })
1648 }
1649 Received::Failure(failure) => Err(failure.into()),
1650 Received::Nothing => Err(NamespaceAttemptError::Creator(format!(
1651 "the namespace creator exited without handing anything over ({status})"
1652 ))),
1653 }
1654 }
1655
1656 pub(crate) fn reap_child(pid: libc::pid_t) -> io::Result<String> {
1658 let mut status: libc::c_int = 0;
1659 loop {
1660 let waited = unsafe { libc::waitpid(pid, &mut status, 0) };
1662 if waited == pid {
1663 break;
1664 }
1665 let error = io::Error::last_os_error();
1666 if error.kind() != io::ErrorKind::Interrupted {
1667 return Err(error);
1668 }
1669 }
1670 Ok(if libc::WIFEXITED(status) {
1671 format!("exit status {}", libc::WEXITSTATUS(status))
1672 } else if libc::WIFSIGNALED(status) {
1673 format!("killed by signal {}", libc::WTERMSIG(status))
1674 } else {
1675 format!("wait status {status}")
1676 })
1677 }
1678
1679 #[cfg(test)]
1682 pub(super) fn decode_failure_record(step: u32, errno: i32) -> Option<NamespaceAttemptError> {
1683 let record = encode_step_record(step, errno);
1684 CreationFailure::decode(&record).map(|failure| {
1685 assert_eq!(failure.encode(), record);
1686 failure.into()
1687 })
1688 }
1689
1690 enum Received {
1692 Handles(Vec<OwnedFd>),
1694 Failure(CreationFailure),
1695 Nothing,
1696 }
1697
1698 unsafe fn write_identity_maps(uid: u32, gid: u32) -> Result<(), ()> {
1711 unsafe {
1713 let previous = crate::sealed::make_dumpable_for_map_writes();
1714 let _ = crate::sealed::write_decimal_map(c"/proc/self/setgroups", None, 0);
1715 let mapped = crate::sealed::write_decimal_map(c"/proc/self/uid_map", Some(uid), uid)
1716 .is_ok()
1717 && crate::sealed::write_decimal_map(c"/proc/self/gid_map", Some(gid), gid).is_ok();
1718 crate::sealed::restore_dumpable(previous);
1719 if mapped {
1720 Ok(())
1721 } else {
1722 Err(())
1723 }
1724 }
1725 }
1726
1727 unsafe fn bring_loopback_up() -> Result<(), ()> {
1736 unsafe {
1739 let sock = libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0);
1740 if sock < 0 {
1741 return Err(());
1742 }
1743 let mut ifr: libc::ifreq = std::mem::zeroed();
1744 ifr.ifr_name[0] = b'l' as libc::c_char;
1745 ifr.ifr_name[1] = b'o' as libc::c_char;
1746 if libc::ioctl(sock, libc::SIOCGIFFLAGS, &mut ifr) < 0 {
1747 libc::close(sock);
1748 return Err(());
1749 }
1750 ifr.ifr_ifru.ifru_flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short;
1751 let rc = libc::ioctl(sock, libc::SIOCSIFFLAGS, &ifr);
1752 libc::close(sock);
1753 if rc < 0 {
1754 return Err(());
1755 }
1756 Ok(())
1757 }
1758 }
1759
1760 unsafe fn add_local_default_route() -> Result<(), ()> {
1777 unsafe {
1780 let mut request = [0u8; ROUTE_REQUEST_LEN];
1781 request[0..4].copy_from_slice(&(ROUTE_REQUEST_LEN as u32).to_ne_bytes());
1782 request[4..6].copy_from_slice(&RTM_NEWROUTE.to_ne_bytes());
1783 request[6..8].copy_from_slice(
1784 &(NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL).to_ne_bytes(),
1785 );
1786 request[8..12].copy_from_slice(&1u32.to_ne_bytes()); request[12..16].copy_from_slice(&0u32.to_ne_bytes()); let rtm = NLMSGHDR_LEN;
1790 request[rtm] = libc::AF_INET as u8;
1791 request[rtm + 1] = 0; request[rtm + 2] = 0; request[rtm + 3] = 0; request[rtm + 4] = RT_TABLE_LOCAL;
1795 request[rtm + 5] = RTPROT_BOOT;
1796 request[rtm + 6] = RT_SCOPE_HOST;
1797 request[rtm + 7] = RTN_LOCAL;
1798 request[rtm + 8..rtm + 12].copy_from_slice(&0u32.to_ne_bytes()); let attr = rtm + RTMSG_LEN;
1801 request[attr..attr + 2].copy_from_slice(&(RTATTR_OIF_LEN as u16).to_ne_bytes());
1802 request[attr + 2..attr + 4].copy_from_slice(&RTA_OIF.to_ne_bytes());
1803 request[attr + 4..attr + 8].copy_from_slice(&LOOPBACK_IFINDEX.to_ne_bytes());
1804
1805 let sock = libc::socket(
1806 libc::AF_NETLINK,
1807 libc::SOCK_RAW | libc::SOCK_CLOEXEC,
1808 libc::NETLINK_ROUTE,
1809 );
1810 if sock < 0 {
1811 return Err(());
1812 }
1813 let mut address: libc::sockaddr_nl = std::mem::zeroed();
1814 address.nl_family = libc::AF_NETLINK as u16;
1815 let sent = libc::sendto(
1816 sock,
1817 request.as_ptr().cast(),
1818 ROUTE_REQUEST_LEN,
1819 0,
1820 std::ptr::addr_of!(address).cast(),
1821 std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
1822 );
1823 if sent < 0 {
1824 libc::close(sock);
1825 return Err(());
1826 }
1827
1828 let mut response = [0u8; 256];
1832 let received = libc::recv(sock, response.as_mut_ptr().cast(), response.len(), 0);
1833 libc::close(sock);
1834 if received < (NLMSGHDR_LEN + 4) as isize {
1835 return Err(());
1836 }
1837 if u16::from_ne_bytes([response[4], response[5]]) == NLMSG_ERROR {
1838 let code = i32::from_ne_bytes([
1839 response[NLMSGHDR_LEN],
1840 response[NLMSGHDR_LEN + 1],
1841 response[NLMSGHDR_LEN + 2],
1842 response[NLMSGHDR_LEN + 3],
1843 ]);
1844 if code != 0 {
1846 return Err(());
1847 }
1848 }
1849 Ok(())
1850 }
1851 }
1852
1853 unsafe fn bind_tcp_listener(port: u16) -> RawFd {
1862 unsafe {
1864 let fd = libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_CLOEXEC, 0);
1865 if fd < 0 {
1866 return -1;
1867 }
1868 let mut address: libc::sockaddr_in = std::mem::zeroed();
1869 address.sin_family = libc::AF_INET as libc::sa_family_t;
1870 address.sin_port = port.to_be();
1871 address.sin_addr.s_addr = libc::INADDR_ANY.to_be();
1872 if libc::bind(
1873 fd,
1874 std::ptr::addr_of!(address).cast(),
1875 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
1876 ) != 0
1877 || libc::listen(fd, 128) != 0
1878 {
1879 libc::close(fd);
1880 return -1;
1881 }
1882 fd
1883 }
1884 }
1885
1886 unsafe fn bind_bridge_listener() -> RawFd {
1895 unsafe {
1897 let fd = libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_CLOEXEC, 0);
1898 if fd < 0 {
1899 return -1;
1900 }
1901 let mut address: libc::sockaddr_in = std::mem::zeroed();
1902 address.sin_family = libc::AF_INET as libc::sa_family_t;
1903 address.sin_port = 0u16.to_be();
1904 address.sin_addr.s_addr = u32::from_be_bytes([127, 0, 0, 1]).to_be();
1905 if libc::bind(
1906 fd,
1907 std::ptr::addr_of!(address).cast(),
1908 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
1909 ) != 0
1910 || libc::listen(fd, 128) != 0
1911 {
1912 libc::close(fd);
1913 return -1;
1914 }
1915 fd
1916 }
1917 }
1918
1919 unsafe fn bind_dns_socket() -> RawFd {
1932 unsafe {
1934 let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0);
1935 if fd < 0 {
1936 return -1;
1937 }
1938 let enable: libc::c_int = 1;
1939 if libc::setsockopt(
1940 fd,
1941 libc::IPPROTO_IP,
1942 libc::IP_PKTINFO,
1943 std::ptr::addr_of!(enable).cast(),
1944 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
1945 ) != 0
1946 {
1947 libc::close(fd);
1948 return -1;
1949 }
1950 let mut address: libc::sockaddr_in = std::mem::zeroed();
1951 address.sin_family = libc::AF_INET as libc::sa_family_t;
1952 address.sin_port = super::DNS_PORT_BE;
1953 address.sin_addr.s_addr = libc::INADDR_ANY.to_be();
1954 if libc::bind(
1955 fd,
1956 std::ptr::addr_of!(address).cast(),
1957 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
1958 ) != 0
1959 {
1960 libc::close(fd);
1961 return -1;
1962 }
1963 fd
1964 }
1965 }
1966
1967 unsafe fn close_all(fds: &[RawFd]) {
1970 for fd in fds {
1971 if *fd >= 0 {
1972 unsafe { libc::close(*fd) };
1974 }
1975 }
1976 }
1977
1978 const CMSG_BUFFER_LEN: usize = 128;
1980
1981 const _: () = assert!(
1987 unsafe {
1991 libc::CMSG_SPACE((MAX_SESSION_NETNS_FDS * std::mem::size_of::<RawFd>()) as u32) as usize
1992 } <= CMSG_BUFFER_LEN,
1993 "CMSG_BUFFER_LEN is too small to hold MAX_SESSION_NETNS_FDS descriptors plus the \
1994 cmsghdr; raise it to match"
1995 );
1996
1997 #[repr(C, align(8))]
2003 struct CmsgBuffer([u8; CMSG_BUFFER_LEN]);
2004
2005 pub(crate) unsafe fn send_fds(sock_fd: RawFd, fds: &[RawFd]) -> Result<(), ()> {
2016 unsafe {
2019 let mut payload = [0u8; 1];
2020 let mut iov = libc::iovec {
2021 iov_base: payload.as_mut_ptr().cast(),
2022 iov_len: payload.len(),
2023 };
2024 let mut control = CmsgBuffer([0u8; CMSG_BUFFER_LEN]);
2025 let bytes = std::mem::size_of_val(fds) as u32;
2026
2027 let mut msg: libc::msghdr = std::mem::zeroed();
2028 msg.msg_iov = &mut iov;
2029 msg.msg_iovlen = 1;
2030 msg.msg_control = control.0.as_mut_ptr().cast();
2031 msg.msg_controllen = libc::CMSG_SPACE(bytes) as _;
2032
2033 let cmsg = libc::CMSG_FIRSTHDR(&msg);
2034 if cmsg.is_null() {
2035 return Err(());
2036 }
2037 (*cmsg).cmsg_level = libc::SOL_SOCKET;
2038 (*cmsg).cmsg_type = libc::SCM_RIGHTS;
2039 (*cmsg).cmsg_len = libc::CMSG_LEN(bytes) as _;
2040 std::ptr::copy_nonoverlapping(
2041 fds.as_ptr().cast::<u8>(),
2042 libc::CMSG_DATA(cmsg),
2043 bytes as usize,
2044 );
2045
2046 if libc::sendmsg(sock_fd, &msg, 0) < 0 {
2047 return Err(());
2048 }
2049 Ok(())
2050 }
2051 }
2052
2053 fn receive_session_netns(sock_fd: RawFd, expected_sockets: usize) -> Result<Received, String> {
2062 unsafe {
2065 let mut payload = [0u8; STEP_RECORD_LEN];
2066 let mut iov = libc::iovec {
2067 iov_base: payload.as_mut_ptr().cast(),
2068 iov_len: payload.len(),
2069 };
2070 let mut control = CmsgBuffer([0u8; CMSG_BUFFER_LEN]);
2071 let mut msg: libc::msghdr = std::mem::zeroed();
2072 msg.msg_iov = &mut iov;
2073 msg.msg_iovlen = 1;
2074 msg.msg_control = control.0.as_mut_ptr().cast();
2075 msg.msg_controllen = control.0.len() as _;
2076
2077 let received = libc::recvmsg(
2078 sock_fd,
2079 &mut msg,
2080 libc::MSG_DONTWAIT | libc::MSG_CMSG_CLOEXEC,
2081 );
2082 if received < 0 {
2083 let error = io::Error::last_os_error();
2084 if error.kind() == io::ErrorKind::WouldBlock {
2085 return Ok(Received::Nothing);
2086 }
2087 return Err(format!("receiving the namespace hand-off failed: {error}"));
2088 }
2089
2090 let cmsg = libc::CMSG_FIRSTHDR(&msg);
2091 if cmsg.is_null() {
2092 if received as usize == STEP_RECORD_LEN {
2093 if let Some(failure) = CreationFailure::decode(&payload) {
2094 return Ok(Received::Failure(failure));
2095 }
2096 }
2097 return Ok(Received::Nothing);
2098 }
2099 if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
2100 return Err(
2101 "the namespace hand-off did not arrive as an SCM_RIGHTS message".to_string(),
2102 );
2103 }
2104 let bytes = (*cmsg).cmsg_len - libc::CMSG_LEN(0) as usize;
2105 let count = bytes / std::mem::size_of::<RawFd>();
2106 let data = libc::CMSG_DATA(cmsg).cast::<RawFd>();
2107 let mut fds = Vec::with_capacity(count);
2110 for index in 0..count {
2111 fds.push(OwnedFd::from_raw_fd(std::ptr::read_unaligned(
2112 data.add(index),
2113 )));
2114 }
2115 if msg.msg_flags & libc::MSG_CTRUNC != 0 {
2116 return Err("the namespace hand-off was truncated".to_string());
2117 }
2118 if count != expected_sockets + BRIDGE_SOCKET_COUNT + NAMESPACE_HANDLE_COUNT {
2119 return Err(format!(
2120 "the namespace creator handed over {count} descriptors; {} were expected \
2121 ({expected_sockets} egress sockets, {BRIDGE_SOCKET_COUNT} bridge listener \
2122 and {NAMESPACE_HANDLE_COUNT} namespace handles)",
2123 expected_sockets + BRIDGE_SOCKET_COUNT + NAMESPACE_HANDLE_COUNT
2124 ));
2125 }
2126 Ok(Received::Handles(fds))
2127 }
2128 }
2129}
2130
2131#[cfg(target_os = "linux")]
2134const DNS_PORT_BE: u16 = crate::egress_proxy::EGRESS_DNS_PORT.to_be();
2135
2136#[cfg(test)]
2137mod tests {
2138 use super::*;
2139 use crate::sealed::{
2140 profile_gap_remedy, ProfileAttachment, ProfileFile, ProfileGap, ProfiledPath, UsernsGrant,
2141 };
2142
2143 fn attachments() -> [ProfileAttachment; 4] {
2144 [
2145 ProfileAttachment::Sealed {
2146 profile: "mur-sealed".to_string(),
2147 },
2148 ProfileAttachment::Other {
2149 profile: "docker-default".to_string(),
2150 },
2151 ProfileAttachment::Unconfined,
2152 ProfileAttachment::NotReported,
2153 ]
2154 }
2155
2156 fn step(step: CreationStep, errno: i32) -> Result<(), NamespaceAttemptError> {
2157 Err(NamespaceAttemptError::Step { step, errno })
2158 }
2159
2160 fn failure(
2161 attempt: Result<(), NamespaceAttemptError>,
2162 grant: UsernsGrant,
2163 attachment: ProfileAttachment,
2164 ) -> EgressNamespaceFailure {
2165 egress_namespace_failure(true, attempt, grant, attachment, ProfileGap::Undetermined)
2166 .expect("a failed attempt is a failure")
2167 }
2168
2169 #[test]
2174 fn a_created_namespace_is_never_refused_whatever_the_readings() {
2175 for grant in UsernsGrant::ALL {
2176 for attachment in attachments() {
2177 assert_eq!(
2178 egress_namespace_failure(
2179 true,
2180 Ok(()),
2181 *grant,
2182 attachment.clone(),
2183 ProfileGap::Undetermined
2184 ),
2185 None,
2186 "{grant:?} / {attachment:?}"
2187 );
2188 }
2189 }
2190 }
2191
2192 #[test]
2195 fn a_failed_creation_is_refused_whatever_the_readings() {
2196 let attempts = [
2197 (
2198 step(CreationStep::Unshare, libc::EPERM),
2199 EgressNamespaceBlocker::CapabilityGrantMissing,
2200 ),
2201 (
2202 step(CreationStep::Unshare, libc::ENOSPC),
2203 EgressNamespaceBlocker::KernelSupportMissing,
2204 ),
2205 (
2206 Err(NamespaceAttemptError::Creator("fork() failed".to_string())),
2207 EgressNamespaceBlocker::CreatorUnavailable,
2208 ),
2209 ];
2210 for (attempt, blocker) in attempts {
2211 for grant in UsernsGrant::ALL {
2212 for attachment in attachments() {
2213 let failure = egress_namespace_failure(
2214 true,
2215 attempt.clone(),
2216 *grant,
2217 attachment.clone(),
2218 ProfileGap::Undetermined,
2219 )
2220 .unwrap_or_else(|| panic!("{attempt:?} under {grant:?} / {attachment:?}"));
2221 assert_eq!(failure.blocker, blocker, "{grant:?} / {attachment:?}");
2222 }
2223 }
2224 }
2225 }
2226
2227 #[test]
2228 fn only_an_unshare_the_kernel_cannot_honour_is_missing_kernel_support() {
2229 for errno in [libc::ENOSPC, libc::EINVAL, libc::ENOSYS, libc::EUSERS] {
2230 assert_eq!(
2231 failure(
2232 step(CreationStep::Unshare, errno),
2233 UsernsGrant::Withheld,
2234 ProfileAttachment::Unconfined
2235 )
2236 .blocker,
2237 EgressNamespaceBlocker::KernelSupportMissing,
2238 "errno {errno}"
2239 );
2240 }
2241 for (failed, errno) in [
2242 (CreationStep::Unshare, libc::EPERM),
2243 (CreationStep::Unshare, libc::EACCES),
2244 (CreationStep::IdentityMaps, libc::EPERM),
2245 (CreationStep::Loopback, libc::EPERM),
2246 (CreationStep::LocalRoute, libc::EINVAL),
2247 (CreationStep::Resolver, libc::ENOSPC),
2248 ] {
2249 assert_eq!(
2250 failure(
2251 step(failed, errno),
2252 UsernsGrant::Withheld,
2253 ProfileAttachment::Unconfined
2254 )
2255 .blocker,
2256 EgressNamespaceBlocker::CapabilityGrantMissing,
2257 "{failed:?} errno {errno}"
2258 );
2259 }
2260 }
2261
2262 #[test]
2263 fn a_creator_that_could_not_run_is_unavailable_and_keeps_its_detail() {
2264 let detail = "fork() for the namespace creator failed: Resource temporarily unavailable \
2265 (os error 11)";
2266 let failure = failure(
2267 Err(NamespaceAttemptError::Creator(detail.to_string())),
2268 UsernsGrant::Withheld,
2269 ProfileAttachment::Unconfined,
2270 );
2271 assert_eq!(failure.blocker, EgressNamespaceBlocker::CreatorUnavailable);
2272 assert_eq!(failure.errno, None);
2273 let reason = failure.reason();
2274 assert!(reason.starts_with(detail), "{reason}");
2275 assert!(!reason.contains("apparmor_parser"), "{reason}");
2276 assert!(!reason.contains("CONFIG_USER_NS"), "{reason}");
2277 assert!(!reason.contains("user.max_user_namespaces"), "{reason}");
2278 }
2279
2280 #[test]
2281 fn non_linux_is_never_refused_for_a_mechanism_it_never_had() {
2282 for attempt in [
2283 Ok(()),
2284 step(CreationStep::Unshare, libc::EPERM),
2285 Err(NamespaceAttemptError::Creator("fork() failed".to_string())),
2286 ] {
2287 assert_eq!(
2288 egress_namespace_failure(
2289 false,
2290 attempt,
2291 UsernsGrant::Withheld,
2292 ProfileAttachment::NotReported,
2293 ProfileGap::Undetermined
2294 ),
2295 None
2296 );
2297 }
2298 #[cfg(not(target_os = "linux"))]
2299 assert_eq!(detect_egress_namespace_failure(), None);
2300 }
2301
2302 #[test]
2307 fn a_worker_only_capsule_needs_the_namespace_and_the_scope() {
2308 let policy = |artifacts: &str| {
2309 let manifest = murmur_artifact::RuntimeManifest::from_yaml_str(&format!(
2310 "name: cap\nversion: 0.1.0\n{artifacts}"
2311 ))
2312 .expect("manifest fixture must parse");
2313 crate::types::capability_policy_from_runtime_manifest(&manifest)
2314 };
2315 let failure = failure(
2316 step(CreationStep::Unshare, libc::EPERM),
2317 UsernsGrant::Withheld,
2318 ProfileAttachment::Unconfined,
2319 );
2320 for (case, artifacts) in [
2321 (
2322 "tool",
2323 "artifacts:\n - name: local/t\n version: 0.1.0\n runtime: tool\n",
2324 ),
2325 (
2326 "driver",
2327 "artifacts:\n - name: local/d\n version: 0.1.0\n runtime: driver\n",
2328 ),
2329 (
2330 "hook",
2331 "artifacts:\n - name: local/h\n version: 0.1.0\n runtime: hook\n",
2332 ),
2333 (
2334 "pulled tool",
2335 "capabilities:\n install:\n tool:\n - local/t-*\n",
2336 ),
2337 ] {
2338 let policy = policy(artifacts);
2339 assert!(policy.guest_workers, "{case}");
2340 assert!(
2341 crate::runtime::has_subprocess_route_at_staging(&policy),
2342 "{case}"
2343 );
2344 assert!(crate::cgroup::requires_process_bounding(&policy), "{case}");
2345 assert!(
2346 matches!(
2347 check_egress_namespace(
2348 crate::runtime::has_subprocess_route_at_staging(&policy),
2349 Some(&failure)
2350 ),
2351 Err(crate::errors::RuntimeError::EgressNamespaceUnavailable { .. })
2352 ),
2353 "{case}"
2354 );
2355 }
2356 for (case, artifacts) in [
2357 ("nothing", ""),
2358 (
2359 "skill",
2360 "artifacts:\n - name: local/s\n version: 0.1.0\n runtime: skill\n",
2361 ),
2362 ] {
2363 let policy = policy(artifacts);
2364 assert!(!policy.guest_workers, "{case}");
2365 assert!(
2366 !crate::runtime::has_subprocess_route_at_staging(&policy),
2367 "{case}"
2368 );
2369 assert!(!crate::cgroup::requires_process_bounding(&policy), "{case}");
2370 }
2371 }
2372
2373 #[test]
2374 fn the_check_refuses_exactly_a_capsule_that_can_spawn_on_a_failing_host() {
2375 let failure = failure(
2376 step(CreationStep::Unshare, libc::EPERM),
2377 UsernsGrant::Withheld,
2378 ProfileAttachment::Unconfined,
2379 );
2380 match check_egress_namespace(true, Some(&failure)) {
2381 Err(crate::errors::RuntimeError::EgressNamespaceUnavailable {
2382 blocker,
2383 reason,
2384 ..
2385 }) => {
2386 assert_eq!(blocker, failure.blocker);
2387 assert_eq!(reason, failure.reason());
2388 }
2389 other => panic!("expected the E-CAP-005 refusal, got {other:?}"),
2390 }
2391 assert!(check_egress_namespace(false, Some(&failure)).is_ok());
2392 assert!(check_egress_namespace(true, None).is_ok());
2393 assert!(check_egress_namespace(false, None).is_ok());
2394 }
2395
2396 #[test]
2399 fn an_unconfined_binary_on_a_restricted_host_is_told_the_profile_before_the_sysctl() {
2400 let reason = failure(
2401 step(CreationStep::Unshare, libc::EPERM),
2402 UsernsGrant::Withheld,
2403 ProfileAttachment::Unconfined,
2404 )
2405 .reason();
2406 assert!(
2407 reason.starts_with(
2408 "unshare(CLONE_NEWUSER | CLONE_NEWNET) failed with EPERM (Operation not \
2409 permitted)."
2410 ),
2411 "{reason}"
2412 );
2413 assert!(
2414 reason.contains("kernel.apparmor_restrict_unprivileged_userns=1"),
2415 "{reason}"
2416 );
2417 let profile = reason
2418 .find("apparmor_parser -r")
2419 .expect("the profile install command must be named");
2420 let sysctl = reason
2421 .find("kernel.apparmor_restrict_unprivileged_userns=0")
2422 .expect("the fallback must still be named for hosts that cannot load a profile");
2423 assert!(
2424 profile < sysctl,
2425 "the profile install must come first: {reason}"
2426 );
2427 assert!(reason.contains("LAST RESORT"), "{reason}");
2428 assert!(reason.contains("every program on the machine"), "{reason}");
2429 assert!(
2430 reason.contains("scripts/install-dev-apparmor.sh"),
2431 "{reason}"
2432 );
2433 assert!(
2434 reason.ends_with(
2435 "Host readings: kernel.apparmor_restrict_unprivileged_userns=1; AppArmor profile \
2436 confining mur: unconfined."
2437 ),
2438 "{reason}"
2439 );
2440 }
2441
2442 #[test]
2443 fn a_binary_another_profile_confines_is_given_the_container_remedy_not_the_sysctl() {
2444 let reason = failure(
2445 step(CreationStep::Unshare, libc::EPERM),
2446 UsernsGrant::Withheld,
2447 ProfileAttachment::Other {
2448 profile: "docker-default".to_string(),
2449 },
2450 )
2451 .reason();
2452 assert!(reason.contains("`docker-default`"), "{reason}");
2453 assert!(!reason.contains("--cap-add"), "{reason}");
2454 assert!(reason.contains("seccomp profile"), "{reason}");
2455 assert!(
2456 reason.contains("unshare(CLONE_NEWUSER) and setns"),
2457 "{reason}"
2458 );
2459 assert!(reason.contains("seccompProfile: Localhost"), "{reason}");
2460 assert!(
2461 !reason.contains("kernel.apparmor_restrict_unprivileged_userns=0"),
2462 "{reason}"
2463 );
2464 assert!(!reason.contains("apparmor_parser"), "{reason}");
2465 assert!(
2466 reason.ends_with("AppArmor profile confining mur: docker-default."),
2467 "{reason}"
2468 );
2469 }
2470
2471 #[test]
2472 fn a_failure_under_the_sealed_profile_points_at_a_stale_load() {
2473 let reason = failure(
2474 step(CreationStep::Unshare, libc::EPERM),
2475 UsernsGrant::ProfileConfining,
2476 ProfileAttachment::Sealed {
2477 profile: "mur-sealed".to_string(),
2478 },
2479 )
2480 .reason();
2481 assert!(
2482 reason.contains("sudo apparmor_parser -r /etc/apparmor.d/mur-sealed"),
2483 "{reason}"
2484 );
2485 assert!(reason.contains("stale"), "{reason}");
2486 assert!(!reason.contains("LAST RESORT"), "{reason}");
2487 }
2488
2489 const READINGS_LINE: &str = "Host readings: kernel.apparmor_restrict_unprivileged_userns=1; \
2492 AppArmor profile confining mur: unconfined.";
2493 const LOAD: &str = "sudo apparmor_parser -r /etc/apparmor.d/mur-sealed";
2494 const NOT_LOADED: &str = "No mur-sealed AppArmor profile naming this binary's path is loaded";
2495 const NOT_LOADED_EITHER: &str = "No mur-sealed AppArmor profile is loaded either";
2496
2497 fn gap_reason(gap: &ProfileGap) -> String {
2499 egress_namespace_failure(
2500 true,
2501 step(CreationStep::IdentityMaps, libc::EPERM),
2502 UsernsGrant::Withheld,
2503 ProfileAttachment::Unconfined,
2504 gap.clone(),
2505 )
2506 .expect("a failed attempt is a failure")
2507 .reason()
2508 }
2509
2510 fn not_loaded(exe: &str, path: ProfiledPath, file: ProfileFile) -> ProfileGap {
2511 ProfileGap::NotLoaded {
2512 exe: exe.into(),
2513 path,
2514 file,
2515 }
2516 }
2517
2518 fn path_not_profiled(
2519 exe: &str,
2520 link: Option<&str>,
2521 profile_loaded: bool,
2522 file: ProfileFile,
2523 ) -> ProfileGap {
2524 ProfileGap::PathNotProfiled {
2525 exe: exe.into(),
2526 link: link.map(Into::into),
2527 profile_loaded,
2528 file,
2529 }
2530 }
2531
2532 #[test]
2535 fn every_profile_gap_is_worded_with_its_own_remedy_and_the_unchanged_readings() {
2536 for gap in [
2537 not_loaded(
2538 "/usr/local/bin/mur",
2539 ProfiledPath::System,
2540 ProfileFile::Installed,
2541 ),
2542 path_not_profiled(
2543 "/usr/local/bin/mur-0.10.0",
2544 Some("/usr/local/bin/mur"),
2545 true,
2546 ProfileFile::Installed,
2547 ),
2548 ProfileGap::NotAttached {
2549 exe: "/root/.cargo/bin/mur".into(),
2550 path: ProfiledPath::Home,
2551 },
2552 ProfileGap::Undetermined,
2553 ] {
2554 let reason = gap_reason(&gap);
2555 assert!(
2556 reason.contains(&format!(
2557 "Likely cause: AppArmor's unprivileged-userns restriction \
2558 (kernel.apparmor_restrict_unprivileged_userns=1) applies to this binary, \
2559 which no mur-sealed profile confines. {} {READINGS_LINE}",
2560 profile_gap_remedy(&gap, env!("CARGO_PKG_VERSION"))
2561 )),
2562 "{reason}"
2563 );
2564 assert!(reason.ends_with(READINGS_LINE), "{reason}");
2565 }
2566 }
2567
2568 #[test]
2569 fn a_profile_not_loaded_is_told_to_load_it_and_nothing_about_the_path() {
2570 let reason = gap_reason(¬_loaded(
2571 "/home/u/.local/bin/mur",
2572 ProfiledPath::Home,
2573 ProfileFile::Installed,
2574 ));
2575 assert!(reason.contains(NOT_LOADED), "{reason}");
2576 assert!(reason.contains("`mur-sealed-home`"), "{reason}");
2577 assert!(reason.contains(&format!("`{LOAD}`")), "{reason}");
2578 assert!(!reason.contains("mv -f"), "{reason}");
2579 assert!(!reason.contains("install-dev-apparmor.sh"), "{reason}");
2580 assert!(!reason.contains("This path is why"), "{reason}");
2581 assert!(reason.contains("LAST RESORT"), "{reason}");
2582 assert!(reason.ends_with(READINGS_LINE), "{reason}");
2583 }
2584
2585 #[test]
2586 fn a_checkout_build_with_no_profile_loaded_is_told_to_run_the_dev_script() {
2587 let reason = gap_reason(¬_loaded(
2588 "/repo/target/debug/mur",
2589 ProfiledPath::Checkout,
2590 ProfileFile::Installed,
2591 ));
2592 assert!(reason.contains(NOT_LOADED), "{reason}");
2593 assert!(
2594 reason.contains("`sudo scripts/install-dev-apparmor.sh`"),
2595 "{reason}"
2596 );
2597 assert!(!reason.contains(LOAD), "{reason}");
2598 assert!(!reason.contains("mv -f"), "{reason}");
2599 assert!(reason.contains("LAST RESORT"), "{reason}");
2600 }
2601
2602 #[test]
2605 fn a_missing_profile_file_is_fetched_from_the_release_tag_or_a_checkout() {
2606 let gap = not_loaded(
2607 "/usr/local/bin/mur",
2608 ProfiledPath::System,
2609 ProfileFile::Absent,
2610 );
2611 let released = profile_gap_remedy(&gap, "0.10.0");
2612 assert!(
2613 released.contains(
2614 "`curl -fsSL https://raw.githubusercontent.com/murmur-nexus/murmur/v0.10.0/\
2615 packaging/apparmor/mur-sealed -o mur-sealed && sudo install -m 644 mur-sealed \
2616 /etc/apparmor.d/mur-sealed && sudo apparmor_parser -r \
2617 /etc/apparmor.d/mur-sealed`"
2618 ),
2619 "{released}"
2620 );
2621 let pre_release = profile_gap_remedy(&gap, "0.11.0-dev");
2622 assert!(
2623 pre_release.contains(
2624 "`sudo install -m 644 packaging/apparmor/mur-sealed /etc/apparmor.d/mur-sealed \
2625 && sudo apparmor_parser -r /etc/apparmor.d/mur-sealed`"
2626 ),
2627 "{pre_release}"
2628 );
2629 assert!(pre_release.contains("0.11.0-dev"), "{pre_release}");
2630 assert!(!pre_release.contains("https://"), "{pre_release}");
2631 assert!(
2632 gap_reason(&gap).contains(&profile_gap_remedy(&gap, env!("CARGO_PKG_VERSION"))),
2633 "the reason renders this build's version"
2634 );
2635 }
2636
2637 #[test]
2638 fn a_symlink_at_a_system_path_is_told_to_move_the_file_over_it_with_sudo() {
2639 let reason = gap_reason(&path_not_profiled(
2640 "/usr/local/bin/mur-0.10.0",
2641 Some("/usr/local/bin/mur"),
2642 true,
2643 ProfileFile::Installed,
2644 ));
2645 assert!(
2646 reason.contains("/usr/local/bin/mur is a symlink to /usr/local/bin/mur-0.10.0"),
2647 "{reason}"
2648 );
2649 assert!(
2650 reason.contains("`sudo mv -f /usr/local/bin/mur-0.10.0 /usr/local/bin/mur`"),
2651 "{reason}"
2652 );
2653 assert!(!reason.contains(NOT_LOADED), "{reason}");
2654 assert!(!reason.contains(NOT_LOADED_EITHER), "{reason}");
2655 assert!(!reason.contains(LOAD), "{reason}");
2656 assert!(!reason.contains("install -m 755"), "{reason}");
2657 assert!(reason.contains("LAST RESORT"), "{reason}");
2658 assert!(reason.ends_with(READINGS_LINE), "{reason}");
2659 }
2660
2661 #[test]
2662 fn a_symlink_at_a_home_path_is_told_to_move_the_file_over_it_without_sudo() {
2663 let reason = gap_reason(&path_not_profiled(
2664 "/space/scratch/mur-0.11.0-dev",
2665 Some("/home/u/.local/bin/mur"),
2666 true,
2667 ProfileFile::Installed,
2668 ));
2669 assert!(
2670 reason.contains("`mv -f /space/scratch/mur-0.11.0-dev /home/u/.local/bin/mur`"),
2671 "{reason}"
2672 );
2673 assert!(!reason.contains("sudo mv"), "{reason}");
2674 assert!(!reason.contains(NOT_LOADED_EITHER), "{reason}");
2675 }
2676
2677 #[test]
2679 fn an_unprofiled_path_with_no_profile_loaded_names_both_fixes_path_first() {
2680 let reason = gap_reason(&path_not_profiled(
2681 "/usr/local/bin/mur-0.10.0",
2682 Some("/usr/local/bin/mur"),
2683 false,
2684 ProfileFile::Absent,
2685 ));
2686 let path = reason
2687 .find("sudo mv -f /usr/local/bin/mur-0.10.0 /usr/local/bin/mur")
2688 .expect("the path fix");
2689 let loaded = reason.find(NOT_LOADED_EITHER).expect("the load step");
2690 let load = reason.find(LOAD).expect("the load command");
2691 assert!(path < loaded && loaded < load, "{reason}");
2692 assert!(reason.ends_with(READINGS_LINE), "{reason}");
2693 }
2694
2695 #[test]
2696 fn an_unprofiled_path_with_no_profiled_link_is_told_to_install_a_real_file() {
2697 let reason = gap_reason(&path_not_profiled(
2698 "/space/home/u/.local/bin/mur",
2699 None,
2700 true,
2701 ProfileFile::Installed,
2702 ));
2703 assert!(
2704 reason.contains(
2705 "`sudo rm -f /usr/local/bin/mur && sudo install -m 755 \
2706 /space/home/u/.local/bin/mur /usr/local/bin/mur`"
2707 ),
2708 "{reason}"
2709 );
2710 assert!(!reason.contains("mv -f"), "{reason}");
2711 assert!(!reason.contains("install-dev-apparmor.sh"), "{reason}");
2712
2713 let elsewhere = gap_reason(&path_not_profiled(
2714 "/cache/targets/card/debug/mur",
2715 None,
2716 true,
2717 ProfileFile::Installed,
2718 ));
2719 assert!(
2720 elsewhere.contains("sudo scripts/install-dev-apparmor.sh"),
2721 "a build outside <checkout>/target is pointed at the dev script: {elsewhere}"
2722 );
2723 }
2724
2725 #[test]
2726 fn a_loaded_profile_that_does_not_attach_is_told_to_reload_it_without_the_sysctl() {
2727 let reason = gap_reason(&ProfileGap::NotAttached {
2728 exe: "/root/.cargo/bin/mur".into(),
2729 path: ProfiledPath::Home,
2730 });
2731 assert!(reason.contains("out of date"), "{reason}");
2732 assert!(reason.contains(&format!("`{LOAD}`")), "{reason}");
2733 assert!(!reason.contains("LAST RESORT"), "{reason}");
2734 assert!(!reason.contains("mv -f"), "{reason}");
2735 assert!(!reason.contains(NOT_LOADED), "{reason}");
2736 assert!(reason.ends_with(READINGS_LINE), "{reason}");
2737 }
2738
2739 #[test]
2742 fn the_gap_changes_nothing_but_the_unconfined_withheld_remedy() {
2743 let gap = not_loaded(
2744 "/usr/local/bin/mur",
2745 ProfiledPath::System,
2746 ProfileFile::Installed,
2747 );
2748 for grant in UsernsGrant::ALL {
2749 for attachment in attachments() {
2750 let with = egress_namespace_failure(
2751 true,
2752 step(CreationStep::Unshare, libc::EPERM),
2753 *grant,
2754 attachment.clone(),
2755 gap.clone(),
2756 )
2757 .unwrap()
2758 .reason();
2759 let without = failure(
2760 step(CreationStep::Unshare, libc::EPERM),
2761 *grant,
2762 attachment.clone(),
2763 )
2764 .reason();
2765 if *grant == UsernsGrant::Withheld && attachment == ProfileAttachment::Unconfined {
2766 assert_ne!(with, without);
2767 } else {
2768 assert_eq!(with, without, "{grant:?} / {attachment:?}");
2769 }
2770 }
2771 }
2772 }
2773
2774 #[test]
2775 fn without_the_restriction_the_cause_is_not_apparmor() {
2776 for (grant, readings) in [
2777 (
2778 UsernsGrant::RestrictionDisabledHostWide,
2779 "kernel.apparmor_restrict_unprivileged_userns=0",
2780 ),
2781 (UsernsGrant::ApparmorAbsent, "AppArmor not enabled"),
2782 ] {
2783 for attachment in [
2784 ProfileAttachment::Unconfined,
2785 ProfileAttachment::NotReported,
2786 ] {
2787 let reason =
2788 failure(step(CreationStep::Unshare, libc::EPERM), grant, attachment).reason();
2789 assert!(
2790 !reason.contains("apparmor_restrict_unprivileged_userns=1"),
2791 "{reason}"
2792 );
2793 assert!(!reason.contains("apparmor_parser"), "{reason}");
2794 assert!(!reason.contains("--cap-add"), "{reason}");
2795 assert!(!reason.contains("CAP_SYS_ADMIN"), "{reason}");
2796 assert!(
2797 reason.contains("seccomp profile is the usual blocker"),
2798 "{reason}"
2799 );
2800 assert!(
2801 reason.contains("unshare(CLONE_NEWUSER) and setns"),
2802 "{reason}"
2803 );
2804 assert!(reason.contains("--security-opt seccomp=<file>"), "{reason}");
2805 assert!(
2806 reason.contains(&format!("Host readings: {readings};")),
2807 "{reason}"
2808 );
2809 }
2810 }
2811 }
2812
2813 #[test]
2816 fn another_security_modules_label_is_not_blamed_as_an_apparmor_profile() {
2817 let reason = failure(
2818 step(CreationStep::Unshare, libc::EPERM),
2819 UsernsGrant::ApparmorAbsent,
2820 ProfileAttachment::Other {
2821 profile: "unconfined_u:unconfined_r:unconfined_t:s0".to_string(),
2822 },
2823 )
2824 .reason();
2825 assert!(reason.contains("not AppArmor"), "{reason}");
2826 assert!(!reason.contains("the AppArmor profile `"), "{reason}");
2827 }
2828
2829 #[test]
2830 fn a_namespace_created_and_then_refused_says_which_half_failed() {
2831 let configure = failure(
2832 step(CreationStep::Loopback, libc::EPERM),
2833 UsernsGrant::RestrictionDisabledHostWide,
2834 ProfileAttachment::Unconfined,
2835 )
2836 .reason();
2837 assert!(
2838 configure.starts_with("bringing lo up failed with EPERM"),
2839 "{configure}"
2840 );
2841 assert!(configure.contains("CAP_NET_ADMIN"), "{configure}");
2842
2843 let owned = failure(
2844 step(CreationStep::IdentityMaps, libc::EACCES),
2845 UsernsGrant::RestrictionDisabledHostWide,
2846 ProfileAttachment::Unconfined,
2847 )
2848 .reason();
2849 assert!(
2850 owned.starts_with("writing uid_map/gid_map failed with EACCES (Permission denied)."),
2851 "{owned}"
2852 );
2853 assert!(owned.contains("could not be owned"), "{owned}");
2854 }
2855
2856 #[test]
2857 fn missing_kernel_support_names_the_limit_or_the_config_symbol_and_no_apparmor() {
2858 for (errno, named) in [
2859 (libc::ENOSPC, "user.max_user_namespaces"),
2860 (libc::EINVAL, "CONFIG_USER_NS"),
2861 (libc::ENOSYS, "CONFIG_USER_NS"),
2862 (libc::EUSERS, "nesting limit"),
2863 ] {
2864 let reason = failure(
2865 step(CreationStep::Unshare, errno),
2866 UsernsGrant::Withheld,
2867 ProfileAttachment::Unconfined,
2868 )
2869 .reason();
2870 assert!(reason.contains(named), "{reason}");
2871 assert!(
2872 reason.contains(errno_name(errno).expect("a named errno")),
2873 "{reason}"
2874 );
2875 assert!(!reason.contains("apparmor_parser"), "{reason}");
2876 assert!(!reason.contains("LAST RESORT"), "{reason}");
2877 }
2878 }
2879
2880 #[test]
2881 fn an_unnamed_errno_is_rendered_by_number() {
2882 assert_eq!(errno_name(libc::EPERM), Some("EPERM"));
2883 assert_eq!(errno_name(-7), None);
2884 let failure = failure(
2885 step(CreationStep::HandOff, 4095),
2886 UsernsGrant::Withheld,
2887 ProfileAttachment::Unconfined,
2888 );
2889 assert_eq!(
2890 failure.summary(),
2891 "handing the namespace to the runtime failed with errno 4095"
2892 );
2893 }
2894
2895 #[test]
2896 fn the_summary_names_the_call_and_the_errno() {
2897 let failed = failure(
2898 step(CreationStep::Unshare, libc::EPERM),
2899 UsernsGrant::Withheld,
2900 ProfileAttachment::Unconfined,
2901 );
2902 assert_eq!(
2903 failed.summary(),
2904 "unshare(CLONE_NEWUSER | CLONE_NEWNET) failed with EPERM"
2905 );
2906 let creator = failure(
2907 Err(NamespaceAttemptError::Creator("fork() failed".to_string())),
2908 UsernsGrant::Withheld,
2909 ProfileAttachment::Unconfined,
2910 );
2911 assert_eq!(creator.summary(), "fork() failed");
2912 }
2913
2914 #[test]
2915 fn every_step_round_trips_through_its_code() {
2916 for step in CreationStep::ALL {
2917 assert_eq!(CreationStep::from_code(step as u32), Some(step));
2918 }
2919 assert_eq!(CreationStep::from_code(0), None);
2920 assert_eq!(CreationStep::from_code(99), None);
2921 }
2922
2923 #[test]
2928 fn a_session_creation_failure_is_refused_in_the_staging_words() {
2929 for (failed, errno, grant, attachment) in [
2930 (
2931 CreationStep::Unshare,
2932 libc::EPERM,
2933 UsernsGrant::Withheld,
2934 ProfileAttachment::Unconfined,
2935 ),
2936 (
2937 CreationStep::LocalRoute,
2938 libc::EPERM,
2939 UsernsGrant::Withheld,
2940 ProfileAttachment::Other {
2941 profile: "docker-default".to_string(),
2942 },
2943 ),
2944 (
2945 CreationStep::Unshare,
2946 libc::ENOSPC,
2947 UsernsGrant::RestrictionDisabledHostWide,
2948 ProfileAttachment::Unconfined,
2949 ),
2950 ] {
2951 let staging = egress_namespace_failure(
2952 true,
2953 step(failed, errno),
2954 grant,
2955 attachment.clone(),
2956 ProfileGap::Undetermined,
2957 );
2958 let staging_refusal = check_egress_namespace(true, staging.as_ref())
2959 .expect_err("staging refuses")
2960 .to_string();
2961 let establish = session_netns_creation_error(
2962 NamespaceAttemptError::Step {
2963 step: failed,
2964 errno,
2965 },
2966 grant,
2967 attachment,
2968 ProfileGap::Undetermined,
2969 );
2970 assert_eq!(establish, format!("sandbox: E-CAP-005: {staging_refusal}"));
2971 assert!(
2972 establish.contains(&staging.expect("a failure").reason()),
2973 "{establish}"
2974 );
2975 }
2976 }
2977
2978 #[test]
2979 fn a_session_creator_that_could_not_run_keeps_the_runtime_account() {
2980 let error = session_netns_creation_error(
2981 NamespaceAttemptError::Creator("fork() for the namespace creator failed".to_string()),
2982 UsernsGrant::Withheld,
2983 ProfileAttachment::Unconfined,
2984 ProfileGap::Undetermined,
2985 );
2986 assert_eq!(
2987 error,
2988 "sandbox: could not create this session's network namespace: fork() for the \
2989 namespace creator failed"
2990 );
2991 }
2992
2993 #[test]
2996 fn the_socket_count_is_one_per_port_plus_the_resolver() {
2997 let plan = CapsuleNetnsPlan {
2998 tcp_ports: vec![80, 443],
2999 uid: 1000,
3000 gid: 1000,
3001 force_pid_namespace_failure: false,
3002 };
3003 assert_eq!(plan.socket_count(), 3);
3004 }
3005
3006 #[test]
3007 fn a_capsule_with_no_tcp_egress_still_binds_the_resolver() {
3008 let plan = CapsuleNetnsPlan {
3011 tcp_ports: Vec::new(),
3012 uid: 1000,
3013 gid: 1000,
3014 force_pid_namespace_failure: false,
3015 };
3016 assert_eq!(plan.socket_count(), 1);
3017 }
3018
3019 #[test]
3020 fn the_widest_allowlist_still_fits_one_scm_rights_message() {
3021 let plan = CapsuleNetnsPlan {
3022 tcp_ports: (0..crate::egress_proxy::MAX_EGRESS_TCP_PORTS as u16).collect(),
3023 uid: 1000,
3024 gid: 1000,
3025 force_pid_namespace_failure: false,
3026 };
3027 assert!(plan.socket_count() <= MAX_NAMESPACE_SOCKETS);
3028 assert_eq!(
3030 MAX_SESSION_NETNS_FDS,
3031 MAX_NAMESPACE_SOCKETS + BRIDGE_SOCKET_COUNT + 3
3032 );
3033 }
3034
3035 #[cfg(target_os = "linux")]
3039 #[test]
3040 fn an_ended_session_refuses_to_create_a_namespace() {
3041 let netns = SessionNetns::new();
3042 netns.end();
3043 let refusal = netns
3044 .establish(
3045 &[],
3046 crate::egress_proxy::EgressPolicy::new(Vec::new(), Vec::new()),
3047 None,
3048 )
3049 .expect_err("an ended session creates nothing");
3050 assert!(refusal.contains("session has ended"), "{refusal}");
3051 assert_eq!(netns.creations(), 0);
3052 }
3053
3054 #[cfg(target_os = "linux")]
3057 #[test]
3058 fn a_session_creates_its_namespace_once_and_ends_it_for_good() {
3059 if skip_without_egress_namespace(
3060 "a_session_creates_its_namespace_once_and_ends_it_for_good",
3061 ) {
3062 return;
3063 }
3064 let policy = || crate::egress_proxy::EgressPolicy::new(Vec::new(), Vec::new());
3065 let netns = SessionNetns::new();
3066 assert_eq!(
3067 netns.creations(),
3068 0,
3069 "nothing is created before it is needed"
3070 );
3071
3072 let first = netns
3073 .establish(&[], policy(), None)
3074 .expect("the namespace is created");
3075 let second = netns
3076 .establish(&[], policy(), None)
3077 .expect("the namespace is reused");
3078 assert_eq!(netns.creations(), 1);
3079 let inode = |fd: std::os::fd::RawFd| {
3080 std::fs::metadata(format!("/proc/self/fd/{fd}"))
3081 .map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
3082 .unwrap()
3083 };
3084 assert_eq!(inode(first.raw_fds().1), inode(second.raw_fds().1));
3085 let host_netns = std::fs::metadata("/proc/self/ns/net")
3086 .map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
3087 .unwrap();
3088 assert_ne!(
3089 inode(first.raw_fds().1),
3090 host_netns,
3091 "the session's namespace is not the runtime's own"
3092 );
3093
3094 netns.end();
3095 netns.end();
3096 assert!(netns.establish(&[], policy(), None).is_err());
3097 assert_eq!(
3098 netns.creations(),
3099 1,
3100 "an ended namespace is never recreated"
3101 );
3102 }
3103
3104 #[cfg(target_os = "linux")]
3107 #[test]
3108 fn a_creation_failure_names_its_step_and_errno() {
3109 assert_eq!(
3110 linux::decode_failure_record(4, libc::EPERM),
3111 Some(NamespaceAttemptError::Step {
3112 step: CreationStep::LocalRoute,
3113 errno: libc::EPERM,
3114 })
3115 );
3116 assert_eq!(linux::decode_failure_record(99, 0), None);
3117 }
3118
3119 #[cfg(target_os = "linux")]
3122 fn close_range_enosys_filter() -> [libc::sock_filter; 7] {
3123 const BPF_LD_W_ABS: u16 = 0x20;
3125 const BPF_JMP_JEQ_K: u16 = 0x15;
3126 const BPF_RET_K: u16 = 0x06;
3127 const SECCOMP_RET_ALLOW: u32 = 0x7fff_0000;
3128 const SECCOMP_RET_ERRNO: u32 = 0x0005_0000;
3129 const NR: u32 = 0;
3131 const ARCH: u32 = 4;
3132 #[cfg(target_arch = "x86_64")]
3133 const AUDIT_ARCH: u32 = 0xc000_003e;
3134 #[cfg(target_arch = "aarch64")]
3135 const AUDIT_ARCH: u32 = 0xc000_00b7;
3136 let statement = |code: u16, jt: u8, jf: u8, k: u32| libc::sock_filter { code, jt, jf, k };
3137 [
3138 statement(BPF_LD_W_ABS, 0, 0, ARCH),
3139 statement(BPF_JMP_JEQ_K, 1, 0, AUDIT_ARCH),
3140 statement(BPF_RET_K, 0, 0, SECCOMP_RET_ALLOW),
3141 statement(BPF_LD_W_ABS, 0, 0, NR),
3142 statement(BPF_JMP_JEQ_K, 0, 1, libc::SYS_close_range as u32),
3143 statement(BPF_RET_K, 0, 0, SECCOMP_RET_ERRNO | libc::ENOSYS as u32),
3144 statement(BPF_RET_K, 0, 0, SECCOMP_RET_ALLOW),
3145 ]
3146 }
3147
3148 #[cfg(target_os = "linux")]
3157 #[test]
3158 #[allow(unsafe_code)]
3159 fn the_holder_ends_with_its_session_where_close_range_is_refused() {
3160 let test = "the_holder_ends_with_its_session_where_close_range_is_refused";
3161 if skip_without_egress_namespace(test) {
3162 return;
3163 }
3164 std::thread::spawn(|| {
3165 let filter = close_range_enosys_filter();
3166 let program = libc::sock_fprog {
3167 len: filter.len() as u16,
3168 filter: filter.as_ptr().cast_mut(),
3169 };
3170 unsafe {
3173 assert_eq!(libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
3174 assert_eq!(
3175 libc::prctl(
3176 libc::PR_SET_SECCOMP,
3177 libc::SECCOMP_MODE_FILTER,
3178 &raw const program,
3179 ),
3180 0,
3181 "installing the filter: {}",
3182 std::io::Error::last_os_error()
3183 );
3184 let refused = libc::syscall(
3186 libc::SYS_close_range,
3187 u32::MAX - 1,
3188 u32::MAX,
3189 0 as libc::c_uint,
3190 );
3191 assert_eq!(refused, -1);
3192 assert_eq!(
3193 std::io::Error::last_os_error().raw_os_error(),
3194 Some(libc::ENOSYS)
3195 );
3196 }
3197
3198 let policy = || crate::egress_proxy::EgressPolicy::new(Vec::new(), Vec::new());
3199 let end_explicitly = |explicit: bool| {
3200 let netns = SessionNetns::new();
3201 let join = netns
3202 .establish(&[], policy(), None)
3203 .expect("the namespace is created");
3204 let pid_ns = std::fs::read_link(format!("/proc/self/fd/{}", join.pid_ns_fd()))
3208 .expect("the session's PID namespace handle is readable");
3209 let holder = std::fs::read_dir("/proc")
3210 .expect("/proc is readable")
3211 .filter_map(Result::ok)
3212 .filter_map(|entry| entry.file_name().to_str()?.parse::<i32>().ok())
3213 .find(|pid| {
3214 std::fs::read_link(format!("/proc/{pid}/ns/pid"))
3215 .is_ok_and(|namespace| namespace == pid_ns)
3216 && std::fs::read_to_string(format!("/proc/{pid}/status")).is_ok_and(
3217 |status| {
3218 status
3219 .lines()
3220 .find_map(|line| line.strip_prefix("NSpid:"))
3221 .and_then(|fields| fields.split_whitespace().last())
3222 == Some("1")
3223 },
3224 )
3225 })
3226 .expect("the session's holder is pid 1 of its PID namespace");
3227 let status = std::fs::read_to_string(format!("/proc/{holder}/status")).unwrap();
3228 assert!(
3229 status.lines().any(|line| line == "Seccomp:\t2"),
3230 "the holder is not under the filter: {status}"
3231 );
3232 let held_fds = || -> Vec<_> {
3239 std::fs::read_dir(format!("/proc/{holder}/fd"))
3240 .expect("the holder's descriptor table is readable")
3241 .filter_map(|entry| std::fs::read_link(entry.ok()?.path()).ok())
3242 .collect()
3243 };
3244 let sleeping = || {
3245 std::fs::read_to_string(format!("/proc/{holder}/stat")).is_ok_and(|stat| {
3246 stat.rsplit_once(") ")
3247 .is_some_and(|(_, rest)| rest.starts_with('S'))
3248 })
3249 };
3250 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
3251 let held = loop {
3252 let held = held_fds();
3253 if (held.len() <= 1 && sleeping()) || std::time::Instant::now() >= deadline {
3254 break held;
3255 }
3256 std::thread::sleep(std::time::Duration::from_millis(20));
3257 };
3258 assert!(sleeping(), "holder {holder} never blocked on its lifeline");
3259 assert_eq!(
3260 held.len(),
3261 1,
3262 "the holder keeps only its lifeline: {held:?}"
3263 );
3264 assert!(
3265 held[0].to_string_lossy().starts_with("pipe:"),
3266 "the holder keeps only its lifeline: {held:?}"
3267 );
3268 if explicit {
3269 netns.end();
3270 } else {
3271 drop(netns);
3272 }
3273 drop(join);
3274 let running = |pid: i32| {
3277 std::fs::read_to_string(format!("/proc/{pid}/stat")).is_ok_and(|stat| {
3278 stat.rsplit_once(") ")
3279 .is_some_and(|(_, rest)| !rest.starts_with('Z'))
3280 })
3281 };
3282 let wait = crate::sandbox::SESSION_END_KILL_WAIT;
3283 let deadline = std::time::Instant::now() + wait;
3284 while running(holder) {
3285 assert!(
3286 std::time::Instant::now() < deadline,
3287 "holder {holder} outlived its session by {wait:?} (ended explicitly: \
3288 {explicit})"
3289 );
3290 std::thread::sleep(std::time::Duration::from_millis(20));
3291 }
3292 };
3293 end_explicitly(true);
3294 end_explicitly(false);
3295 })
3296 .join()
3297 .expect("the filtered thread's assertions hold");
3298 }
3299
3300 #[cfg(target_os = "linux")]
3304 #[test]
3305 fn the_probe_and_a_session_creation_agree_about_this_host() {
3306 let probe = detect_egress_namespace_failure();
3307 let session = linux::create_session_netns(&CapsuleNetnsPlan::resolve(Vec::new())).map(drop);
3308 match (&probe, &session) {
3309 (None, Ok(())) => {}
3310 (Some(failure), Err(NamespaceAttemptError::Step { step, errno })) => {
3311 assert_eq!(failure.failed_call, step.describe());
3312 assert_eq!(failure.errno, Some(*errno));
3313 }
3314 other => panic!("the probe and a session creation disagree: {other:?}"),
3315 }
3316 }
3317}