Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

std: Properly handle interior NULs in std::process #31056

Merged
merged 1 commit into from
Feb 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 51 additions & 5 deletions src/libstd/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,11 +343,7 @@ impl fmt::Debug for Command {
/// non-utf8 data is lossily converted using the utf8 replacement
/// character.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
try!(write!(f, "{:?}", self.inner.program));
for arg in &self.inner.args {
try!(write!(f, " {:?}", arg));
}
Ok(())
self.inner.fmt(f)
}
}

Expand Down Expand Up @@ -877,4 +873,54 @@ mod tests {
assert!(output.contains("RUN_TEST_NEW_ENV=123"),
"didn't find RUN_TEST_NEW_ENV inside of:\n\n{}", output);
}

// Regression tests for #30858.
#[test]
fn test_interior_nul_in_progname_is_error() {
match Command::new("has-some-\0\0s-inside").spawn() {
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
Ok(_) => panic!(),
}
}

#[test]
fn test_interior_nul_in_arg_is_error() {
match Command::new("echo").arg("has-some-\0\0s-inside").spawn() {
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
Ok(_) => panic!(),
}
}

#[test]
fn test_interior_nul_in_args_is_error() {
match Command::new("echo").args(&["has-some-\0\0s-inside"]).spawn() {
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
Ok(_) => panic!(),
}
}

#[test]
fn test_interior_nul_in_current_dir_is_error() {
match Command::new("echo").current_dir("has-some-\0\0s-inside").spawn() {
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
Ok(_) => panic!(),
}
}

// Regression tests for #30862.
#[test]
fn test_interior_nul_in_env_key_is_error() {
match env_cmd().env("has-some-\0\0s-inside", "value").spawn() {
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
Ok(_) => panic!(),
}
}

