denoland/deno · error

program path contains NUL

Error message

program path contains NUL

What it means

The desktop tooling spawns subprocesses through raw `posix_spawn`, so `std::process::Command` values are flattened into C strings by `flatten()`. `CString::new` fails when the program path contains an interior NUL byte (0x00), because NUL terminates C strings and is illegal in OS paths; the failure is mapped to `io::ErrorKind::InvalidInput` with this message.

Source

Thrown at cli/tools/desktop.rs:5728

      if !self.exited {
        // SAFETY: `kill(2)` with a pid we spawned is always safe to call; a
        // stale pid simply returns ESRCH, which we ignore.
        unsafe {
          libc::kill(self.pid, libc::SIGKILL);
        }
      }
    }
  }

  /// Flattened `(program, argv, envp, cwd)` for `posix_spawn`.
  type SpawnArgs = (CString, Vec<CString>, Vec<CString>, Option<CString>);

  /// Convert a std::process::Command into the argv/envp/cwd tuple posix_spawn
  /// needs. Inherits the parent's env, then applies Command::env() overrides
  /// (matching what std::process::Command does internally).
  fn flatten(cmd: &std::process::Command) -> std::io::Result<SpawnArgs> {
    let program = CString::new(cmd.get_program().as_bytes()).map_err(|_| {
      std::io::Error::new(
        std::io::ErrorKind::InvalidInput,
        "program path contains NUL",
      )
    })?;
    let mut argv: Vec<CString> = Vec::with_capacity(cmd.get_args().len() + 1);
    argv.push(program.clone());
    for a in cmd.get_args() {
      argv.push(CString::new(a.as_bytes()).map_err(|_| {
        std::io::Error::new(
          std::io::ErrorKind::InvalidInput,
          "argv contains NUL",
        )
      })?);
    }
    let mut env_map: std::collections::BTreeMap<OsString, OsString> =
      std::env::vars_os().collect();
    for (k, v) in cmd.get_envs() {
      match v {

View on GitHub (pinned to f7822238ca)

Solutions

  1. Validate the program path rejects NUL before building the Command (a shared `reject_nul` helper).
  2. Trace and fix the producer of the NUL byte — the path is corrupted upstream, not merely unwelcome here.
  3. Sanitize at the trust boundary: strip or reject control bytes in user-supplied paths on ingestion.

Example fix

// before
let mut cmd = std::process::Command::new(&program_path); // program_path contains 0x00

// after
fn reject_nul(bytes: &[u8], what: &str) -> std::io::Result<()> {
  if bytes.contains(&0) {
    return Err(std::io::Error::new(
      std::io::ErrorKind::InvalidInput,
      format!("{what} contains NUL"),
    ));
  }
  Ok(())
}
reject_nul(program_path.as_bytes(), "program path")?;
let mut cmd = std::process::Command::new(&program_path);
Defensive patterns

Strategy: validation

Validate before calling

fn reject_nul(bytes: &[u8], what: &str) -> std::io::Result<()> {
  if bytes.contains(&0) {
    return Err(std::io::Error::new(
      std::io::ErrorKind::InvalidInput,
      format!("{what} contains NUL"),
    ));
  }
  Ok(())
}
// before spawning
reject_nul(cmd.get_program().as_bytes(), "program path")?;

Type guard

fn is_nul_free(s: &std::ffi::OsStr) -> bool {
  !s.as_bytes().contains(&0)
}

Try / catch

match spawn(cmd) {
  Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => {
    // reject the input upstream; do not retry unchanged
  }
  other => other,
}

Prevention

When it happens

Trigger: Spawning a subprocess whose program path was assembled from data containing a NUL byte — e.g. user/web input, a truncated buffer read as a path, or bytes misinterpreted as UTF-8 — reaching `posix_spawn`-based Child::spawn in cli/tools/desktop.rs.

Common situations: Passing request-derived strings into a spawn call without sanitization; binary protocols feeding path fields; environment or config values with embedded NULs from a broken upstream encoder.

Related errors


AI-assisted analysis of denoland/deno@f7822238ca (2026-08-20). Data as JSON: /api/errors/b533d22e15c5757a. Report an issue: GitHub.