1use super::landlock::LandlockManager;
2use crate::error::{NucleusError, Result};
3use crate::oci::OciBundle;
4use nix::unistd::{Gid, Uid};
5use sha2::{Digest, Sha256};
6use std::ffi::CString;
7use std::fs::{self, DirBuilder, OpenOptions};
8use std::io;
9use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt};
10use std::path::{Component, Path, PathBuf};
11use std::process::Command;
12use tracing::{debug, info, warn};
13
14const NIX_STORE_EXEC_ROOT: &str = "/nix/store";
15const NIXOS_ROOTLESS_USERNS_WRAPPER_DIR: &str = "/run/wrappers/bin";
16const ROOTLESS_USERNS_HELPERS: &[&str] = &["newuidmap", "newgidmap"];
17const ROOTLESS_USERNS_HELPER_EXEC_ROOTS_ENV: &str = "NUCLEUS_ROOTLESS_USERNS_HELPER_EXEC_ROOTS";
18const RUNSC_DEBUG_DIR_ENV: &str = "NUCLEUS_RUNSC_DEBUG_DIR";
19const RUNSC_REEXEC_VIA_PROC_SELF_EXE_ENV: &str = "NUCLEUS_RUNSC_REEXEC_VIA_PROC_SELF_EXE";
20const RUNSC_SUPERVISOR_PATH: &str =
21 "/run/wrappers/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum GVisorNetworkMode {
26 None,
28 Sandbox,
31 Host,
33}
34
35#[derive(
37 Debug,
38 Clone,
39 Copy,
40 PartialEq,
41 Eq,
42 Default,
43 clap::ValueEnum,
44 serde::Serialize,
45 serde::Deserialize,
46)]
47#[serde(rename_all = "kebab-case")]
48pub enum GVisorPlatform {
49 #[default]
51 Systrap,
52 Kvm,
54 Ptrace,
56}
57
58impl GVisorPlatform {
59 pub fn as_flag(self) -> &'static str {
60 match self {
61 Self::Systrap => "systrap",
62 Self::Kvm => "kvm",
63 Self::Ptrace => "ptrace",
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct GVisorOciRunOptions {
71 pub network_mode: GVisorNetworkMode,
73 pub ignore_cgroups: bool,
75 pub runsc_rootless: bool,
81 pub stage_runsc_binary: bool,
88 pub require_supervisor_exec_policy: bool,
90 pub platform: GVisorPlatform,
92 pub console_socket: Option<PathBuf>,
94}
95
96impl Default for GVisorOciRunOptions {
97 fn default() -> Self {
98 Self {
99 network_mode: GVisorNetworkMode::None,
100 ignore_cgroups: false,
101 runsc_rootless: false,
102 stage_runsc_binary: false,
103 require_supervisor_exec_policy: false,
104 platform: GVisorPlatform::default(),
105 console_socket: None,
106 }
107 }
108}
109
110impl GVisorOciRunOptions {
111 fn network_flag(&self) -> &'static str {
112 match self.network_mode {
113 GVisorNetworkMode::None => "none",
114 GVisorNetworkMode::Sandbox => "sandbox",
115 GVisorNetworkMode::Host => "host",
116 }
117 }
118}
119
120pub struct GVisorRuntime {
125 runsc_path: String,
126}
127
128impl GVisorRuntime {
129 pub fn new() -> Result<Self> {
133 let runsc_path = Self::find_runsc()?;
134 info!("Found runsc at: {}", runsc_path);
135 Ok(Self { runsc_path })
136 }
137
138 pub fn with_path(runsc_path: String) -> Self {
144 Self { runsc_path }
145 }
146
147 pub fn resolve_path() -> Result<String> {
151 Self::find_runsc()
152 }
153
154 fn find_runsc() -> Result<String> {
156 let paths = vec![
158 "/usr/local/bin/runsc",
159 "/usr/bin/runsc",
160 "/opt/gvisor/runsc",
161 ];
162
163 for path in &paths {
164 if let Some(validated) = Self::validate_runsc_path(Path::new(path))? {
165 return Ok(validated);
166 }
167 }
168
169 if Uid::effective().is_root() {
172 return Err(NucleusError::GVisorError(
173 "runsc binary not found in trusted system paths".to_string(),
174 ));
175 }
176
177 if let Some(path_var) = std::env::var_os("PATH") {
179 for dir in std::env::split_paths(&path_var) {
180 let candidate = dir.join("runsc");
181 if let Some(validated) = Self::validate_runsc_path(&candidate)? {
182 return Ok(validated);
183 }
184 }
185 }
186
187 Err(NucleusError::GVisorError(
188 "runsc binary not found. Please install gVisor.".to_string(),
189 ))
190 }
191
192 fn validate_runsc_path(path: &Path) -> Result<Option<String>> {
193 if !path.exists() {
194 return Ok(None);
195 }
196 if !path.is_file() {
197 return Ok(None);
198 }
199
200 let canonical = std::fs::canonicalize(path).map_err(|e| {
201 NucleusError::GVisorError(format!(
202 "Failed to canonicalize runsc path {:?}: {}",
203 path, e
204 ))
205 })?;
206
207 let resolved = Self::unwrap_nix_wrapper(&canonical).unwrap_or_else(|| canonical.clone());
212
213 let metadata = std::fs::metadata(&resolved).map_err(|e| {
214 NucleusError::GVisorError(format!("Failed to stat runsc path {:?}: {}", resolved, e))
215 })?;
216
217 let mode = metadata.permissions().mode();
218 if mode & 0o022 != 0 {
219 return Err(NucleusError::GVisorError(format!(
220 "Refusing insecure runsc binary permissions at {:?} (mode {:o})",
221 resolved, mode
222 )));
223 }
224 if mode & 0o111 == 0 {
225 return Ok(None);
226 }
227
228 use std::os::unix::fs::MetadataExt;
231 let owner = metadata.uid();
232 let current_uid = nix::unistd::Uid::effective().as_raw();
233 if !Self::is_trusted_runsc_owner(&resolved, owner, current_uid) {
234 return Err(NucleusError::GVisorError(format!(
235 "Refusing runsc binary at {:?} owned by uid {} (expected root, current user {}, or immutable /nix/store artifact)",
236 resolved, owner, current_uid
237 )));
238 }
239
240 Ok(Some(resolved.to_string_lossy().to_string()))
241 }
242
243 fn is_trusted_runsc_owner(path: &Path, owner: u32, current_uid: u32) -> bool {
244 if owner == 0 || owner == current_uid {
245 return true;
246 }
247
248 if path.starts_with("/nix/store") {
254 if let Ok(meta) = std::fs::metadata(path) {
255 let mode = meta.permissions().mode();
256 if mode & 0o200 != 0 {
258 return false;
259 }
260 } else {
261 return false;
262 }
263 if let Some(parent) = path.parent() {
265 if let Ok(parent_meta) = std::fs::metadata(parent) {
266 let parent_mode = parent_meta.permissions().mode();
267 if parent_mode & 0o222 != 0 {
268 return false;
269 }
270 } else {
271 return false;
272 }
273 }
274 return true;
275 }
276
277 false
278 }
279
280 fn unwrap_nix_wrapper(path: &Path) -> Option<std::path::PathBuf> {
286 let content = std::fs::read_to_string(path).ok()?;
287 if content.len() > 4096 || !content.starts_with("#!") {
289 return None;
290 }
291 for line in content.lines().rev() {
293 let trimmed = line.trim();
294 if trimmed.starts_with("exec ") {
295 for token in trimmed.split_whitespace() {
298 let unquoted = token.trim_matches('"');
299 if unquoted.starts_with('/') && unquoted.contains("runsc") {
300 let candidate = std::path::PathBuf::from(unquoted);
301 if candidate.exists() && candidate.is_file() {
302 debug!("Resolved Nix wrapper {:?} → {:?}", path, candidate);
303 return Some(candidate);
304 }
305 }
306 }
307 }
308 }
309 None
310 }
311
312 pub fn exec_with_oci_bundle(&self, container_id: &str, bundle: &OciBundle) -> Result<()> {
318 self.exec_with_oci_bundle_options(container_id, bundle, GVisorOciRunOptions::default())
319 }
320
321 pub fn exec_with_oci_bundle_options(
333 &self,
334 container_id: &str,
335 bundle: &OciBundle,
336 options: GVisorOciRunOptions,
337 ) -> Result<()> {
338 info!(
339 "Executing with gVisor using OCI bundle at {:?} (network: {:?}, platform: {:?})",
340 bundle.bundle_path(),
341 options.network_mode,
342 options.platform,
343 );
344
345 let runsc_root = Self::secure_runsc_root(container_id)?;
350
351 let runsc_runtime_dir = runsc_root.join("runtime");
352 Self::ensure_secure_runsc_dir(&runsc_runtime_dir, "runsc runtime directory")?;
353
354 let (program_path, mut exec_allow_roots) =
355 self.prepare_supervisor_runsc_program(&runsc_root, options.stage_runsc_binary)?;
356 if options.runsc_rootless {
357 exec_allow_roots.extend(Self::rootless_userns_helper_exec_roots());
358 }
359
360 let debug_dir = Self::runsc_debug_dir()?;
364 let mut args = self.build_oci_run_args(
365 container_id,
366 bundle,
367 &runsc_root,
368 &options,
369 debug_dir.as_deref(),
370 );
371 args[0] = program_path.to_string_lossy().to_string();
372
373 debug!("runsc OCI args: {:?}", args);
374
375 let program = CString::new(program_path.to_string_lossy().as_ref())
377 .map_err(|e| NucleusError::GVisorError(format!("Invalid runsc path: {}", e)))?;
378
379 let c_args: Result<Vec<CString>> = args
380 .iter()
381 .map(|arg| {
382 CString::new(arg.as_str())
383 .map_err(|e| NucleusError::GVisorError(format!("Invalid argument: {}", e)))
384 })
385 .collect();
386 let c_args = c_args?;
387
388 let reexec_via_proc_self_exe =
395 options.runsc_rootless && !options.require_supervisor_exec_policy;
396 let c_env = self.exec_environment(&runsc_runtime_dir, reexec_via_proc_self_exe)?;
397
398 if options.require_supervisor_exec_policy {
404 self.apply_supervisor_exec_policy(
405 &exec_allow_roots,
406 options.require_supervisor_exec_policy,
407 )?;
408 }
409
410 nix::unistd::execve::<std::ffi::CString, std::ffi::CString>(&program, &c_args, &c_env)?;
412
413 Ok(())
415 }
416
417 #[allow(clippy::too_many_arguments)]
421 pub fn exec_with_oci_bundle_network(
422 &self,
423 container_id: &str,
424 bundle: &OciBundle,
425 network_mode: GVisorNetworkMode,
426 ignore_cgroups: bool,
427 runsc_rootless: bool,
428 stage_runsc_binary: bool,
429 require_supervisor_exec_policy: bool,
430 platform: GVisorPlatform,
431 ) -> Result<()> {
432 self.exec_with_oci_bundle_options(
433 container_id,
434 bundle,
435 GVisorOciRunOptions {
436 network_mode,
437 ignore_cgroups,
438 runsc_rootless,
439 stage_runsc_binary,
440 require_supervisor_exec_policy,
441 platform,
442 console_socket: None,
443 },
444 )
445 }
446
447 pub fn is_available() -> bool {
449 Self::find_runsc().is_ok()
450 }
451
452 pub fn version(&self) -> Result<String> {
454 let output = Command::new(&self.runsc_path)
455 .arg("--version")
456 .output()
457 .map_err(|e| NucleusError::GVisorError(format!("Failed to get version: {}", e)))?;
458
459 if !output.status.success() {
460 return Err(NucleusError::GVisorError(
461 "Failed to get runsc version".to_string(),
462 ));
463 }
464
465 let version = String::from_utf8_lossy(&output.stdout).to_string();
466 Ok(version.trim().to_string())
467 }
468
469 fn exec_environment(
470 &self,
471 runtime_dir: &Path,
472 reexec_via_proc_self_exe: bool,
473 ) -> Result<Vec<CString>> {
474 let mut env = Vec::new();
475 let mut push = |key: &str, value: String| -> Result<()> {
476 env.push(
477 CString::new(format!("{}={}", key, value))
478 .map_err(|e| NucleusError::GVisorError(format!("Invalid {}: {}", key, e)))?,
479 );
480 Ok(())
481 };
482
483 push("PATH", RUNSC_SUPERVISOR_PATH.to_string())?;
488 let runtime_dir = runtime_dir.to_string_lossy().to_string();
489 push("TMPDIR", runtime_dir.clone())?;
490 push("XDG_RUNTIME_DIR", runtime_dir)?;
491
492 push("HOME", "/root".to_string())?;
496 push("USER", "root".to_string())?;
497 push("LOGNAME", "root".to_string())?;
498 if reexec_via_proc_self_exe {
499 push(RUNSC_REEXEC_VIA_PROC_SELF_EXE_ENV, "1".to_string())?;
500 }
501
502 Ok(env)
503 }
504
505 fn runsc_debug_dir() -> Result<Option<PathBuf>> {
506 let Some(raw) = std::env::var_os(RUNSC_DEBUG_DIR_ENV).filter(|value| !value.is_empty())
507 else {
508 return Ok(None);
509 };
510
511 let path = Self::absolute_path(Path::new(&raw), "runsc debug directory")?;
512 Self::ensure_secure_runsc_dir(&path, "runsc debug directory")?;
513 Ok(Some(path))
514 }
515
516 fn prepare_supervisor_runsc_program(
517 &self,
518 runsc_root: &Path,
519 force_private_stage: bool,
520 ) -> Result<(PathBuf, Vec<PathBuf>)> {
521 let canonical = fs::canonicalize(&self.runsc_path).map_err(|e| {
522 NucleusError::GVisorError(format!(
523 "Failed to canonicalize runsc path {:?}: {}",
524 self.runsc_path, e
525 ))
526 })?;
527 let canonical = Self::unwrap_nix_wrapper(&canonical).unwrap_or(canonical);
528
529 Self::ensure_secure_runsc_dir(runsc_root, "runsc root directory")?;
530
531 if !force_private_stage {
532 if let Some(exec_root) = Self::nix_store_runsc_exec_root(&canonical) {
533 return Ok((canonical, Self::supervisor_exec_allow_roots(exec_root)));
537 }
538 }
539
540 let private_dir = runsc_root.join("exec-allow");
541 Self::ensure_secure_runsc_dir(&private_dir, "private runsc exec directory")?;
542
543 if force_private_stage {
544 debug!(
549 source = ?canonical,
550 target_root = ?private_dir,
551 "Staging runsc binary for private supervisor exec root"
552 );
553 }
554
555 let stage_dir = Self::create_unique_runsc_stage_dir(&private_dir)?;
560 let staged = stage_dir.join("runsc");
561 Self::copy_runsc_nofollow(&canonical, &staged)?;
562
563 Ok((staged, Self::supervisor_exec_allow_roots(private_dir)))
564 }
565
566 fn supervisor_exec_allow_roots(program_root: PathBuf) -> Vec<PathBuf> {
567 vec![program_root]
570 }
571
572 fn rootless_userns_helper_exec_roots() -> Vec<PathBuf> {
573 let mut roots: Vec<PathBuf> = ROOTLESS_USERNS_HELPERS
577 .iter()
578 .map(|helper| PathBuf::from(NIXOS_ROOTLESS_USERNS_WRAPPER_DIR).join(helper))
579 .collect();
580
581 if let Some(raw_roots) = std::env::var_os(ROOTLESS_USERNS_HELPER_EXEC_ROOTS_ENV)
586 .filter(|value| !value.is_empty())
587 {
588 roots.extend(std::env::split_paths(&raw_roots).filter(|path| path.is_absolute()));
589 }
590
591 roots
592 }
593
594 fn nix_store_runsc_exec_root(program: &Path) -> Option<PathBuf> {
595 if !program.starts_with(NIX_STORE_EXEC_ROOT) {
596 return None;
597 }
598
599 program.parent().map(Path::to_path_buf)
600 }
601
602 fn secure_runsc_root(container_id: &str) -> Result<PathBuf> {
603 let artifact_base = Self::gvisor_artifact_base()?;
604 let artifact_dir = artifact_base.join(Self::runsc_state_component(container_id));
605
606 if Self::host_root_requires_trusted_runsc_ancestry() {
607 Self::ensure_trusted_host_root_runsc_ancestry(
608 &artifact_base,
609 "gVisor runsc artifact base",
610 )?;
611 }
612
613 Self::ensure_secure_runsc_dir(&artifact_base, "gVisor runsc artifact base")?;
614 Self::ensure_secure_runsc_dir(&artifact_dir, "gVisor runsc artifact directory")?;
615
616 let runsc_root = artifact_dir.join("runsc-root");
617 Self::ensure_secure_runsc_dir(&runsc_root, "runsc root directory")?;
618 Ok(runsc_root)
619 }
620
621 fn gvisor_artifact_base() -> Result<PathBuf> {
622 if let Some(path) =
623 std::env::var_os("NUCLEUS_GVISOR_ARTIFACT_BASE").filter(|path| !path.is_empty())
624 {
625 return Self::absolute_path(Path::new(&path), "gVisor artifact base");
626 }
627
628 if !Uid::effective().is_root() || Self::root_uid_maps_to_unprivileged_host_uid_from_proc() {
629 if let Some(dir) = dirs::runtime_dir() {
630 return Ok(dir.join("nucleus-gvisor"));
631 }
632 }
633
634 if Uid::effective().is_root() {
635 Ok(PathBuf::from("/run/nucleus-gvisor"))
636 } else {
637 Ok(std::env::temp_dir().join(format!("nucleus-gvisor-{}", Uid::effective().as_raw())))
638 }
639 }
640
641 fn absolute_path(path: &Path, label: &str) -> Result<PathBuf> {
642 if path.is_absolute() {
643 return Ok(path.to_path_buf());
644 }
645
646 std::env::current_dir()
647 .map(|cwd| cwd.join(path))
648 .map_err(|e| {
649 NucleusError::GVisorError(format!(
650 "Failed to resolve current directory for {} {:?}: {}",
651 label, path, e
652 ))
653 })
654 }
655
656 fn runsc_state_component(container_id: &str) -> String {
657 if container_id.len() == 32 && container_id.chars().all(|c| c.is_ascii_hexdigit()) {
658 return container_id.to_string();
659 }
660
661 let digest = Sha256::digest(container_id.as_bytes());
662 format!("id-{}", hex::encode(&digest[..16]))
663 }
664
665 fn root_uid_maps_to_unprivileged_host_uid_from_proc() -> bool {
666 fs::read_to_string("/proc/self/uid_map")
667 .map(|uid_map| Self::root_uid_maps_to_unprivileged_host_uid(&uid_map))
668 .unwrap_or(false)
669 }
670
671 fn root_uid_maps_to_unprivileged_host_uid(uid_map: &str) -> bool {
672 for line in uid_map.lines() {
673 let mut fields = line.split_whitespace();
674 let Some(namespace_start) = fields.next() else {
675 continue;
676 };
677 let Some(host_start) = fields.next() else {
678 continue;
679 };
680 let Some(length) = fields.next() else {
681 continue;
682 };
683 if fields.next().is_some() {
684 continue;
685 }
686
687 let Ok(namespace_start) = namespace_start.parse::<u64>() else {
688 continue;
689 };
690 let Ok(host_start) = host_start.parse::<u64>() else {
691 continue;
692 };
693 let Ok(length) = length.parse::<u64>() else {
694 continue;
695 };
696
697 if namespace_start == 0 && length > 0 {
698 return host_start != 0;
699 }
700 }
701
702 false
703 }
704
705 fn host_root_requires_trusted_runsc_ancestry() -> bool {
706 Uid::effective().is_root() && !Self::root_uid_maps_to_unprivileged_host_uid_from_proc()
707 }
708
709 fn ensure_trusted_host_root_runsc_ancestry(path: &Path, label: &str) -> Result<()> {
710 let path = Self::absolute_path(path, label)?;
711
712 let mut current = PathBuf::new();
713 for component in path.components() {
714 match component {
715 Component::Prefix(prefix) => current.push(prefix.as_os_str()),
716 Component::RootDir => current.push(component.as_os_str()),
717 Component::CurDir => {}
718 Component::ParentDir => {
719 return Err(NucleusError::GVisorError(format!(
720 "{} {:?} contains a parent-directory component",
721 label, path
722 )));
723 }
724 Component::Normal(name) => {
725 current.push(name);
726 match fs::symlink_metadata(¤t) {
727 Ok(metadata) => Self::ensure_trusted_host_root_runsc_ancestor_component(
728 ¤t, metadata, label,
729 )?,
730 Err(e) if e.kind() == io::ErrorKind::NotFound => break,
731 Err(e) => {
732 return Err(NucleusError::GVisorError(format!(
733 "Failed to stat {} ancestor {:?}: {}",
734 label, current, e
735 )));
736 }
737 }
738 }
739 }
740 }
741
742 Ok(())
743 }
744
745 fn ensure_trusted_host_root_runsc_ancestor_component(
746 path: &Path,
747 metadata: fs::Metadata,
748 label: &str,
749 ) -> Result<()> {
750 if metadata.file_type().is_symlink() {
751 return Err(NucleusError::GVisorError(format!(
752 "Refusing symlink {} ancestor {:?}",
753 label, path
754 )));
755 }
756 if !metadata.file_type().is_dir() {
757 return Err(NucleusError::GVisorError(format!(
758 "{} ancestor {:?} is not a directory",
759 label, path
760 )));
761 }
762
763 let owner = metadata.uid();
764 if owner != 0 {
765 return Err(NucleusError::GVisorError(format!(
766 "{} ancestor {:?} is owned by uid {} (expected root)",
767 label, path, owner
768 )));
769 }
770
771 let mode = metadata.permissions().mode();
772 if mode & 0o022 != 0 && mode & 0o1000 == 0 {
773 return Err(NucleusError::GVisorError(format!(
774 "{} ancestor {:?} has unsafe permissions {:o}",
775 label,
776 path,
777 mode & 0o7777
778 )));
779 }
780
781 Ok(())
782 }
783
784 fn ensure_secure_runsc_dir(path: &Path, label: &str) -> Result<()> {
785 if let Some(parent) = path
786 .parent()
787 .filter(|parent| !parent.as_os_str().is_empty())
788 {
789 Self::ensure_trusted_runsc_parent(parent, label)?;
790 }
791
792 let mut created = false;
793 match fs::symlink_metadata(path) {
794 Ok(metadata) if metadata.file_type().is_symlink() => {
795 return Err(NucleusError::GVisorError(format!(
796 "Refusing symlink {} {:?}",
797 label, path
798 )));
799 }
800 Ok(metadata) if !metadata.file_type().is_dir() => {
801 return Err(NucleusError::GVisorError(format!(
802 "{} {:?} is not a directory",
803 label, path
804 )));
805 }
806 Ok(_) => {}
807 Err(e) if e.kind() == io::ErrorKind::NotFound => {
808 match DirBuilder::new().mode(0o700).create(path) {
809 Ok(()) => {
810 created = true;
811 }
812 Err(create_err) if create_err.kind() == io::ErrorKind::AlreadyExists => {}
813 Err(create_err) => {
814 return Err(NucleusError::GVisorError(format!(
815 "Failed to create {} {:?}: {}",
816 label, path, create_err
817 )));
818 }
819 }
820 }
821 Err(e) => {
822 return Err(NucleusError::GVisorError(format!(
823 "Failed to stat {} {:?}: {}",
824 label, path, e
825 )));
826 }
827 }
828
829 if created {
830 fs::set_permissions(path, fs::Permissions::from_mode(0o700)).map_err(|e| {
831 NucleusError::GVisorError(format!(
832 "Failed to secure newly-created {} permissions {:?}: {}",
833 label, path, e
834 ))
835 })?;
836 }
837
838 let dir = OpenOptions::new()
839 .read(true)
840 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_DIRECTORY)
841 .open(path)
842 .map_err(|e| {
843 NucleusError::GVisorError(format!(
844 "Failed to open {} {:?} without following symlinks: {}",
845 label, path, e
846 ))
847 })?;
848
849 let metadata = dir.metadata().map_err(|e| {
850 NucleusError::GVisorError(format!("Failed to stat {} {:?}: {}", label, path, e))
851 })?;
852 if !metadata.file_type().is_dir() {
853 return Err(NucleusError::GVisorError(format!(
854 "{} {:?} is not a directory",
855 label, path
856 )));
857 }
858
859 let owner = metadata.uid();
860 let expected = Uid::effective().as_raw();
861 if owner != expected {
862 if crate::isolation::uid_is_mapped_in_current_userns(owner)
869 && Uid::effective().is_root()
870 {
871 debug!(
872 "Reclaiming {} {:?} from mapped uid {} to container root",
873 label, path, owner
874 );
875 nix::unistd::chown(path, Some(Uid::from_raw(0)), Some(Gid::from_raw(0)))
876 .map_err(|e| {
877 NucleusError::GVisorError(format!(
878 "Failed to reclaim {} {:?}: {}",
879 label, path, e
880 ))
881 })?;
882 } else {
883 return Err(NucleusError::GVisorError(format!(
884 "{} {:?} is owned by uid {} (expected {})",
885 label, path, owner, expected
886 )));
887 }
888 }
889
890 let mode = metadata.permissions().mode() & 0o777;
891 if mode != 0o700 {
892 dir.set_permissions(fs::Permissions::from_mode(0o700))
893 .map_err(|e| {
894 NucleusError::GVisorError(format!(
895 "Failed to secure {} permissions {:?}: {}",
896 label, path, e
897 ))
898 })?;
899 }
900
901 Ok(())
902 }
903
904 fn ensure_trusted_runsc_parent(parent: &Path, label: &str) -> Result<()> {
905 let metadata = fs::symlink_metadata(parent).map_err(|e| {
906 NucleusError::GVisorError(format!(
907 "Failed to stat parent for {} {:?}: {}",
908 label, parent, e
909 ))
910 })?;
911 if metadata.file_type().is_symlink() {
912 return Err(NucleusError::GVisorError(format!(
913 "Refusing symlink parent for {} {:?}",
914 label, parent
915 )));
916 }
917 if !metadata.file_type().is_dir() {
918 return Err(NucleusError::GVisorError(format!(
919 "Parent for {} {:?} is not a directory",
920 label, parent
921 )));
922 }
923
924 let owner = metadata.uid();
925 let current = Uid::effective().as_raw();
926 let owner_trusted = owner == current
931 || owner == 0
932 || crate::isolation::uid_is_mapped_in_current_userns(owner);
933 let mode = metadata.permissions().mode();
934 let unsafe_writable = mode & 0o022 != 0 && mode & 0o1000 == 0;
935 if !owner_trusted || unsafe_writable {
936 return Err(NucleusError::GVisorError(format!(
937 "Parent for {} {:?} is not trusted (owner uid {}, mode {:o})",
938 label,
939 parent,
940 owner,
941 mode & 0o7777
942 )));
943 }
944
945 Ok(())
946 }
947
948 fn create_unique_runsc_stage_dir(private_dir: &Path) -> Result<PathBuf> {
949 let nonce = std::time::SystemTime::now()
950 .duration_since(std::time::UNIX_EPOCH)
951 .map(|duration| duration.as_nanos())
952 .unwrap_or_default();
953
954 for attempt in 0..100u32 {
955 let stage_dir = private_dir.join(format!(
956 "stage-{}-{}-{}",
957 std::process::id(),
958 nonce,
959 attempt
960 ));
961 match DirBuilder::new().mode(0o700).create(&stage_dir) {
962 Ok(()) => {
963 Self::ensure_secure_runsc_dir(&stage_dir, "runsc stage directory")?;
964 return Ok(stage_dir);
965 }
966 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
967 Err(e) => {
968 return Err(NucleusError::GVisorError(format!(
969 "Failed to create runsc stage directory {:?}: {}",
970 stage_dir, e
971 )));
972 }
973 }
974 }
975
976 Err(NucleusError::GVisorError(format!(
977 "Failed to create unique runsc stage directory under {:?}",
978 private_dir
979 )))
980 }
981
982 fn copy_runsc_nofollow(source: &Path, staged: &Path) -> Result<()> {
983 let mut source_file = OpenOptions::new()
984 .read(true)
985 .custom_flags(libc::O_CLOEXEC)
986 .open(source)
987 .map_err(|e| {
988 NucleusError::GVisorError(format!(
989 "Failed to open runsc source {:?}: {}",
990 source, e
991 ))
992 })?;
993
994 let source_meta = source_file.metadata().map_err(|e| {
995 NucleusError::GVisorError(format!("Failed to stat runsc source {:?}: {}", source, e))
996 })?;
997 if !source_meta.file_type().is_file() {
998 return Err(NucleusError::GVisorError(format!(
999 "runsc source {:?} is not a regular file",
1000 source
1001 )));
1002 }
1003
1004 let mut staged_file = OpenOptions::new()
1005 .write(true)
1006 .create_new(true)
1007 .mode(0o500)
1008 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
1009 .open(staged)
1010 .map_err(|e| {
1011 NucleusError::GVisorError(format!(
1012 "Failed to create staged runsc binary {:?}: {}",
1013 staged, e
1014 ))
1015 })?;
1016
1017 io::copy(&mut source_file, &mut staged_file).map_err(|e| {
1018 NucleusError::GVisorError(format!(
1019 "Failed to stage runsc binary from {:?} to {:?}: {}",
1020 source, staged, e
1021 ))
1022 })?;
1023 staged_file
1024 .set_permissions(fs::Permissions::from_mode(0o500))
1025 .map_err(|e| {
1026 NucleusError::GVisorError(format!(
1027 "Failed to secure staged runsc binary {:?}: {}",
1028 staged, e
1029 ))
1030 })?;
1031 staged_file.sync_all().map_err(|e| {
1032 NucleusError::GVisorError(format!(
1033 "Failed to sync staged runsc binary {:?}: {}",
1034 staged, e
1035 ))
1036 })?;
1037
1038 Ok(())
1039 }
1040
1041 fn apply_supervisor_exec_policy(
1042 &self,
1043 allowed_roots: &[PathBuf],
1044 required: bool,
1045 ) -> Result<()> {
1046 let mut landlock = LandlockManager::new();
1047 let applied = landlock.apply_execute_allowlist_policy(allowed_roots, !required)?;
1048 if applied {
1049 info!(
1050 allowed_roots = ?allowed_roots,
1051 "Applied gVisor supervisor execute allowlist"
1052 );
1053 } else if required {
1054 return Err(NucleusError::LandlockError(
1055 "Required gVisor supervisor execute allowlist was not applied".to_string(),
1056 ));
1057 } else {
1058 warn!(
1059 allowed_roots = ?allowed_roots,
1060 "gVisor supervisor execute allowlist unavailable"
1061 );
1062 }
1063 Ok(())
1064 }
1065
1066 fn build_oci_run_args(
1067 &self,
1068 container_id: &str,
1069 bundle: &OciBundle,
1070 runsc_root: &Path,
1071 options: &GVisorOciRunOptions,
1072 debug_dir: Option<&Path>,
1073 ) -> Vec<String> {
1074 let mut args = vec![
1075 self.runsc_path.clone(),
1076 "--root".to_string(),
1077 runsc_root.to_string_lossy().to_string(),
1078 ];
1079
1080 if options.runsc_rootless {
1081 args.push("--rootless".to_string());
1082 }
1083
1084 if options.ignore_cgroups {
1085 args.push("--ignore-cgroups".to_string());
1086 }
1087
1088 if let Some(debug_dir) = debug_dir {
1089 let debug_pattern = debug_dir
1090 .join("%TIMESTAMP%.%COMMAND%.log")
1091 .to_string_lossy()
1092 .to_string();
1093 let panic_log = debug_dir
1094 .join("panic.%TIMESTAMP%.log")
1095 .to_string_lossy()
1096 .to_string();
1097 args.extend([
1098 "--debug".to_string(),
1099 "--alsologtostderr".to_string(),
1100 "--debug-to-user-log".to_string(),
1101 "--debug-log".to_string(),
1102 debug_pattern,
1103 "--panic-log".to_string(),
1104 panic_log,
1105 ]);
1106 }
1107
1108 args.extend([
1109 "--network".to_string(),
1110 options.network_flag().to_string(),
1111 "--platform".to_string(),
1112 options.platform.as_flag().to_string(),
1113 "run".to_string(),
1114 ]);
1115
1116 if let Some(console_socket) = &options.console_socket {
1117 args.extend([
1118 "--console-socket".to_string(),
1119 console_socket.to_string_lossy().to_string(),
1120 ]);
1121 }
1122
1123 args.extend([
1124 "--bundle".to_string(),
1125 bundle.bundle_path().to_string_lossy().to_string(),
1126 container_id.to_string(),
1127 ]);
1128
1129 args
1130 }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135 use super::*;
1136 use crate::oci::OciConfig;
1137 use std::path::{Path, PathBuf};
1138 use std::sync::{Mutex, MutexGuard};
1139
1140 static ENV_LOCK: Mutex<()> = Mutex::new(());
1141
1142 struct EnvLock {
1143 _guard: MutexGuard<'static, ()>,
1144 }
1145
1146 impl EnvLock {
1147 fn acquire() -> Self {
1148 Self {
1149 _guard: ENV_LOCK.lock().unwrap(),
1150 }
1151 }
1152 }
1153
1154 struct EnvVarGuard {
1155 key: &'static str,
1156 previous: Option<std::ffi::OsString>,
1157 }
1158
1159 impl EnvVarGuard {
1160 fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {
1161 let previous = std::env::var_os(key);
1162 std::env::set_var(key, value);
1163 Self { key, previous }
1164 }
1165
1166 fn remove(key: &'static str) -> Self {
1167 let previous = std::env::var_os(key);
1168 std::env::remove_var(key);
1169 Self { key, previous }
1170 }
1171 }
1172
1173 impl Drop for EnvVarGuard {
1174 fn drop(&mut self) {
1175 match &self.previous {
1176 Some(value) => std::env::set_var(self.key, value),
1177 None => std::env::remove_var(self.key),
1178 }
1179 }
1180 }
1181
1182 #[test]
1183 fn test_gvisor_availability() {
1184 let available = GVisorRuntime::is_available();
1187 println!("gVisor available: {}", available);
1188 }
1189
1190 #[test]
1191 fn test_gvisor_new() {
1192 let runtime = GVisorRuntime::new();
1193 if let Ok(rt) = runtime {
1194 println!("Found runsc at: {}", rt.runsc_path);
1195 if let Ok(version) = rt.version() {
1196 println!("runsc version: {}", version);
1197 }
1198 }
1199 }
1200
1201 #[test]
1202 fn test_find_runsc() {
1203 match GVisorRuntime::find_runsc() {
1205 Ok(path) => {
1206 println!("Found runsc at: {}", path);
1207 assert!(!path.is_empty());
1208 }
1209 Err(e) => {
1210 println!("runsc not found (expected if gVisor not installed): {}", e);
1211 }
1212 }
1213 }
1214
1215 #[test]
1216 fn test_validate_runsc_rejects_world_writable() {
1217 let dir = tempfile::tempdir().unwrap();
1218 let fake_runsc = dir.path().join("runsc");
1219 std::fs::write(&fake_runsc, "#!/bin/sh\necho fake").unwrap();
1220 std::fs::set_permissions(&fake_runsc, std::fs::Permissions::from_mode(0o777)).unwrap();
1222
1223 let result = GVisorRuntime::validate_runsc_path(&fake_runsc);
1224 assert!(
1225 result.is_err(),
1226 "validate_runsc_path must reject world-writable binaries"
1227 );
1228 }
1229
1230 #[test]
1231 fn test_validate_runsc_rejects_group_writable() {
1232 let dir = tempfile::tempdir().unwrap();
1233 let fake_runsc = dir.path().join("runsc");
1234 std::fs::write(&fake_runsc, "#!/bin/sh\necho fake").unwrap();
1235 std::fs::set_permissions(&fake_runsc, std::fs::Permissions::from_mode(0o775)).unwrap();
1237
1238 let result = GVisorRuntime::validate_runsc_path(&fake_runsc);
1239 assert!(
1240 result.is_err(),
1241 "validate_runsc_path must reject group-writable binaries"
1242 );
1243 }
1244
1245 #[test]
1246 fn test_runsc_owner_accepts_nix_store_artifact_owner() {
1247 let nix_binary = std::fs::read_dir("/nix/store")
1251 .ok()
1252 .and_then(|mut entries| {
1253 entries.find_map(|e| {
1254 let dir = e.ok()?.path();
1255 let candidate = dir.join("bin/runsc");
1256 if candidate.exists() {
1257 Some(candidate)
1258 } else {
1259 None
1260 }
1261 })
1262 });
1263
1264 let path = match nix_binary {
1265 Some(p) => p,
1266 None => {
1267 eprintln!("skipping: no runsc binary found in /nix/store");
1268 return;
1269 }
1270 };
1271
1272 assert!(GVisorRuntime::is_trusted_runsc_owner(&path, 65534, 1000));
1273 }
1274
1275 #[test]
1276 fn test_exec_environment_uses_hardcoded_path() {
1277 std::env::set_var("PATH", "/tmp/evil-inject/bin:/opt/attacker/sbin");
1282 let rt = GVisorRuntime::with_path("/fake/runsc".to_string());
1283 let tmp = tempfile::tempdir().unwrap();
1284 let env = rt.exec_environment(tmp.path(), false).unwrap();
1285 let path_entry = env
1286 .iter()
1287 .find(|e| e.to_str().is_ok_and(|s| s.starts_with("PATH=")))
1288 .expect("exec_environment must set PATH");
1289 let path_val = path_entry.to_str().unwrap();
1290 assert!(
1291 !path_val.contains("evil-inject") && !path_val.contains("attacker"),
1292 "exec_environment must use hardcoded PATH, not host PATH. Got: {}",
1293 path_val
1294 );
1295 assert_eq!(
1296 path_val,
1297 format!("PATH={RUNSC_SUPERVISOR_PATH}"),
1298 "exec_environment PATH must be the standard hardcoded value"
1299 );
1300 }
1301
1302 #[test]
1303 fn test_exec_environment_can_request_proc_self_exe_reexec() {
1304 let rt = GVisorRuntime::with_path("/fake/runsc".to_string());
1305 let tmp = tempfile::tempdir().unwrap();
1306 let env = rt.exec_environment(tmp.path(), true).unwrap();
1307 let expected = format!("{}=1", RUNSC_REEXEC_VIA_PROC_SELF_EXE_ENV);
1308
1309 assert!(
1310 env.iter()
1311 .any(|entry| entry.to_str().is_ok_and(|entry| entry == expected)),
1312 "runsc env must request /proc/self/exe re-exec when explicitly requested"
1313 );
1314 }
1315
1316 #[test]
1317 fn test_exec_environment_keeps_real_exe_reexec_for_supervisor_policy() {
1318 let rt = GVisorRuntime::with_path("/fake/runsc".to_string());
1319 let tmp = tempfile::tempdir().unwrap();
1320 let env = rt.exec_environment(tmp.path(), false).unwrap();
1321
1322 assert!(
1323 env.iter().all(|entry| entry.to_str().map_or(true, |entry| {
1324 !entry.starts_with(RUNSC_REEXEC_VIA_PROC_SELF_EXE_ENV)
1325 })),
1326 "runsc env must not enable /proc/self/exe re-exec when supervisor policy is active"
1327 );
1328 }
1329
1330 #[test]
1331 fn test_builtin_rootless_args_pass_runsc_rootless() {
1332 let rt = GVisorRuntime::with_path("/nix/store/fake-runsc/bin/runsc".to_string());
1333 let tmp = tempfile::tempdir().unwrap();
1334 let bundle = OciBundle::new(
1335 tmp.path().join("bundle"),
1336 OciConfig::new(vec!["/bin/true".to_string()], None),
1337 );
1338
1339 let args = rt.build_oci_run_args(
1340 "container-id",
1341 &bundle,
1342 tmp.path(),
1343 &GVisorOciRunOptions {
1344 network_mode: GVisorNetworkMode::Host,
1345 ignore_cgroups: true,
1346 runsc_rootless: true,
1347 stage_runsc_binary: false,
1348 require_supervisor_exec_policy: false,
1349 platform: GVisorPlatform::Systrap,
1350 console_socket: None,
1351 },
1352 None,
1353 );
1354
1355 assert!(args.iter().any(|arg| arg == "--rootless"));
1356 assert!(args.iter().any(|arg| arg == "--ignore-cgroups"));
1357 }
1358
1359 #[test]
1360 fn test_runsc_rootless_does_not_force_supervisor_exec_policy() {
1361 let source = include_str!("gvisor.rs");
1362 let start = source.find("pub fn exec_with_oci_bundle_options").unwrap();
1363 let end = source[start..]
1364 .find("pub fn exec_with_oci_bundle_network")
1365 .map(|offset| start + offset)
1366 .unwrap();
1367 let fn_body = &source[start..end];
1368
1369 assert!(
1370 fn_body.contains("if options.require_supervisor_exec_policy"),
1371 "supervisor execute policy must be selected explicitly"
1372 );
1373 assert!(
1374 !fn_body.contains("|| options.runsc_rootless"),
1375 "runsc --rootless must not force the host-side supervisor execute policy"
1376 );
1377 assert!(
1378 fn_body.contains("options.runsc_rootless && !options.require_supervisor_exec_policy"),
1379 "rootless runsc must use /proc/self/exe helper re-exec only without supervisor policy"
1380 );
1381 }
1382
1383 #[test]
1384 fn test_rootless_oci_args_do_not_pass_runsc_rootless() {
1385 let rt = GVisorRuntime::with_path("/nix/store/fake-runsc/bin/runsc".to_string());
1386 let tmp = tempfile::tempdir().unwrap();
1387 let bundle = OciBundle::new(
1388 tmp.path().join("bundle"),
1389 OciConfig::new(vec!["/bin/true".to_string()], None),
1390 );
1391
1392 let args = rt.build_oci_run_args(
1393 "container-id",
1394 &bundle,
1395 tmp.path(),
1396 &GVisorOciRunOptions {
1397 network_mode: GVisorNetworkMode::Host,
1398 ignore_cgroups: true,
1399 runsc_rootless: false,
1400 stage_runsc_binary: false,
1401 require_supervisor_exec_policy: false,
1402 platform: GVisorPlatform::Systrap,
1403 console_socket: None,
1404 },
1405 None,
1406 );
1407
1408 assert!(!args.iter().any(|arg| arg == "--rootless"));
1409 assert!(args.iter().any(|arg| arg == "--ignore-cgroups"));
1410 }
1411
1412 #[test]
1413 fn test_runsc_args_pass_console_socket_after_run_subcommand() {
1414 let rt = GVisorRuntime::with_path("/nix/store/fake-runsc/bin/runsc".to_string());
1415 let tmp = tempfile::tempdir().unwrap();
1416 let console_socket = tmp.path().join("console.sock");
1417 let bundle = OciBundle::new(
1418 tmp.path().join("bundle"),
1419 OciConfig::new(vec!["/bin/true".to_string()], None),
1420 );
1421
1422 let args = rt.build_oci_run_args(
1423 "container-id",
1424 &bundle,
1425 tmp.path(),
1426 &GVisorOciRunOptions {
1427 network_mode: GVisorNetworkMode::Host,
1428 ignore_cgroups: true,
1429 runsc_rootless: false,
1430 stage_runsc_binary: false,
1431 require_supervisor_exec_policy: false,
1432 platform: GVisorPlatform::Systrap,
1433 console_socket: Some(console_socket.clone()),
1434 },
1435 None,
1436 );
1437
1438 let run_index = args.iter().position(|arg| arg == "run").unwrap();
1439 let socket_index = args
1440 .iter()
1441 .position(|arg| arg == "--console-socket")
1442 .unwrap();
1443 assert!(socket_index > run_index);
1444 assert_eq!(
1445 args.get(socket_index + 1),
1446 Some(&console_socket.to_string_lossy().to_string())
1447 );
1448 }
1449
1450 #[test]
1451 fn test_runsc_debug_dir_adds_debug_flags_before_subcommand() {
1452 let rt = GVisorRuntime::with_path("/nix/store/fake-runsc/bin/runsc".to_string());
1453 let tmp = tempfile::tempdir().unwrap();
1454 let debug_dir = tmp.path().join("runsc-debug");
1455 let bundle = OciBundle::new(
1456 tmp.path().join("bundle"),
1457 OciConfig::new(vec!["/bin/true".to_string()], None),
1458 );
1459
1460 let args = rt.build_oci_run_args(
1461 "container-id",
1462 &bundle,
1463 tmp.path(),
1464 &GVisorOciRunOptions {
1465 network_mode: GVisorNetworkMode::Host,
1466 ignore_cgroups: true,
1467 runsc_rootless: true,
1468 stage_runsc_binary: false,
1469 require_supervisor_exec_policy: false,
1470 platform: GVisorPlatform::Ptrace,
1471 console_socket: None,
1472 },
1473 Some(&debug_dir),
1474 );
1475
1476 let run_index = args.iter().position(|arg| arg == "run").unwrap();
1477 let debug_index = args.iter().position(|arg| arg == "--debug").unwrap();
1478 assert!(
1479 debug_index < run_index,
1480 "runsc debug flags must be top-level flags before the subcommand"
1481 );
1482 assert!(args.iter().any(|arg| arg == "--alsologtostderr"));
1483 assert!(args.iter().any(|arg| arg == "--debug-to-user-log"));
1484 assert!(args.iter().any(|arg| arg == "--debug-log"));
1485 assert!(args.iter().any(|arg| arg.contains("%COMMAND%.log")));
1486 assert!(args.iter().any(|arg| arg.contains("panic.%TIMESTAMP%.log")));
1487 }
1488
1489 #[test]
1490 fn test_non_nix_runsc_is_staged_for_supervisor_exec_policy() {
1491 let tmp = tempfile::tempdir().unwrap();
1492 let fake_runsc = tmp.path().join("runsc-source");
1493 std::fs::write(&fake_runsc, b"fake-runsc").unwrap();
1494 std::fs::set_permissions(&fake_runsc, std::fs::Permissions::from_mode(0o500)).unwrap();
1495
1496 let rt = GVisorRuntime::with_path(fake_runsc.to_string_lossy().to_string());
1497 let runsc_root = tmp.path().join("runsc-root");
1498 let (program, allow_roots) = rt
1499 .prepare_supervisor_runsc_program(&runsc_root, false)
1500 .unwrap();
1501
1502 assert!(program.starts_with(runsc_root.join("exec-allow")));
1503 assert_eq!(allow_roots, vec![runsc_root.join("exec-allow")]);
1504 assert_eq!(std::fs::read(&program).unwrap(), b"fake-runsc");
1505 let mode = std::fs::metadata(&program).unwrap().permissions().mode() & 0o777;
1506 assert_eq!(mode, 0o500);
1507 }
1508
1509 #[test]
1510 fn test_nix_store_runsc_uses_package_bin_exec_root() {
1511 let program = Path::new("/nix/store/hash-gvisor-20251110.0/bin/.runsc-wrapped");
1512
1513 assert_eq!(
1514 GVisorRuntime::nix_store_runsc_exec_root(program),
1515 Some(PathBuf::from("/nix/store/hash-gvisor-20251110.0/bin"))
1516 );
1517 }
1518
1519 #[test]
1520 fn test_non_nix_runsc_has_no_store_exec_root() {
1521 let program = Path::new("/tmp/nucleus/runsc");
1522
1523 assert_eq!(GVisorRuntime::nix_store_runsc_exec_root(program), None);
1524 }
1525
1526 #[test]
1527 fn test_supervisor_exec_allow_roots_use_only_runsc_exec_root() {
1528 let roots = GVisorRuntime::supervisor_exec_allow_roots(PathBuf::from(NIX_STORE_EXEC_ROOT));
1529
1530 assert_eq!(roots, vec![PathBuf::from(NIX_STORE_EXEC_ROOT)]);
1531 }
1532
1533 #[test]
1534 fn test_rootless_userns_helper_exec_roots_include_default_wrappers() {
1535 let _env_lock = EnvLock::acquire();
1536 let _helper_roots = EnvVarGuard::remove(ROOTLESS_USERNS_HELPER_EXEC_ROOTS_ENV);
1537 let roots = GVisorRuntime::rootless_userns_helper_exec_roots();
1538
1539 assert_eq!(
1540 roots,
1541 vec![
1542 PathBuf::from("/run/wrappers/bin/newuidmap"),
1543 PathBuf::from("/run/wrappers/bin/newgidmap"),
1544 ]
1545 );
1546 }
1547
1548 #[test]
1549 fn test_rootless_userns_helper_exec_roots_include_configured_absolute_roots() {
1550 let _env_lock = EnvLock::acquire();
1551 let configured_roots = std::env::join_paths([
1552 PathBuf::from("/nix/store/shadow/bin/newuidmap"),
1553 PathBuf::from("relative/newgidmap"),
1554 PathBuf::from("/nix/store/shadow/bin/newgidmap"),
1555 ])
1556 .unwrap();
1557 let _helper_roots =
1558 EnvVarGuard::set(ROOTLESS_USERNS_HELPER_EXEC_ROOTS_ENV, configured_roots);
1559
1560 let roots = GVisorRuntime::rootless_userns_helper_exec_roots();
1561
1562 assert_eq!(
1563 roots,
1564 vec![
1565 PathBuf::from("/run/wrappers/bin/newuidmap"),
1566 PathBuf::from("/run/wrappers/bin/newgidmap"),
1567 PathBuf::from("/nix/store/shadow/bin/newuidmap"),
1568 PathBuf::from("/nix/store/shadow/bin/newgidmap"),
1569 ]
1570 );
1571 }
1572
1573 #[test]
1574 fn test_rootless_supervisor_policy_allows_userns_helpers() {
1575 let source = include_str!("gvisor.rs");
1576 let start = source.find("pub fn exec_with_oci_bundle_options").unwrap();
1577 let end = source[start..]
1578 .find("pub fn exec_with_oci_bundle_network")
1579 .map(|offset| start + offset)
1580 .unwrap();
1581 let fn_body = &source[start..end];
1582 let helper_roots = fn_body
1583 .find("exec_allow_roots.extend(Self::rootless_userns_helper_exec_roots())")
1584 .unwrap();
1585 let policy = fn_body.find("self.apply_supervisor_exec_policy").unwrap();
1586
1587 assert!(
1588 helper_roots < policy,
1589 "rootless runsc must allow newuidmap/newgidmap before installing Landlock"
1590 );
1591 }
1592
1593 #[test]
1594 fn test_runsc_root_uses_hardened_artifact_dir_not_bundle_parent() {
1595 let _env_lock = EnvLock::acquire();
1596 let tmp = tempfile::tempdir().unwrap();
1597 let artifact_base = tmp.path().join("gvisor-artifacts");
1598 let _artifact_base = EnvVarGuard::set("NUCLEUS_GVISOR_ARTIFACT_BASE", &artifact_base);
1599 let _runtime = EnvVarGuard::remove("XDG_RUNTIME_DIR");
1600
1601 let bundle_parent = tmp.path().join("shared");
1602 std::fs::create_dir_all(&bundle_parent).unwrap();
1603 std::fs::set_permissions(&bundle_parent, std::fs::Permissions::from_mode(0o777)).unwrap();
1604 let bundle = OciBundle::new(
1605 bundle_parent.join("bundle"),
1606 OciConfig::new(vec!["/bin/true".to_string()], None),
1607 );
1608
1609 let runsc_root = GVisorRuntime::secure_runsc_root("container-id").unwrap();
1610
1611 assert!(runsc_root
1612 .starts_with(artifact_base.join(GVisorRuntime::runsc_state_component("container-id"))));
1613 assert!(
1614 !runsc_root.starts_with(bundle.bundle_path().parent().unwrap()),
1615 "runsc root must not be derived from a custom bundle parent"
1616 );
1617 }
1618
1619 #[test]
1620 fn test_runsc_staging_rejects_symlink_exec_allow_dir() {
1621 let tmp = tempfile::tempdir().unwrap();
1622 let fake_runsc = tmp.path().join("runsc-source");
1623 std::fs::write(&fake_runsc, b"fake-runsc").unwrap();
1624 std::fs::set_permissions(&fake_runsc, std::fs::Permissions::from_mode(0o500)).unwrap();
1625
1626 let runsc_root = tmp.path().join("runsc-root");
1627 std::fs::create_dir(&runsc_root).unwrap();
1628 std::fs::set_permissions(&runsc_root, std::fs::Permissions::from_mode(0o700)).unwrap();
1629 let victim_dir = tmp.path().join("victim");
1630 std::fs::create_dir(&victim_dir).unwrap();
1631 std::os::unix::fs::symlink(&victim_dir, runsc_root.join("exec-allow")).unwrap();
1632
1633 let rt = GVisorRuntime::with_path(fake_runsc.to_string_lossy().to_string());
1634 let err = rt
1635 .prepare_supervisor_runsc_program(&runsc_root, false)
1636 .unwrap_err()
1637 .to_string();
1638
1639 assert!(
1640 err.contains("Refusing symlink private runsc exec directory"),
1641 "unexpected error: {}",
1642 err
1643 );
1644 assert!(
1645 !victim_dir.join("runsc").exists(),
1646 "staging must not follow the exec-allow symlink"
1647 );
1648 }
1649
1650 #[test]
1651 fn test_runsc_owner_rejects_untrusted_non_store_owner() {
1652 assert!(!GVisorRuntime::is_trusted_runsc_owner(
1653 Path::new("/tmp/runsc"),
1654 4242,
1655 1000
1656 ));
1657 }
1658}