1use std::ffi::{OsStr, OsString};
42use std::path::Path;
43use std::time::Instant;
44
45use async_trait::async_trait;
46use tokio::process::Command;
47use tracing::{Span, instrument};
48
49use crate::backend::{BackendCapabilities, EnforcedLimits, SandboxBackend};
50use crate::child_io::{collect_output, note_truncation, truncate_utf8};
51use crate::error::SandboxError;
52use crate::sandbox::{AccessMode, AllowedPath, SandboxEnforcer, SandboxPolicy};
53use crate::types::{ExecRequest, ExecResult, Language};
54
55const MAX_OUTPUT_BYTES: usize = 1_024 * 1_024;
57
58const NON_WINDOWS_TOOLCHAIN_ENV_KEYS: &[&str] = &[
60 "PATH",
61 "DEVELOPER_DIR",
62 "SDKROOT",
63 "HOME",
64 "TMPDIR",
65 "RUSTUP_HOME",
66 "CARGO_HOME",
67 "RUSTUP_TOOLCHAIN",
68];
69
70const WINDOWS_TOOLCHAIN_ENV_KEYS: &[&str] = &[
76 "PATH",
77 "LIB",
78 "LIBPATH",
79 "INCLUDE",
80 "SystemRoot",
81 "TEMP",
82 "TMP",
83 "USERPROFILE",
84 "RUSTUP_HOME",
85 "RUSTUP_TOOLCHAIN",
86];
87
88#[derive(Debug, Clone)]
104pub struct ProcessConfig {
105 pub rustc_path: String,
107 pub python_path: String,
109 pub node_path: String,
111 pub max_output_bytes: usize,
117}
118
119impl Default for ProcessConfig {
120 fn default() -> Self {
121 Self {
122 rustc_path: "rustc".to_string(),
123 python_path: "python3".to_string(),
124 node_path: "node".to_string(),
125 max_output_bytes: MAX_OUTPUT_BYTES,
126 }
127 }
128}
129
130pub struct ProcessBackend {
165 config: ProcessConfig,
166 enforcer: Option<Box<dyn SandboxEnforcer>>,
167 policy: Option<SandboxPolicy>,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum IsolationClass {
176 SubprocessOnly,
181 OsEnforced,
184}
185
186fn resolve_program(program: &OsStr) -> Option<std::path::PathBuf> {
192 let as_path = std::path::Path::new(program);
193 if as_path.components().count() > 1 {
194 return None;
195 }
196
197 let path_var = std::env::var_os("PATH")?;
198 std::env::split_paths(&path_var).find_map(|dir| {
199 let candidate = dir.join(program);
200 candidate.is_file().then_some(candidate)
201 })
202}
203
204impl ProcessBackend {
205 pub fn new(config: ProcessConfig) -> Self {
210 Self { config, enforcer: None, policy: None }
211 }
212
213 pub fn isolation(&self) -> IsolationClass {
218 match (self.enforcer.is_some(), self.policy.is_some()) {
219 (true, true) => IsolationClass::OsEnforced,
220 _ => IsolationClass::SubprocessOnly,
221 }
222 }
223
224 pub fn with_sandbox(
238 config: ProcessConfig,
239 enforcer: Box<dyn SandboxEnforcer>,
240 policy: SandboxPolicy,
241 ) -> Self {
242 Self { config, enforcer: Some(enforcer), policy: Some(policy) }
243 }
244}
245
246impl Default for ProcessBackend {
247 fn default() -> Self {
248 Self::new(ProcessConfig::default())
249 }
250}
251
252impl std::fmt::Debug for ProcessBackend {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 f.debug_struct("ProcessBackend")
256 .field("config", &self.config)
257 .field("enforcer", &self.enforcer.as_ref().map(|e| e.name()))
258 .field("policy", &self.policy)
259 .finish()
260 }
261}
262
263#[async_trait]
264impl SandboxBackend for ProcessBackend {
265 fn name(&self) -> &str {
266 "process"
267 }
268
269 fn capabilities(&self) -> BackendCapabilities {
270 let has_enforcer = self.enforcer.is_some();
271 let denies_network = self.policy.as_ref().is_some_and(|p| !p.allow_network);
272
273 BackendCapabilities {
274 supported_languages: vec![
275 Language::Rust,
276 Language::Python,
277 Language::JavaScript,
278 Language::TypeScript,
279 Language::Command,
280 ],
281 isolation_class: if has_enforcer {
282 "process+sandbox".to_string()
283 } else {
284 "process".to_string()
285 },
286 enforced_limits: EnforcedLimits {
287 timeout: true,
288 memory: false,
289 network_isolation: has_enforcer && denies_network,
290 filesystem_write_isolation: has_enforcer,
291 filesystem_read_isolation: has_enforcer
295 && cfg!(any(target_os = "linux", target_os = "macos")),
296 environment_isolation: true,
297 },
298 }
299 }
300
301 #[instrument(
302 skip_all,
303 fields(
304 backend = "process",
305 language = %request.language,
306 exit_code,
307 duration_ms,
308 )
309 )]
310 async fn execute(&self, request: ExecRequest) -> Result<ExecResult, SandboxError> {
311 if let Some(limit) = request.memory_limit_mb {
312 tracing::debug!(
313 memory_limit_mb = limit,
314 "memory limit not enforced by process backend"
315 );
316 }
317
318 match request.language {
319 Language::Rust => self.execute_rust(&request).await,
320 Language::Python => self.execute_python(&request).await,
321 Language::JavaScript | Language::TypeScript => self.execute_javascript(&request).await,
322 Language::Command => self.execute_command(&request).await,
323 Language::Wasm => Err(SandboxError::InvalidRequest(
324 "Wasm execution is not supported by ProcessBackend. Use WasmBackend instead."
325 .to_string(),
326 )),
327 }
328 }
329}
330
331impl ProcessBackend {
332 async fn execute_rust(&self, request: &ExecRequest) -> Result<ExecResult, SandboxError> {
334 let dir = tempfile::tempdir()?;
335 let src_path = dir.path().join("main.rs");
336 let bin_path = dir.path().join("main");
337
338 std::fs::write(&src_path, &request.code)?;
339
340 let toolchain_env = Self::toolchain_env();
346 #[cfg(windows)]
347 let has_msvc_library_path =
348 toolchain_env.iter().any(|(key, _)| key.eq_ignore_ascii_case("LIB"));
349
350 let compile_result = {
351 let mut cmd = Command::new(&self.config.rustc_path);
352 #[cfg(windows)]
357 cmd.arg("-Clinker=rust-lld");
358 cmd.arg(&src_path).arg("-o").arg(&bin_path);
359 self.run_command_with_env(cmd, request, &toolchain_env, Some(dir.path())).await?
360 };
361
362 #[cfg(windows)]
363 let compile_result = {
364 let mut result = compile_result;
365 if result.exit_code != 0 && !has_msvc_library_path {
366 result.stderr.push_str(
367 "\nWindows Rust linking requires the MSVC Build Tools and Windows SDK. \
368 Install the `Desktop development with C++` workload; ProcessBackend could \
369 not discover its LIB paths from this host.",
370 );
371 }
372 result
373 };
374
375 if compile_result.exit_code != 0 {
376 Span::current().record("exit_code", compile_result.exit_code);
377 Span::current().record("duration_ms", compile_result.duration.as_millis() as u64);
378 return Ok(compile_result);
379 }
380
381 self.run_command(Command::new(&bin_path), request, Some(dir.path())).await
383 }
384
385 async fn execute_python(&self, request: &ExecRequest) -> Result<ExecResult, SandboxError> {
387 let dir = tempfile::tempdir()?;
388 let src_path = dir.path().join("script.py");
389 std::fs::write(&src_path, &request.code)?;
390
391 let mut cmd = Command::new(&self.config.python_path);
392 cmd.arg(&src_path);
393 self.run_command(cmd, request, Some(dir.path())).await
394 }
395
396 async fn execute_javascript(&self, request: &ExecRequest) -> Result<ExecResult, SandboxError> {
398 let dir = tempfile::tempdir()?;
399 let src_path = dir.path().join("script.js");
400 std::fs::write(&src_path, &request.code)?;
401
402 let mut cmd = Command::new(&self.config.node_path);
403 cmd.arg(&src_path);
404 self.run_command(cmd, request, Some(dir.path())).await
405 }
406
407 async fn execute_command(&self, request: &ExecRequest) -> Result<ExecResult, SandboxError> {
409 #[cfg(windows)]
410 let cmd = {
411 use std::os::windows::process::CommandExt;
412
413 let mut c = Command::new("cmd");
414 c.arg("/D").arg("/C");
415 c.as_std_mut().raw_arg(&request.code);
420 c
421 };
422 #[cfg(not(windows))]
423 let cmd = {
424 let mut c = Command::new("sh");
425 c.arg("-c").arg(&request.code);
426 c
427 };
428 self.run_command(cmd, request, None).await
429 }
430
431 async fn run_command(
436 &self,
437 cmd: Command,
438 request: &ExecRequest,
439 scratch_dir: Option<&Path>,
440 ) -> Result<ExecResult, SandboxError> {
441 self.run_command_with_env(cmd, request, &[], scratch_dir).await
442 }
443
444 pub fn toolchain_env() -> Vec<(String, OsString)> {
467 let keys =
474 if cfg!(windows) { WINDOWS_TOOLCHAIN_ENV_KEYS } else { NON_WINDOWS_TOOLCHAIN_ENV_KEYS };
475
476 let environment: Vec<(String, OsString)> = keys
477 .iter()
478 .filter_map(|key| std::env::var_os(key).map(|value| ((*key).to_string(), value)))
479 .collect();
480
481 #[cfg(windows)]
482 let mut environment = environment;
483
484 #[cfg(windows)]
485 if !environment.iter().any(|(key, _)| key.eq_ignore_ascii_case("LIB"))
486 && let Some(linker) = find_msvc_tools::find(std::env::consts::ARCH, "link.exe")
487 {
488 for (key, value) in linker.get_envs() {
489 let Some(value) = value else {
490 continue;
491 };
492 let Some(allowed_key) = WINDOWS_TOOLCHAIN_ENV_KEYS
493 .iter()
494 .find(|allowed| key.eq_ignore_ascii_case(OsStr::new(allowed)))
495 else {
496 continue;
497 };
498 if !environment
499 .iter()
500 .any(|(existing, _)| existing.eq_ignore_ascii_case(allowed_key))
501 {
502 environment.push(((*allowed_key).to_string(), value.to_os_string()));
503 }
504 }
505 }
506
507 environment
508 }
509
510 async fn run_command_with_env(
516 &self,
517 cmd: Command,
518 request: &ExecRequest,
519 extra_env: &[(String, OsString)],
520 scratch_dir: Option<&Path>,
521 ) -> Result<ExecResult, SandboxError> {
522 let mut owned_scratch: Option<tempfile::TempDir> = None;
524
525 let mut cmd = if let (Some(enforcer), Some(policy)) = (&self.enforcer, &self.policy) {
530 let scratch = match scratch_dir {
531 Some(dir) => dir.to_path_buf(),
532 None => owned_scratch.insert(tempfile::tempdir()?).path().to_path_buf(),
533 };
534 let mut policy = policy.clone();
538 policy
539 .allowed_paths
540 .push(AllowedPath { path: scratch.clone(), mode: AccessMode::ReadWrite });
541
542 let std_cmd = cmd.as_std();
543 let args: Vec<OsString> = std_cmd.get_args().map(OsStr::to_owned).collect();
544 let wrapped = enforcer.wrap_command(std_cmd.get_program(), &args, &policy)?;
545
546 let program = resolve_program(&wrapped.program)
550 .map_or(wrapped.program, std::path::PathBuf::into_os_string);
551 let mut wrapped_cmd = Command::new(program);
552 wrapped_cmd.args(&wrapped.args).current_dir(&scratch);
553 enforcer.configure_command(&mut wrapped_cmd, &policy)?;
554 wrapped_cmd
555 } else {
556 match resolve_program(cmd.as_std().get_program()) {
557 Some(resolved) => {
558 let args: Vec<OsString> =
559 cmd.as_std().get_args().map(OsStr::to_owned).collect();
560 let mut resolved_cmd = Command::new(resolved);
561 resolved_cmd.args(&args);
562 resolved_cmd
563 }
564 None => cmd,
565 }
566 };
567
568 cmd.env_clear();
572 for (k, v) in extra_env {
573 cmd.env(k, v);
574 }
575 if let Some(policy) = &self.policy {
576 for (k, v) in &policy.env {
577 cmd.env(k, v);
578 }
579 }
580 for (k, v) in &request.env {
581 cmd.env(k, v);
582 }
583 cmd.kill_on_drop(true);
584
585 #[cfg(unix)]
591 {
592 use std::os::unix::process::CommandExt;
593 cmd.as_std_mut().process_group(0);
594 }
595
596 cmd.stdout(std::process::Stdio::piped());
597 cmd.stderr(std::process::Stdio::piped());
598
599 if request.stdin.is_some() {
600 cmd.stdin(std::process::Stdio::piped());
601 } else {
602 cmd.stdin(std::process::Stdio::null());
603 }
604
605 let start = Instant::now();
606 let child = cmd.spawn()?;
607
608 let cap = self.config.max_output_bytes;
611 let stdin = request.stdin.as_ref().map(|input| input.as_bytes().to_vec());
612 let output = collect_output(child, stdin, request.timeout, cap).await;
613 let duration = start.elapsed();
614
615 match output {
616 Ok(Some(captured)) => {
617 let exit_code = captured.status.code().unwrap_or(-1);
618 if captured.stdout_truncated || captured.stderr_truncated {
619 tracing::warn!(
620 max_output_bytes = cap,
621 stdout.truncated = captured.stdout_truncated,
622 stderr.truncated = captured.stderr_truncated,
623 "sandbox output exceeded the cap and was truncated"
624 );
625 }
626 let stdout =
627 note_truncation(truncate_utf8(captured.stdout, cap), captured.stdout_truncated);
628 let stderr =
629 note_truncation(truncate_utf8(captured.stderr, cap), captured.stderr_truncated);
630
631 Span::current().record("exit_code", exit_code);
632 Span::current().record("duration_ms", duration.as_millis() as u64);
633
634 Ok(ExecResult { stdout, stderr, exit_code, duration })
635 }
636 Ok(None) => {
637 Span::current().record("duration_ms", duration.as_millis() as u64);
638 Err(SandboxError::Timeout { timeout: request.timeout })
639 }
640 Err(e) => {
641 Err(SandboxError::ExecutionFailed(format!("failed to wait for child process: {e}")))
642 }
643 }
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650 use std::collections::HashMap;
651 use std::time::Duration;
652
653 fn make_request(language: Language, code: &str) -> ExecRequest {
654 let mut env = HashMap::new();
655 if let Ok(path) = std::env::var("PATH") {
658 env.insert("PATH".to_string(), path);
659 }
660 if let Ok(sr) = std::env::var("SYSTEMROOT") {
662 env.insert("SYSTEMROOT".to_string(), sr);
663 }
664 ExecRequest {
665 language,
666 code: code.to_string(),
667 stdin: None,
668 timeout: Duration::from_secs(30),
669 memory_limit_mb: None,
670 env,
671 }
672 }
673
674 #[tokio::test]
675 async fn test_python_execution() {
676 let backend = ProcessBackend::default();
677 let request = make_request(Language::Python, "print('hello')");
678 let result = backend.execute(request).await.unwrap();
679 assert!(result.stdout.contains("hello"), "stdout: {}", result.stdout);
680 assert_eq!(result.exit_code, 0);
681 }
682
683 #[tokio::test]
684 async fn test_javascript_execution() {
685 if std::process::Command::new("node").arg("--version").output().is_err() {
687 eprintln!("skipping test_javascript_execution: node not found");
688 return;
689 }
690 let backend = ProcessBackend::default();
691 let request = make_request(Language::JavaScript, "console.log('hello')");
692 let result = backend.execute(request).await.unwrap();
693 assert!(result.stdout.contains("hello"), "stdout: {}", result.stdout);
694 assert_eq!(result.exit_code, 0);
695 }
696
697 #[tokio::test]
698 async fn test_command_execution() {
699 let backend = ProcessBackend::default();
700 let request = make_request(Language::Command, "echo hello");
701 let result = backend.execute(request).await.unwrap();
702 assert!(result.stdout.contains("hello"), "stdout: {}", result.stdout);
703 assert_eq!(result.exit_code, 0);
704 }
705
706 #[tokio::test]
707 #[cfg(windows)]
708 async fn test_command_supports_quoted_script_paths() {
709 let directory = tempfile::tempdir().unwrap();
710 let script = directory.path().join("quoted helper.cmd");
711 std::fs::write(&script, "@echo quoted-path-ok\r\n").unwrap();
712
713 let backend = ProcessBackend::default();
714 let request = make_request(Language::Command, &format!("\"{}\"", script.display()));
715 let result = backend.execute(request).await.unwrap();
716
717 assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
718 assert!(result.stdout.contains("quoted-path-ok"), "stdout: {}", result.stdout);
719 }
720
721 #[tokio::test]
722 async fn test_timeout_enforcement() {
723 let backend = ProcessBackend::default();
724 let code =
725 if cfg!(windows) { "ping -n 11 127.0.0.1".to_string() } else { "sleep 10".to_string() };
726 let mut request = make_request(Language::Command, &code);
727 request.timeout = Duration::from_secs(1);
728 let result = backend.execute(request).await;
729 assert!(
730 matches!(result, Err(SandboxError::Timeout { .. })),
731 "expected Timeout, got: {result:?}"
732 );
733 }
734
735 #[tokio::test]
736 #[cfg(unix)]
737 async fn test_timeout_terminates_background_descendants() {
738 let backend = ProcessBackend::default();
739 let directory = tempfile::tempdir().unwrap();
740 let marker = directory.path().join("escaped-child");
741 let escaped_marker = marker.to_string_lossy().replace('\'', "'\\''");
742 let code = format!("(sleep 1; touch '{escaped_marker}') & wait");
743 let mut request = make_request(Language::Command, &code);
744 request.timeout = Duration::from_millis(100);
745
746 let result = backend.execute(request).await;
747 assert!(matches!(result, Err(SandboxError::Timeout { .. })));
748 tokio::time::sleep(Duration::from_millis(1_200)).await;
749 assert!(!marker.exists(), "a background descendant survived the execution timeout");
750 }
751
752 #[tokio::test]
754 #[cfg(unix)]
755 async fn test_normal_exit_terminates_background_descendants() {
756 let backend = ProcessBackend::default();
757 let directory = tempfile::tempdir().unwrap();
758 let marker = directory.path().join("escaped-child");
759 let escaped_marker = marker.to_string_lossy().replace('\'', "'\\''");
760 let code = format!(
761 "nohup sh -c 'sleep 1; touch \"$0\"' '{escaped_marker}' >/dev/null 2>&1 & echo done"
762 );
763
764 let result = backend.execute(make_request(Language::Command, &code)).await.unwrap();
765 assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
766 assert_eq!(result.stdout.trim(), "done");
767
768 tokio::time::sleep(Duration::from_millis(1_500)).await;
769 assert!(!marker.exists(), "a background descendant survived a normal exit");
770 }
771
772 #[tokio::test]
777 #[cfg(unix)]
778 async fn test_large_stdin_does_not_deadlock_against_output() {
779 let backend = ProcessBackend::default();
780 let mut request = make_request(Language::Command, "head -c 262144 /dev/zero; wc -c");
781 request.stdin = Some("s".repeat(512 * 1_024));
782
783 let result = tokio::time::timeout(Duration::from_secs(60), backend.execute(request))
784 .await
785 .expect("the execution deadlocked")
786 .expect("the execution completes");
787
788 assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
789 assert!(
790 result.stdout.trim_end().ends_with("524288"),
791 "stdin was not fully delivered: {:?}",
792 &result.stdout[result.stdout.len().saturating_sub(32)..]
793 );
794 }
795
796 #[tokio::test]
798 #[cfg(unix)]
799 async fn test_unread_stdin_is_bounded_by_the_timeout() {
800 let backend = ProcessBackend::default();
801 let mut request = make_request(Language::Command, "sleep 30");
802 request.stdin = Some("s".repeat(4 * 1_024 * 1_024));
803 request.timeout = Duration::from_millis(300);
804
805 let started = std::time::Instant::now();
806 let result = backend.execute(request).await;
807
808 assert!(matches!(result, Err(SandboxError::Timeout { .. })), "got: {result:?}");
809 assert!(started.elapsed() < Duration::from_secs(10), "took {:?}", started.elapsed());
810 }
811
812 #[tokio::test]
813 #[cfg(not(windows))]
814 async fn test_environment_isolation() {
815 let backend = ProcessBackend::default();
816 let mut env = HashMap::new();
817 env.insert("MY_TEST_VAR".to_string(), "test_value".to_string());
818 let request = ExecRequest {
819 language: Language::Command,
820 code: "/usr/bin/env".to_string(),
822 stdin: None,
823 timeout: Duration::from_secs(10),
824 memory_limit_mb: None,
825 env,
826 };
827 let result = backend.execute(request).await.unwrap();
828 assert!(result.stdout.contains("MY_TEST_VAR=test_value"), "stdout: {}", result.stdout);
830 assert!(
832 !result.stdout.contains("HOME="),
833 "HOME should not be inherited: {}",
834 result.stdout
835 );
836 }
837
838 #[tokio::test]
839 #[cfg(windows)]
840 async fn test_environment_isolation() {
841 let backend = ProcessBackend::default();
842 let mut env = HashMap::new();
843 env.insert("MY_TEST_VAR".to_string(), "test_value".to_string());
844 let request = ExecRequest {
845 language: Language::Command,
846 code: "set MY_TEST_VAR".to_string(),
847 stdin: None,
848 timeout: Duration::from_secs(10),
849 memory_limit_mb: None,
850 env,
851 };
852 let result = backend.execute(request).await.unwrap();
853 assert!(result.stdout.contains("MY_TEST_VAR=test_value"), "stdout: {}", result.stdout);
854 }
855
856 #[tokio::test]
857 async fn test_nonzero_exit_code() {
858 let backend = ProcessBackend::default();
859 let code = if cfg!(windows) { "exit /b 42" } else { "exit 42" };
860 let request = make_request(Language::Command, code);
861 let result = backend.execute(request).await.unwrap();
862 assert_eq!(result.exit_code, 42);
863 }
864
865 #[tokio::test]
866 async fn test_wasm_returns_invalid_request() {
867 let backend = ProcessBackend::default();
868 let request = make_request(Language::Wasm, "");
869 let result = backend.execute(request).await;
870 assert!(
871 matches!(result, Err(SandboxError::InvalidRequest(_))),
872 "expected InvalidRequest, got: {result:?}"
873 );
874 }
875
876 #[test]
877 fn test_capabilities() {
878 let backend = ProcessBackend::default();
879 let caps = backend.capabilities();
880 assert_eq!(caps.isolation_class, "process");
881 assert!(caps.enforced_limits.timeout);
882 assert!(caps.enforced_limits.environment_isolation);
883 assert!(!caps.enforced_limits.memory);
884 assert!(!caps.enforced_limits.network_isolation);
885 assert!(!caps.enforced_limits.filesystem_write_isolation);
886 assert!(!caps.enforced_limits.filesystem_read_isolation);
887 assert!(caps.supported_languages.contains(&Language::Rust));
888 assert!(caps.supported_languages.contains(&Language::Python));
889 assert!(caps.supported_languages.contains(&Language::JavaScript));
890 assert!(caps.supported_languages.contains(&Language::TypeScript));
891 assert!(caps.supported_languages.contains(&Language::Command));
892 assert!(!caps.supported_languages.contains(&Language::Wasm));
893 }
894
895 #[test]
896 fn test_name() {
897 let backend = ProcessBackend::default();
898 assert_eq!(backend.name(), "process");
899 }
900
901 #[test]
902 fn test_process_config_default() {
903 let config = ProcessConfig::default();
904 assert_eq!(config.rustc_path, "rustc");
905 assert_eq!(config.python_path, "python3");
906 assert_eq!(config.node_path, "node");
907 }
908
909 #[test]
910 fn windows_compiler_environment_is_a_minimal_allowlist() {
911 assert_eq!(
912 WINDOWS_TOOLCHAIN_ENV_KEYS,
913 &[
914 "PATH",
915 "LIB",
916 "LIBPATH",
917 "INCLUDE",
918 "SystemRoot",
919 "TEMP",
920 "TMP",
921 "USERPROFILE",
922 "RUSTUP_HOME",
923 "RUSTUP_TOOLCHAIN",
924 ]
925 );
926 }
927}