#[test]
fn test_interior_nul_in_env_value_is_error() {
match env_cmd().env("key", "has-some-\0\0s-inside").spawn() {
Err(e) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
Ok(_) => panic!(),
}
}
}
6 changes: 3 additions & 3 deletions src/libstd/sys/unix/ext/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,17 @@ pub trait CommandExt {
#[stable(feature = "rust1", since = "1.0.0")]
impl CommandExt for process::Command {
fn uid(&mut self, id: uid_t) -> &mut process::Command {
self.as_inner_mut().uid = Some(id);
self.as_inner_mut().uid(id);
self
}

fn gid(&mut self, id: gid_t) -> &mut process::Command {
self.as_inner_mut().gid = Some(id);
self.as_inner_mut().gid(id);
self
}

fn session_leader(&mut self, on: bool) -> &mut process::Command {
self.as_inner_mut().session_leader = on;
self.as_inner_mut().session_leader(on);
self
}
}
Expand Down
72 changes: 57 additions & 15 deletions src/libstd/sys/unix/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,57 +31,95 @@ use sys::{self, cvt, cvt_r};

#[derive(Clone)]
pub struct Command {
pub program: CString,
pub args: Vec<CString>,
pub env: Option<HashMap<OsString, OsString>>,
pub cwd: Option<CString>,
pub uid: Option<uid_t>,
pub gid: Option<gid_t>,
pub session_leader: bool,
program: CString,
args: Vec<CString>,
env: Option<HashMap<OsString, OsString>>, // Guaranteed to have no NULs.
cwd: Option<CString>,
uid: Option<uid_t>,
gid: Option<gid_t>,
session_leader: bool,
saw_nul: bool,
}

impl Command {
pub fn new(program: &OsStr) -> Command {
let mut saw_nul = false;
Command {
program: os2c(program),
program: os2c(program, &mut saw_nul),
args: Vec::new(),
env: None,
cwd: None,
uid: None,
gid: None,
session_leader: false,
saw_nul: saw_nul,
}
}

pub fn arg(&mut self, arg: &OsStr) {
self.args.push(os2c(arg));
self.args.push(os2c(arg, &mut self.saw_nul));
}
pub fn args<'a, I: Iterator<Item = &'a OsStr>>(&mut self, args: I) {
self.args.extend(args.map(os2c));
let mut saw_nul = self.saw_nul;
self.args.extend(args.map(|arg| os2c(arg, &mut saw_nul)));
self.saw_nul = saw_nul;
}
fn init_env_map(&mut self) {
if self.env.is_none() {
// Will not add NULs to env: preexisting environment will not contain any.
self.env = Some(env::vars_os().collect());
}
}
pub fn env(&mut self, key: &OsStr, val: &OsStr) {
let k = OsString::from_vec(os2c(key, &mut self.saw_nul).into_bytes());
let v = OsString::from_vec(os2c(val, &mut self.saw_nul).into_bytes());

// Will not add NULs to env: return without inserting if any were seen.
if self.saw_nul {
return;
}

self.init_env_map();
self.env.as_mut().unwrap().insert(key.to_os_string(), val.to_os_string());
self.env.as_mut()
.unwrap()
.insert(k, v);
}
pub fn env_remove(&mut self, key: &OsStr) {
self.init_env_map();
self.env.as_mut().unwrap().remove(&key.to_os_string());
self.env.as_mut().unwrap().remove(key);
}
pub fn env_clear(&mut self) {
self.env = Some(HashMap::new())
}
pub fn cwd(&mut self, dir: &OsStr) {
self.cwd = Some(os2c(dir));
self.cwd = Some(os2c(dir, &mut self.saw_nul));
}
pub fn uid(&mut self, id: uid_t) {
self.uid = Some(id);
}
pub fn gid(&mut self, id: gid_t) {
self.gid = Some(id);
}
pub fn session_leader(&mut self, session_leader: bool) {
self.session_leader = session_leader;
}
}

fn os2c(s: &OsStr, saw_nul: &mut bool) -> CString {
CString::new(s.as_bytes()).unwrap_or_else(|_e| {
*saw_nul = true;
CString::new("<string-with-nul>").unwrap()
})
}

fn os2c(s: &OsStr) -> CString {
CString::new(s.as_bytes()).unwrap()
impl fmt::Debug for Command {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
try!(write!(f, "{:?}", self.program));
for arg in &self.args {
try!(write!(f, " {:?}", arg));
}
Ok(())
}
}

////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -175,6 +213,10 @@ impl Process {
in_fd: Stdio,
out_fd: Stdio,
err_fd: Stdio) -> io::Result<Process> {
if cfg.saw_nul {
return Err(io::Error::new(ErrorKind::InvalidInput, "nul byte found in provided data"));
}

let dirp = cfg.cwd.as_ref().map(|c| c.as_ptr()).unwrap_or(ptr::null());

let (envp, _a, _b) = make_envp(cfg.env.as_ref());
Expand Down
81 changes: 52 additions & 29 deletions src/libstd/sys/windows/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use env;
use ffi::{OsString, OsStr};
use fmt;
use fs;
use io::{self, Error};
use io::{self, Error, ErrorKind};
use libc::c_void;
use mem;
use os::windows::ffi::OsStrExt;
Expand All @@ -43,13 +43,21 @@ fn mk_key(s: &OsStr) -> OsString {
})
}

fn ensure_no_nuls<T: AsRef<OsStr>>(str: T) -> io::Result<T> {
if str.as_ref().encode_wide().any(|b| b == 0) {
Err(io::Error::new(ErrorKind::InvalidInput, "nul byte found in provided data"))
} else {
Ok(str)
}
}

#[derive(Clone)]
pub struct Command {
pub program: OsString,
pub args: Vec<OsString>,
pub env: Option<HashMap<OsString, OsString>>,
pub cwd: Option<OsString>,
pub detach: bool, // not currently exposed in std::process
program: OsString,
args: Vec<OsString>,
env: Option<HashMap<OsString, OsString>>,
cwd: Option<OsString>,
detach: bool, // not currently exposed in std::process
}

impl Command {
Expand Down Expand Up @@ -92,6 +100,16 @@ impl Command {
}
}

impl fmt::Debug for Command {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
try!(write!(f, "{:?}", self.program));
for arg in &self.args {
try!(write!(f, " {:?}", arg));
}
Ok(())
}
}

////////////////////////////////////////////////////////////////////////////////
// Processes
////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -153,7 +171,7 @@ impl Process {
si.hStdError = stderr.raw();

let program = program.as_ref().unwrap_or(&cfg.program);
let mut cmd_str = make_command_line(program, &cfg.args);
let mut cmd_str = try!(make_command_line(program, &cfg.args));
cmd_str.push(0); // add null terminator

// stolen from the libuv code.
Expand All @@ -162,8 +180,8 @@ impl Process {
flags |= c::DETACHED_PROCESS | c::CREATE_NEW_PROCESS_GROUP;
}

