deno_runtime/ops/
process.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3use deno_core::op2;
4use deno_core::serde_json;
5use deno_core::AsyncMutFuture;
6use deno_core::AsyncRefCell;
7use deno_core::OpState;
8use deno_core::RcRef;
9use deno_core::Resource;
10use deno_core::ResourceId;
11use deno_core::ToJsBuffer;
12use deno_io::fs::FileResource;
13use deno_io::ChildStderrResource;
14use deno_io::ChildStdinResource;
15use deno_io::ChildStdoutResource;
16use deno_io::IntoRawIoHandle;
17use deno_permissions::PermissionsContainer;
18use deno_permissions::RunQueryDescriptor;
19use serde::Deserialize;
20use serde::Serialize;
21use std::borrow::Cow;
22use std::cell::RefCell;
23use std::collections::HashMap;
24use std::ffi::OsString;
25use std::io::Write;
26use std::path::Path;
27use std::path::PathBuf;
28use std::process::ExitStatus;
29use std::rc::Rc;
30use tokio::process::Command;
31
32#[cfg(windows)]
33use std::os::windows::process::CommandExt;
34
35use crate::ops::signal::SignalError;
36#[cfg(unix)]
37use std::os::unix::prelude::ExitStatusExt;
38#[cfg(unix)]
39use std::os::unix::process::CommandExt;
40
41pub const UNSTABLE_FEATURE_NAME: &str = "process";
42
43#[derive(Copy, Clone, Eq, PartialEq, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum Stdio {
46  Inherit,
47  Piped,
48  Null,
49  IpcForInternalUse,
50}
51
52impl Stdio {
53  pub fn as_stdio(&self) -> std::process::Stdio {
54    match &self {
55      Stdio::Inherit => std::process::Stdio::inherit(),
56      Stdio::Piped => std::process::Stdio::piped(),
57      Stdio::Null => std::process::Stdio::null(),
58      _ => unreachable!(),
59    }
60  }
61}
62
63#[derive(Copy, Clone, Eq, PartialEq)]
64pub enum StdioOrRid {
65  Stdio(Stdio),
66  Rid(ResourceId),
67}
68
69impl<'de> Deserialize<'de> for StdioOrRid {
70  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71  where
72    D: serde::Deserializer<'de>,
73  {
74    use serde_json::Value;
75    let value = Value::deserialize(deserializer)?;
76    match value {
77      Value::String(val) => match val.as_str() {
78        "inherit" => Ok(StdioOrRid::Stdio(Stdio::Inherit)),
79        "piped" => Ok(StdioOrRid::Stdio(Stdio::Piped)),
80        "null" => Ok(StdioOrRid::Stdio(Stdio::Null)),
81        "ipc_for_internal_use" => {
82          Ok(StdioOrRid::Stdio(Stdio::IpcForInternalUse))
83        }
84        val => Err(serde::de::Error::unknown_variant(
85          val,
86          &["inherit", "piped", "null"],
87        )),
88      },
89      Value::Number(val) => match val.as_u64() {
90        Some(val) if val <= ResourceId::MAX as u64 => {
91          Ok(StdioOrRid::Rid(val as ResourceId))
92        }
93        _ => Err(serde::de::Error::custom("Expected a positive integer")),
94      },
95      _ => Err(serde::de::Error::custom(
96        r#"Expected a resource id, "inherit", "piped", or "null""#,
97      )),
98    }
99  }
100}
101
102impl StdioOrRid {
103  pub fn as_stdio(
104    &self,
105    state: &mut OpState,
106  ) -> Result<std::process::Stdio, ProcessError> {
107    match &self {
108      StdioOrRid::Stdio(val) => Ok(val.as_stdio()),
109      StdioOrRid::Rid(rid) => {
110        FileResource::with_file(state, *rid, |file| Ok(file.as_stdio()?))
111          .map_err(ProcessError::Resource)
112      }
113    }
114  }
115
116  pub fn is_ipc(&self) -> bool {
117    matches!(self, StdioOrRid::Stdio(Stdio::IpcForInternalUse))
118  }
119}
120
121#[allow(clippy::disallowed_types)]
122pub type NpmProcessStateProviderRc =
123  deno_fs::sync::MaybeArc<dyn NpmProcessStateProvider>;
124
125pub trait NpmProcessStateProvider:
126  std::fmt::Debug + deno_fs::sync::MaybeSend + deno_fs::sync::MaybeSync
127{
128  /// Gets a string containing the serialized npm state of the process.
129  ///
130  /// This will be set on the `DENO_DONT_USE_INTERNAL_NODE_COMPAT_STATE` environment
131  /// variable when doing a `child_process.fork`. The implementor can then check this environment
132  /// variable on startup to repopulate the internal npm state.
133  fn get_npm_process_state(&self) -> String {
134    // This method is only used in the CLI.
135    String::new()
136  }
137}
138
139#[derive(Debug)]
140pub struct EmptyNpmProcessStateProvider;
141impl NpmProcessStateProvider for EmptyNpmProcessStateProvider {}
142deno_core::extension!(
143  deno_process,
144  ops = [
145    op_spawn_child,
146    op_spawn_wait,
147    op_spawn_sync,
148    op_spawn_kill,
149    deprecated::op_run,
150    deprecated::op_run_status,
151    deprecated::op_kill,
152  ],
153  options = { get_npm_process_state: Option<NpmProcessStateProviderRc>  },
154  state = |state, options| {
155    state.put::<NpmProcessStateProviderRc>(options.get_npm_process_state.unwrap_or(deno_fs::sync::MaybeArc::new(EmptyNpmProcessStateProvider)));
156  },
157);
158
159/// Second member stores the pid separately from the RefCell. It's needed for
160/// `op_spawn_kill`, where the RefCell is borrowed mutably by `op_spawn_wait`.
161struct ChildResource(RefCell<tokio::process::Child>, u32);
162
163impl Resource for ChildResource {
164  fn name(&self) -> Cow<str> {
165    "child".into()
166  }
167}
168
169#[derive(Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct SpawnArgs {
172  cmd: String,
173  args: Vec<String>,
174  cwd: Option<String>,
175  clear_env: bool,
176  env: Vec<(String, String)>,
177  #[cfg(unix)]
178  gid: Option<u32>,
179  #[cfg(unix)]
180  uid: Option<u32>,
181  #[cfg(windows)]
182  windows_raw_arguments: bool,
183  ipc: Option<i32>,
184
185  #[serde(flatten)]
186  stdio: ChildStdio,
187
188  extra_stdio: Vec<Stdio>,
189  detached: bool,
190  needs_npm_process_state: bool,
191}
192
193#[derive(Debug, thiserror::Error)]
194pub enum ProcessError {
195  #[error("Failed to spawn '{command}': {error}")]
196  SpawnFailed {
197    command: String,
198    #[source]
199    error: Box<ProcessError>,
200  },
201  #[error("{0}")]
202  Io(#[from] std::io::Error),
203  #[cfg(unix)]
204  #[error(transparent)]
205  Nix(nix::Error),
206  #[error("failed resolving cwd: {0}")]
207  FailedResolvingCwd(#[source] std::io::Error),
208  #[error(transparent)]
209  Permission(#[from] deno_permissions::PermissionCheckError),
210  #[error(transparent)]
211  RunPermission(#[from] CheckRunPermissionError),
212  #[error(transparent)]
213  Resource(deno_core::error::AnyError),
214  #[error(transparent)]
215  BorrowMut(std::cell::BorrowMutError),
216  #[error(transparent)]
217  Which(which::Error),
218  #[error("Child process has already terminated.")]
219  ChildProcessAlreadyTerminated,
220  #[error("Invalid pid")]
221  InvalidPid,
222  #[error(transparent)]
223  Signal(#[from] SignalError),
224  #[error("Missing cmd")]
225  MissingCmd, // only for Deno.run
226}
227
228#[derive(Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct ChildStdio {
231  stdin: StdioOrRid,
232  stdout: StdioOrRid,
233  stderr: StdioOrRid,
234}
235
236#[derive(Serialize)]
237#[serde(rename_all = "camelCase")]
238pub struct ChildStatus {
239  success: bool,
240  code: i32,
241  signal: Option<String>,
242}
243
244impl TryFrom<ExitStatus> for ChildStatus {
245  type Error = SignalError;
246
247  fn try_from(status: ExitStatus) -> Result<Self, Self::Error> {
248    let code = status.code();
249    #[cfg(unix)]
250    let signal = status.signal();
251    #[cfg(not(unix))]
252    let signal: Option<i32> = None;
253
254    let status = if let Some(signal) = signal {
255      ChildStatus {
256        success: false,
257        code: 128 + signal,
258        #[cfg(unix)]
259        signal: Some(
260          crate::ops::signal::signal_int_to_str(signal)?.to_string(),
261        ),
262        #[cfg(not(unix))]
263        signal: None,
264      }
265    } else {
266      let code = code.expect("Should have either an exit code or a signal.");
267
268      ChildStatus {
269        success: code == 0,
270        code,
271        signal: None,
272      }
273    };
274
275    Ok(status)
276  }
277}
278
279#[derive(Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct SpawnOutput {
282  status: ChildStatus,
283  stdout: Option<ToJsBuffer>,
284  stderr: Option<ToJsBuffer>,
285}
286
287type CreateCommand = (
288  std::process::Command,
289  Option<ResourceId>,
290  Vec<Option<ResourceId>>,
291  Vec<deno_io::RawBiPipeHandle>,
292);
293
294pub fn npm_process_state_tempfile(
295  contents: &[u8],
296) -> Result<deno_io::RawIoHandle, std::io::Error> {
297  let mut temp_file = tempfile::tempfile()?;
298  temp_file.write_all(contents)?;
299  let handle = temp_file.into_raw_io_handle();
300  #[cfg(windows)]
301  {
302    use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
303    // make the handle inheritable
304    // SAFETY: winapi call, handle is valid
305    unsafe {
306      windows_sys::Win32::Foundation::SetHandleInformation(
307        handle as _,
308        HANDLE_FLAG_INHERIT,
309        HANDLE_FLAG_INHERIT,
310      );
311    }
312    Ok(handle)
313  }
314  #[cfg(unix)]
315  {
316    // SAFETY: libc call, fd is valid
317    let inheritable = unsafe {
318      // duplicate the FD to get a new one that doesn't have the CLOEXEC flag set
319      // so it can be inherited by the child process
320      libc::dup(handle)
321    };
322    // SAFETY: libc call, fd is valid
323    unsafe {
324      // close the old one
325      libc::close(handle);
326    }
327    Ok(inheritable)
328  }
329}
330
331pub const NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME: &str =
332  "DENO_DONT_USE_INTERNAL_NODE_COMPAT_STATE_FD";
333
334fn create_command(
335  state: &mut OpState,
336  mut args: SpawnArgs,
337  api_name: &str,
338) -> Result<CreateCommand, ProcessError> {
339  let maybe_npm_process_state = if args.needs_npm_process_state {
340    let provider = state.borrow::<NpmProcessStateProviderRc>();
341    let process_state = provider.get_npm_process_state();
342    let fd = npm_process_state_tempfile(process_state.as_bytes())?;
343    args.env.push((
344      NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME.to_string(),
345      (fd as usize).to_string(),
346    ));
347    Some(fd)
348  } else {
349    None
350  };
351
352  let (cmd, run_env) = compute_run_cmd_and_check_permissions(
353    &args.cmd,
354    args.cwd.as_deref(),
355    &args.env,
356    args.clear_env,
357    state,
358    api_name,
359  )?;
360  let mut command = std::process::Command::new(cmd);
361
362  #[cfg(windows)]
363  {
364    if args.detached {
365      // TODO(nathanwhit): Currently this causes the process to hang
366      // until the detached process exits (so never). It repros with just the
367      // rust std library, so it's either a bug or requires more control than we have.
368      // To be resolved at the same time as additional stdio support.
369      log::warn!("detached processes are not currently supported on Windows");
370    }
371    if args.windows_raw_arguments {
372      for arg in args.args.iter() {
373        command.raw_arg(arg);
374      }
375    } else {
376      command.args(args.args);
377    }
378  }
379
380  #[cfg(not(windows))]
381  command.args(args.args);
382
383  command.current_dir(run_env.cwd);
384  command.env_clear();
385  command.envs(run_env.envs);
386
387  #[cfg(unix)]
388  if let Some(gid) = args.gid {
389    command.gid(gid);
390  }
391  #[cfg(unix)]
392  if let Some(uid) = args.uid {
393    command.uid(uid);
394  }
395
396  if args.stdio.stdin.is_ipc() {
397    args.ipc = Some(0);
398  } else {
399    command.stdin(args.stdio.stdin.as_stdio(state)?);
400  }
401
402  command.stdout(match args.stdio.stdout {
403    StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(1).as_stdio(state)?,
404    value => value.as_stdio(state)?,
405  });
406  command.stderr(match args.stdio.stderr {
407    StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(2).as_stdio(state)?,
408    value => value.as_stdio(state)?,
409  });
410
411  #[cfg(unix)]
412  // TODO(bartlomieju):
413  #[allow(clippy::undocumented_unsafe_blocks)]
414  unsafe {
415    let mut extra_pipe_rids = Vec::new();
416    let mut fds_to_dup = Vec::new();
417    let mut fds_to_close = Vec::new();
418    let mut ipc_rid = None;
419    if let Some(fd) = maybe_npm_process_state {
420      fds_to_close.push(fd);
421    }
422    if let Some(ipc) = args.ipc {
423      if ipc >= 0 {
424        let (ipc_fd1, ipc_fd2) = deno_io::bi_pipe_pair_raw()?;
425        fds_to_dup.push((ipc_fd2, ipc));
426        fds_to_close.push(ipc_fd2);
427        /* One end returned to parent process (this) */
428        let pipe_rid =
429          state
430            .resource_table
431            .add(deno_node::IpcJsonStreamResource::new(
432              ipc_fd1 as _,
433              deno_node::IpcRefTracker::new(state.external_ops_tracker.clone()),
434            )?);
435        /* The other end passed to child process via NODE_CHANNEL_FD */
436        command.env("NODE_CHANNEL_FD", format!("{}", ipc));
437        ipc_rid = Some(pipe_rid);
438      }
439    }
440
441    for (i, stdio) in args.extra_stdio.into_iter().enumerate() {
442      // index 0 in `extra_stdio` actually refers to fd 3
443      // because we handle stdin,stdout,stderr specially
444      let fd = (i + 3) as i32;
445      // TODO(nathanwhit): handle inherited, but this relies on the parent process having
446      // fds open already. since we don't generally support dealing with raw fds,
447      // we can't properly support this
448      if matches!(stdio, Stdio::Piped) {
449        let (fd1, fd2) = deno_io::bi_pipe_pair_raw()?;
450        fds_to_dup.push((fd2, fd));
451        fds_to_close.push(fd2);
452        let rid = state.resource_table.add(
453          match deno_io::BiPipeResource::from_raw_handle(fd1) {
454            Ok(v) => v,
455            Err(e) => {
456              log::warn!("Failed to open bidirectional pipe for fd {fd}: {e}");
457              extra_pipe_rids.push(None);
458              continue;
459            }
460          },
461        );
462        extra_pipe_rids.push(Some(rid));
463      } else {
464        extra_pipe_rids.push(None);
465      }
466    }
467
468    let detached = args.detached;
469    command.pre_exec(move || {
470      if detached {
471        libc::setsid();
472      }
473      for &(src, dst) in &fds_to_dup {
474        if src >= 0 && dst >= 0 {
475          let _fd = libc::dup2(src, dst);
476          libc::close(src);
477        }
478      }
479      libc::setgroups(0, std::ptr::null());
480      Ok(())
481    });
482
483    Ok((command, ipc_rid, extra_pipe_rids, fds_to_close))
484  }
485
486  #[cfg(windows)]
487  {
488    let mut ipc_rid = None;
489    let mut handles_to_close = Vec::with_capacity(1);
490    if let Some(handle) = maybe_npm_process_state {
491      handles_to_close.push(handle);
492    }
493    if let Some(ipc) = args.ipc {
494      if ipc >= 0 {
495        let (hd1, hd2) = deno_io::bi_pipe_pair_raw()?;
496
497        /* One end returned to parent process (this) */
498        let pipe_rid = Some(state.resource_table.add(
499          deno_node::IpcJsonStreamResource::new(
500            hd1 as i64,
501            deno_node::IpcRefTracker::new(state.external_ops_tracker.clone()),
502          )?,
503        ));
504
505        /* The other end passed to child process via NODE_CHANNEL_FD */
506        command.env("NODE_CHANNEL_FD", format!("{}", hd2 as i64));
507
508        handles_to_close.push(hd2);
509
510        ipc_rid = pipe_rid;
511      }
512    }
513
514    if args.extra_stdio.iter().any(|s| matches!(s, Stdio::Piped)) {
515      log::warn!(
516        "Additional stdio pipes beyond stdin/stdout/stderr are not currently supported on windows"
517      );
518    }
519
520    Ok((command, ipc_rid, vec![], handles_to_close))
521  }
522}
523
524#[derive(Serialize)]
525#[serde(rename_all = "camelCase")]
526struct Child {
527  rid: ResourceId,
528  pid: u32,
529  stdin_rid: Option<ResourceId>,
530  stdout_rid: Option<ResourceId>,
531  stderr_rid: Option<ResourceId>,
532  ipc_pipe_rid: Option<ResourceId>,
533  extra_pipe_rids: Vec<Option<ResourceId>>,
534}
535
536fn spawn_child(
537  state: &mut OpState,
538  command: std::process::Command,
539  ipc_pipe_rid: Option<ResourceId>,
540  extra_pipe_rids: Vec<Option<ResourceId>>,
541  detached: bool,
542) -> Result<Child, ProcessError> {
543  let mut command = tokio::process::Command::from(command);
544  // TODO(@crowlkats): allow detaching processes.
545  //  currently deno will orphan a process when exiting with an error or Deno.exit()
546  // We want to kill child when it's closed
547  if !detached {
548    command.kill_on_drop(true);
549  }
550
551  let mut child = match command.spawn() {
552    Ok(child) => child,
553    Err(err) => {
554      let command = command.as_std();
555      let command_name = command.get_program().to_string_lossy();
556
557      if let Some(cwd) = command.get_current_dir() {
558        // launching a sub process always depends on the real
559        // file system so using these methods directly is ok
560        #[allow(clippy::disallowed_methods)]
561        if !cwd.exists() {
562          return Err(
563            std::io::Error::new(
564              std::io::ErrorKind::NotFound,
565              format!(
566                "Failed to spawn '{}': No such cwd '{}'",
567                command_name,
568                cwd.to_string_lossy()
569              ),
570            )
571            .into(),
572          );
573        }
574
575        #[allow(clippy::disallowed_methods)]
576        if !cwd.is_dir() {
577          return Err(
578            std::io::Error::new(
579              std::io::ErrorKind::NotFound,
580              format!(
581                "Failed to spawn '{}': cwd is not a directory '{}'",
582                command_name,
583                cwd.to_string_lossy()
584              ),
585            )
586            .into(),
587          );
588        }
589      }
590
591      return Err(ProcessError::SpawnFailed {
592        command: command.get_program().to_string_lossy().to_string(),
593        error: Box::new(err.into()),
594      });
595    }
596  };
597
598  let pid = child.id().expect("Process ID should be set.");
599
600  let stdin_rid = child
601    .stdin
602    .take()
603    .map(|stdin| state.resource_table.add(ChildStdinResource::from(stdin)));
604
605  let stdout_rid = child
606    .stdout
607    .take()
608    .map(|stdout| state.resource_table.add(ChildStdoutResource::from(stdout)));
609
610  let stderr_rid = child
611    .stderr
612    .take()
613    .map(|stderr| state.resource_table.add(ChildStderrResource::from(stderr)));
614
615  let child_rid = state
616    .resource_table
617    .add(ChildResource(RefCell::new(child), pid));
618
619  Ok(Child {
620    rid: child_rid,
621    pid,
622    stdin_rid,
623    stdout_rid,
624    stderr_rid,
625    ipc_pipe_rid,
626    extra_pipe_rids,
627  })
628}
629
630fn compute_run_cmd_and_check_permissions(
631  arg_cmd: &str,
632  arg_cwd: Option<&str>,
633  arg_envs: &[(String, String)],
634  arg_clear_env: bool,
635  state: &mut OpState,
636  api_name: &str,
637) -> Result<(PathBuf, RunEnv), ProcessError> {
638  let run_env =
639    compute_run_env(arg_cwd, arg_envs, arg_clear_env).map_err(|e| {
640      ProcessError::SpawnFailed {
641        command: arg_cmd.to_string(),
642        error: Box::new(e),
643      }
644    })?;
645  let cmd =
646    resolve_cmd(arg_cmd, &run_env).map_err(|e| ProcessError::SpawnFailed {
647      command: arg_cmd.to_string(),
648      error: Box::new(e),
649    })?;
650  check_run_permission(
651    state,
652    &RunQueryDescriptor::Path {
653      requested: arg_cmd.to_string(),
654      resolved: cmd.clone(),
655    },
656    &run_env,
657    api_name,
658  )?;
659  Ok((cmd, run_env))
660}
661
662struct RunEnv {
663  envs: HashMap<OsString, OsString>,
664  cwd: PathBuf,
665}
666
667/// Computes the current environment, which will then be used to inform
668/// permissions and finally spawning. This is very important to compute
669/// ahead of time so that the environment used to verify permissions is
670/// the same environment used to spawn the sub command. This protects against
671/// someone doing timing attacks by changing the environment on a worker.
672fn compute_run_env(
673  arg_cwd: Option<&str>,
674  arg_envs: &[(String, String)],
675  arg_clear_env: bool,
676) -> Result<RunEnv, ProcessError> {
677  #[allow(clippy::disallowed_methods)]
678  let cwd =
679    std::env::current_dir().map_err(ProcessError::FailedResolvingCwd)?;
680  let cwd = arg_cwd
681    .map(|cwd_arg| resolve_path(cwd_arg, &cwd))
682    .unwrap_or(cwd);
683  let envs = if arg_clear_env {
684    arg_envs
685      .iter()
686      .map(|(k, v)| (OsString::from(k), OsString::from(v)))
687      .collect()
688  } else {
689    let mut envs = std::env::vars_os()
690      .map(|(k, v)| {
691        (
692          if cfg!(windows) {
693            k.to_ascii_uppercase()
694          } else {
695            k
696          },
697          v,
698        )
699      })
700      .collect::<HashMap<_, _>>();
701    for (key, value) in arg_envs {
702      envs.insert(
703        OsString::from(if cfg!(windows) {
704          key.to_ascii_uppercase()
705        } else {
706          key.clone()
707        }),
708        OsString::from(value.clone()),
709      );
710    }
711    envs
712  };
713  Ok(RunEnv { envs, cwd })
714}
715
716fn resolve_cmd(cmd: &str, env: &RunEnv) -> Result<PathBuf, ProcessError> {
717  let is_path = cmd.contains('/');
718  #[cfg(windows)]
719  let is_path = is_path || cmd.contains('\\') || Path::new(&cmd).is_absolute();
720  if is_path {
721    Ok(resolve_path(cmd, &env.cwd))
722  } else {
723    let path = env.envs.get(&OsString::from("PATH"));
724    match which::which_in(cmd, path, &env.cwd) {
725      Ok(cmd) => Ok(cmd),
726      Err(which::Error::CannotFindBinaryPath) => {
727        Err(std::io::Error::from(std::io::ErrorKind::NotFound).into())
728      }
729      Err(err) => Err(ProcessError::Which(err)),
730    }
731  }
732}
733
734fn resolve_path(path: &str, cwd: &Path) -> PathBuf {
735  deno_path_util::normalize_path(cwd.join(path))
736}
737
738#[derive(Debug, thiserror::Error)]
739pub enum CheckRunPermissionError {
740  #[error(transparent)]
741  Permission(#[from] deno_permissions::PermissionCheckError),
742  #[error("{0}")]
743  Other(deno_core::error::AnyError),
744}
745
746fn check_run_permission(
747  state: &mut OpState,
748  cmd: &RunQueryDescriptor,
749  run_env: &RunEnv,
750  api_name: &str,
751) -> Result<(), CheckRunPermissionError> {
752  let permissions = state.borrow_mut::<PermissionsContainer>();
753  if !permissions.query_run_all(api_name) {
754    // error the same on all platforms
755    let env_var_names = get_requires_allow_all_env_vars(run_env);
756    if !env_var_names.is_empty() {
757      // we don't allow users to launch subprocesses with any LD_ or DYLD_*
758      // env vars set because this allows executing code (ex. LD_PRELOAD)
759      return Err(CheckRunPermissionError::Other(
760        deno_core::error::custom_error(
761          "NotCapable",
762          format!(
763            "Requires --allow-run permissions to spawn subprocess with {0} environment variable{1}. Alternatively, spawn with {2} environment variable{1} unset.",
764            env_var_names.join(", "),
765            if env_var_names.len() != 1 { "s" } else { "" },
766            if env_var_names.len() != 1 { "these" } else { "the" }
767          ),
768        ),
769      ));
770    }
771    permissions.check_run(cmd, api_name)?;
772  }
773  Ok(())
774}
775
776fn get_requires_allow_all_env_vars(env: &RunEnv) -> Vec<&str> {
777  fn requires_allow_all(key: &str) -> bool {
778    let key = key.trim();
779    // we could be more targted here, but there are quite a lot of
780    // LD_* and DYLD_* env variables
781    key.starts_with("LD_") || key.starts_with("DYLD_")
782  }
783
784  fn is_empty(value: &OsString) -> bool {
785    value.is_empty()
786      || value.to_str().map(|v| v.trim().is_empty()).unwrap_or(false)
787  }
788
789  let mut found_envs = env
790    .envs
791    .iter()
792    .filter_map(|(k, v)| {
793      let key = k.to_str()?;
794      if requires_allow_all(key) && !is_empty(v) {
795        Some(key)
796      } else {
797        None
798      }
799    })
800    .collect::<Vec<_>>();
801  found_envs.sort();
802  found_envs
803}
804
805#[op2(stack_trace)]
806#[serde]
807fn op_spawn_child(
808  state: &mut OpState,
809  #[serde] args: SpawnArgs,
810  #[string] api_name: String,
811) -> Result<Child, ProcessError> {
812  let detached = args.detached;
813  let (command, pipe_rid, extra_pipe_rids, handles_to_close) =
814    create_command(state, args, &api_name)?;
815  let child = spawn_child(state, command, pipe_rid, extra_pipe_rids, detached);
816  for handle in handles_to_close {
817    deno_io::close_raw_handle(handle);
818  }
819  child
820}
821
822#[op2(async)]
823#[allow(clippy::await_holding_refcell_ref)]
824#[serde]
825async fn op_spawn_wait(
826  state: Rc<RefCell<OpState>>,
827  #[smi] rid: ResourceId,
828) -> Result<ChildStatus, ProcessError> {
829  let resource = state
830    .borrow_mut()
831    .resource_table
832    .get::<ChildResource>(rid)
833    .map_err(ProcessError::Resource)?;
834  let result = resource
835    .0
836    .try_borrow_mut()
837    .map_err(ProcessError::BorrowMut)?
838    .wait()
839    .await?
840    .try_into()?;
841  if let Ok(resource) = state.borrow_mut().resource_table.take_any(rid) {
842    resource.close();
843  }
844  Ok(result)
845}
846
847#[op2(stack_trace)]
848#[serde]
849fn op_spawn_sync(
850  state: &mut OpState,
851  #[serde] args: SpawnArgs,
852) -> Result<SpawnOutput, ProcessError> {
853  let stdout = matches!(args.stdio.stdout, StdioOrRid::Stdio(Stdio::Piped));
854  let stderr = matches!(args.stdio.stderr, StdioOrRid::Stdio(Stdio::Piped));
855  let (mut command, _, _, _) =
856    create_command(state, args, "Deno.Command().outputSync()")?;
857  let output = command.output().map_err(|e| ProcessError::SpawnFailed {
858    command: command.get_program().to_string_lossy().to_string(),
859    error: Box::new(e.into()),
860  })?;
861
862  Ok(SpawnOutput {
863    status: output.status.try_into()?,
864    stdout: if stdout {
865      Some(output.stdout.into())
866    } else {
867      None
868    },
869    stderr: if stderr {
870      Some(output.stderr.into())
871    } else {
872      None
873    },
874  })
875}
876
877#[op2(fast)]
878fn op_spawn_kill(
879  state: &mut OpState,
880  #[smi] rid: ResourceId,
881  #[string] signal: String,
882) -> Result<(), ProcessError> {
883  if let Ok(child_resource) = state.resource_table.get::<ChildResource>(rid) {
884    deprecated::kill(child_resource.1 as i32, &signal)?;
885    return Ok(());
886  }
887  Err(ProcessError::ChildProcessAlreadyTerminated)
888}
889
890mod deprecated {
891  use super::*;
892
893  #[derive(Deserialize)]
894  #[serde(rename_all = "camelCase")]
895  pub struct RunArgs {
896    cmd: Vec<String>,
897    cwd: Option<String>,
898    env: Vec<(String, String)>,
899    stdin: StdioOrRid,
900    stdout: StdioOrRid,
901    stderr: StdioOrRid,
902  }
903
904  struct ChildResource {
905    child: AsyncRefCell<tokio::process::Child>,
906  }
907
908  impl Resource for ChildResource {
909    fn name(&self) -> Cow<str> {
910      "child".into()
911    }
912  }
913
914  impl ChildResource {
915    fn borrow_mut(self: Rc<Self>) -> AsyncMutFuture<tokio::process::Child> {
916      RcRef::map(self, |r| &r.child).borrow_mut()
917    }
918  }
919
920  #[derive(Serialize)]
921  #[serde(rename_all = "camelCase")]
922  // TODO(@AaronO): maybe find a more descriptive name or a convention for return structs
923  pub struct RunInfo {
924    rid: ResourceId,
925    pid: Option<u32>,
926    stdin_rid: Option<ResourceId>,
927    stdout_rid: Option<ResourceId>,
928    stderr_rid: Option<ResourceId>,
929  }
930
931  #[op2(stack_trace)]
932  #[serde]
933  pub fn op_run(
934    state: &mut OpState,
935    #[serde] run_args: RunArgs,
936  ) -> Result<RunInfo, ProcessError> {
937    let args = run_args.cmd;
938    let cmd = args.first().ok_or(ProcessError::MissingCmd)?;
939    let (cmd, run_env) = compute_run_cmd_and_check_permissions(
940      cmd,
941      run_args.cwd.as_deref(),
942      &run_args.env,
943      /* clear env */ false,
944      state,
945      "Deno.run()",
946    )?;
947
948    let mut c = Command::new(cmd);
949    for arg in args.iter().skip(1) {
950      c.arg(arg);
951    }
952    c.current_dir(run_env.cwd);
953
954    c.env_clear();
955    for (key, value) in run_env.envs {
956      c.env(key, value);
957    }
958
959    #[cfg(unix)]
960    // TODO(bartlomieju):
961    #[allow(clippy::undocumented_unsafe_blocks)]
962    unsafe {
963      c.pre_exec(|| {
964        libc::setgroups(0, std::ptr::null());
965        Ok(())
966      });
967    }
968
969    // TODO: make this work with other resources, eg. sockets
970    c.stdin(run_args.stdin.as_stdio(state)?);
971    c.stdout(
972      match run_args.stdout {
973        StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(1),
974        value => value,
975      }
976      .as_stdio(state)?,
977    );
978    c.stderr(
979      match run_args.stderr {
980        StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(2),
981        value => value,
982      }
983      .as_stdio(state)?,
984    );
985
986    // We want to kill child when it's closed
987    c.kill_on_drop(true);
988
989    // Spawn the command.
990    let mut child = c.spawn()?;
991    let pid = child.id();
992
993    let stdin_rid = match child.stdin.take() {
994      Some(child_stdin) => {
995        let rid = state
996          .resource_table
997          .add(ChildStdinResource::from(child_stdin));
998        Some(rid)
999      }
1000      None => None,
1001    };
1002
1003    let stdout_rid = match child.stdout.take() {
1004      Some(child_stdout) => {
1005        let rid = state
1006          .resource_table
1007          .add(ChildStdoutResource::from(child_stdout));
1008        Some(rid)
1009      }
1010      None => None,
1011    };
1012
1013    let stderr_rid = match child.stderr.take() {
1014      Some(child_stderr) => {
1015        let rid = state
1016          .resource_table
1017          .add(ChildStderrResource::from(child_stderr));
1018        Some(rid)
1019      }
1020      None => None,
1021    };
1022
1023    let child_resource = ChildResource {
1024      child: AsyncRefCell::new(child),
1025    };
1026    let child_rid = state.resource_table.add(child_resource);
1027
1028    Ok(RunInfo {
1029      rid: child_rid,
1030      pid,
1031      stdin_rid,
1032      stdout_rid,
1033      stderr_rid,
1034    })
1035  }
1036
1037  #[derive(Serialize)]
1038  #[serde(rename_all = "camelCase")]
1039  pub struct ProcessStatus {
1040    got_signal: bool,
1041    exit_code: i32,
1042    exit_signal: i32,
1043  }
1044
1045  #[op2(async)]
1046  #[serde]
1047  pub async fn op_run_status(
1048    state: Rc<RefCell<OpState>>,
1049    #[smi] rid: ResourceId,
1050  ) -> Result<ProcessStatus, ProcessError> {
1051    let resource = state
1052      .borrow_mut()
1053      .resource_table
1054      .get::<ChildResource>(rid)
1055      .map_err(ProcessError::Resource)?;
1056    let mut child = resource.borrow_mut().await;
1057    let run_status = child.wait().await?;
1058    let code = run_status.code();
1059
1060    #[cfg(unix)]
1061    let signal = run_status.signal();
1062    #[cfg(not(unix))]
1063    let signal = Default::default();
1064
1065    code
1066      .or(signal)
1067      .expect("Should have either an exit code or a signal.");
1068    let got_signal = signal.is_some();
1069
1070    Ok(ProcessStatus {
1071      got_signal,
1072      exit_code: code.unwrap_or(-1),
1073      exit_signal: signal.unwrap_or(-1),
1074    })
1075  }
1076
1077  #[cfg(unix)]
1078  pub fn kill(pid: i32, signal: &str) -> Result<(), ProcessError> {
1079    let signo = super::super::signal::signal_str_to_int(signal)?;
1080    use nix::sys::signal::kill as unix_kill;
1081    use nix::sys::signal::Signal;
1082    use nix::unistd::Pid;
1083    let sig = Signal::try_from(signo).map_err(ProcessError::Nix)?;
1084    unix_kill(Pid::from_raw(pid), Some(sig)).map_err(ProcessError::Nix)
1085  }
1086
1087  #[cfg(not(unix))]
1088  pub fn kill(pid: i32, signal: &str) -> Result<(), ProcessError> {
1089    use std::io::Error;
1090    use std::io::ErrorKind::NotFound;
1091    use winapi::shared::minwindef::DWORD;
1092    use winapi::shared::minwindef::FALSE;
1093    use winapi::shared::minwindef::TRUE;
1094    use winapi::shared::winerror::ERROR_INVALID_PARAMETER;
1095    use winapi::um::errhandlingapi::GetLastError;
1096    use winapi::um::handleapi::CloseHandle;
1097    use winapi::um::processthreadsapi::OpenProcess;
1098    use winapi::um::processthreadsapi::TerminateProcess;
1099    use winapi::um::winnt::PROCESS_TERMINATE;
1100
1101    if !matches!(signal, "SIGKILL" | "SIGTERM") {
1102      Err(SignalError::InvalidSignalStr(signal.to_string()).into())
1103    } else if pid <= 0 {
1104      Err(ProcessError::InvalidPid)
1105    } else {
1106      let handle =
1107        // SAFETY: winapi call
1108        unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as DWORD) };
1109
1110      if handle.is_null() {
1111        // SAFETY: winapi call
1112        let err = match unsafe { GetLastError() } {
1113          ERROR_INVALID_PARAMETER => Error::from(NotFound), // Invalid `pid`.
1114          errno => Error::from_raw_os_error(errno as i32),
1115        };
1116        Err(err.into())
1117      } else {
1118        // SAFETY: winapi calls
1119        unsafe {
1120          let is_terminated = TerminateProcess(handle, 1);
1121          CloseHandle(handle);
1122          match is_terminated {
1123            FALSE => Err(Error::last_os_error().into()),
1124            TRUE => Ok(()),
1125            _ => unreachable!(),
1126          }
1127        }
1128      }
1129    }
1130  }
1131
1132  #[op2(fast, stack_trace)]
1133  pub fn op_kill(
1134    state: &mut OpState,
1135    #[smi] pid: i32,
1136    #[string] signal: String,
1137    #[string] api_name: String,
1138  ) -> Result<(), ProcessError> {
1139    state
1140      .borrow_mut::<PermissionsContainer>()
1141      .check_run_all(&api_name)?;
1142    kill(pid, &signal)
1143  }
1144}