let (envp, _data) = make_envp(cfg.env.as_ref());
let (dirp, _data) = make_dirp(cfg.cwd.as_ref());
let (envp, _data) = try!(make_envp(cfg.env.as_ref()));
let (dirp, _data) = try!(make_dirp(cfg.cwd.as_ref()));
let mut pi = zeroed_process_information();
try!(unsafe {
// `CreateProcess` is racy!
Expand Down Expand Up @@ -265,22 +283,24 @@ fn zeroed_process_information() -> c::PROCESS_INFORMATION {
}
}

// Produces a wide string *without terminating null*
fn make_command_line(prog: &OsStr, args: &[OsString]) -> Vec<u16> {
// Produces a wide string *without terminating null*; returns an error if
// `prog` or any of the `args` contain a nul.
fn make_command_line(prog: &OsStr, args: &[OsString]) -> io::Result<Vec<u16>> {
// Encode the command and arguments in a command line string such
// that the spawned process may recover them using CommandLineToArgvW.
let mut cmd: Vec<u16> = Vec::new();
append_arg(&mut cmd, prog);
try!(append_arg(&mut cmd, prog));
for arg in args {
cmd.push(' ' as u16);
append_arg(&mut cmd, arg);
try!(append_arg(&mut cmd, arg));
}
return cmd;
return Ok(cmd);

fn append_arg(cmd: &mut Vec<u16>, arg: &OsStr) {
fn append_arg(cmd: &mut Vec<u16>, arg: &OsStr) -> io::Result<()> {
// If an argument has 0 characters then we need to quote it to ensure
// that it actually gets passed through on the command line or otherwise
// it will be dropped entirely when parsed on the other end.
try!(ensure_no_nuls(arg));
let arg_bytes = &arg.as_inner().inner.as_inner();
let quote = arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t')
|| arg_bytes.is_empty();
Expand Down Expand Up @@ -312,11 +332,12 @@ fn make_command_line(prog: &OsStr, args: &[OsString]) -> Vec<u16> {
}
cmd.push('"' as u16);
}
Ok(())
}
}

fn make_envp(env: Option<&collections::HashMap<OsString, OsString>>)
-> (*mut c_void, Vec<u16>) {
-> io::Result<(*mut c_void, Vec<u16>)> {
// On Windows we pass an "environment block" which is not a char**, but
// rather a concatenation of null-terminated k=v\0 sequences, with a final
// \0 to terminate.
Expand All @@ -325,26 +346,27 @@ fn make_envp(env: Option<&collections::HashMap<OsString, OsString>>)
let mut blk = Vec::new();

for pair in env {
blk.extend(pair.0.encode_wide());
blk.extend(try!(ensure_no_nuls(pair.0)).encode_wide());
blk.push('=' as u16);
blk.extend(pair.1.encode_wide());
blk.extend(try!(ensure_no_nuls(pair.1)).encode_wide());
blk.push(0);
}
blk.push(0);
(blk.as_mut_ptr() as *mut c_void, blk)
Ok((blk.as_mut_ptr() as *mut c_void, blk))
}
_ => (ptr::null_mut(), Vec::new())
_ => Ok((ptr::null_mut(), Vec::new()))
}
}

fn make_dirp(d: Option<&OsString>) -> (*const u16, Vec<u16>) {
fn make_dirp(d: Option<&OsString>) -> io::Result<(*const u16, Vec<u16>)> {

match d {
Some(dir) => {
let mut dir_str: Vec<u16> = dir.encode_wide().collect();
let mut dir_str: Vec<u16> = try!(ensure_no_nuls(dir)).encode_wide().collect();
dir_str.push(0);
(dir_str.as_ptr(), dir_str)
Ok((dir_str.as_ptr(), dir_str))
},
None => (ptr::null(), Vec::new())
None => Ok((ptr::null(), Vec::new()))
}
}

Expand Down Expand Up @@ -397,11 +419,12 @@ mod tests {
#[test]
fn test_make_command_line() {
fn test_wrapper(prog: &str, args: &[&str]) -> String {
String::from_utf16(
&make_command_line(OsStr::new(prog),
&args.iter()
.map(|a| OsString::from(a))
.collect::<Vec<OsString>>())).unwrap()
let command_line = &make_command_line(OsStr::new(prog),
&args.iter()
.map(|a| OsString::from(a))
.collect::<Vec<OsString>>())
.unwrap();
String::from_utf16(command_line).unwrap()
}

assert_eq!(
Expand Down