From 553b7e67d70dbf72dd705616acaa496a20fba765 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 6 Feb 2014 22:38:05 -0800 Subject: [PATCH 1/2] Allow configuration of uid/gid/detach on processes This just copies the libuv implementation for libnative which seems reasonable enough (uid/gid fail on windows). Closes #12082 --- src/compiletest/procsrv.rs | 10 +-- src/libnative/io/process.rs | 95 +++++++++++++++----- src/librustuv/process.rs | 16 +++- src/libstd/io/process.rs | 129 +++++++++++++++++++++++----- src/libstd/libc.rs | 6 +- src/libstd/run.rs | 34 ++++++-- src/test/run-pass/issue-10626.rs | 5 +- src/test/run-pass/process-detach.rs | 56 ++++++++++++ 8 files changed, 287 insertions(+), 64 deletions(-) create mode 100644 src/test/run-pass/process-detach.rs diff --git a/src/compiletest/procsrv.rs b/src/compiletest/procsrv.rs index 019803a933779..1016c3cf0e61b 100644 --- a/src/compiletest/procsrv.rs +++ b/src/compiletest/procsrv.rs @@ -51,10 +51,7 @@ pub fn run(lib_path: &str, let env = env + target_env(lib_path, prog); let mut opt_process = run::Process::new(prog, args, run::ProcessOptions { env: Some(env), - dir: None, - in_fd: None, - out_fd: None, - err_fd: None + .. run::ProcessOptions::new() }); match opt_process { @@ -83,10 +80,7 @@ pub fn run_background(lib_path: &str, let env = env + target_env(lib_path, prog); let opt_process = run::Process::new(prog, args, run::ProcessOptions { env: Some(env), - dir: None, - in_fd: None, - out_fd: None, - err_fd: None + .. run::ProcessOptions::new() }); match opt_process { diff --git a/src/libnative/io/process.rs b/src/libnative/io/process.rs index b796535371f7a..affa3ebf54456 100644 --- a/src/libnative/io/process.rs +++ b/src/libnative/io/process.rs @@ -98,8 +98,8 @@ impl Process { let env = config.env.map(|a| a.to_owned()); let cwd = config.cwd.map(|a| Path::new(a)); - let res = spawn_process_os(config.program, config.args, env, - cwd.as_ref(), in_fd, out_fd, err_fd); + let res = spawn_process_os(config, env, cwd.as_ref(), in_fd, out_fd, + err_fd); unsafe { for pipe in in_pipe.iter() { let _ = libc::close(pipe.input); } @@ -180,7 +180,7 @@ struct SpawnProcessResult { } #[cfg(windows)] -fn spawn_process_os(prog: &str, args: &[~str], +fn spawn_process_os(config: p::ProcessConfig, env: Option<~[(~str, ~str)]>, dir: Option<&Path>, in_fd: c_int, out_fd: c_int, @@ -202,6 +202,14 @@ fn spawn_process_os(prog: &str, args: &[~str], use std::mem; + if config.gid.is_some() || config.uid.is_some() { + return Err(io::IoError { + kind: io::OtherIoError, + desc: "unsupported gid/uid requested on windows", + detail: None, + }) + } + unsafe { let mut si = zeroed_startupinfo(); @@ -237,16 +245,23 @@ fn spawn_process_os(prog: &str, args: &[~str], fail!("failure in DuplicateHandle: {}", os::last_os_error()); } - let cmd = make_command_line(prog, args); + let cmd = make_command_line(config.program, config.args); let mut pi = zeroed_process_information(); let mut create_err = None; + // stolen from the libuv code. + let mut flags = 0; + if config.detach { + flags |= libc::DETACHED_PROCESS | libc::CREATE_NEW_PROCESS_GROUP; + } + with_envp(env, |envp| { with_dirp(dir, |dirp| { cmd.with_c_str(|cmdp| { let created = CreateProcessA(ptr::null(), cast::transmute(cmdp), ptr::mut_null(), ptr::mut_null(), TRUE, - 0, envp, dirp, &mut si, &mut pi); + flags, envp, dirp, &mut si, + &mut pi); if created == FALSE { create_err = Some(super::last_error()); } @@ -364,7 +379,7 @@ fn make_command_line(prog: &str, args: &[~str]) -> ~str { } #[cfg(unix)] -fn spawn_process_os(prog: &str, args: &[~str], +fn spawn_process_os(config: p::ProcessConfig, env: Option<~[(~str, ~str)]>, dir: Option<&Path>, in_fd: c_int, out_fd: c_int, @@ -372,7 +387,6 @@ fn spawn_process_os(prog: &str, args: &[~str], use std::libc::funcs::posix88::unistd::{fork, dup2, close, chdir, execvp}; use std::libc::funcs::bsd44::getdtablesize; use std::libc::c_ulong; - use std::unstable::intrinsics; mod rustrt { extern { @@ -441,22 +455,34 @@ fn spawn_process_os(prog: &str, args: &[~str], } drop(input); + fn fail(output: &mut file::FileDesc) -> ! { + let errno = os::errno(); + let bytes = [ + (errno << 24) as u8, + (errno << 16) as u8, + (errno << 8) as u8, + (errno << 0) as u8, + ]; + assert!(output.inner_write(bytes).is_ok()); + unsafe { libc::_exit(1) } + } + rustrt::rust_unset_sigprocmask(); if in_fd == -1 { let _ = libc::close(libc::STDIN_FILENO); } else if retry(|| dup2(in_fd, 0)) == -1 { - fail!("failure in dup2(in_fd, 0): {}", os::last_os_error()); + fail(&mut output); } if out_fd == -1 { let _ = libc::close(libc::STDOUT_FILENO); } else if retry(|| dup2(out_fd, 1)) == -1 { - fail!("failure in dup2(out_fd, 1): {}", os::last_os_error()); + fail(&mut output); } if err_fd == -1 { let _ = libc::close(libc::STDERR_FILENO); } else if retry(|| dup2(err_fd, 2)) == -1 { - fail!("failure in dup3(err_fd, 2): {}", os::last_os_error()); + fail(&mut output); } // close all other fds for fd in range(3, getdtablesize()).rev() { @@ -465,9 +491,44 @@ fn spawn_process_os(prog: &str, args: &[~str], } } + match config.gid { + Some(u) => { + if libc::setgid(u as libc::gid_t) != 0 { + fail(&mut output); + } + } + None => {} + } + match config.uid { + Some(u) => { + // When dropping privileges from root, the `setgroups` call will + // remove any extraneous groups. If we don't call this, then + // even though our uid has dropped, we may still have groups + // that enable us to do super-user things. This will fail if we + // aren't root, so don't bother checking the return value, this + // is just done as an optimistic privilege dropping function. + extern { + fn setgroups(ngroups: libc::c_int, + ptr: *libc::c_void) -> libc::c_int; + } + let _ = setgroups(0, 0 as *libc::c_void); + + if libc::setuid(u as libc::uid_t) != 0 { + fail(&mut output); + } + } + None => {} + } + if config.detach { + // Don't check the error of setsid because it fails if we're the + // process leader already. We just forked so it shouldn't return + // error, but ignore it anyway. + let _ = libc::setsid(); + } + with_dirp(dir, |dirp| { if !dirp.is_null() && chdir(dirp) == -1 { - fail!("failure in chdir: {}", os::last_os_error()); + fail(&mut output); } }); @@ -476,17 +537,9 @@ fn spawn_process_os(prog: &str, args: &[~str], set_environ(envp); } }); - with_argv(prog, args, |argv| { + with_argv(config.program, config.args, |argv| { let _ = execvp(*argv, argv); - let errno = os::errno(); - let bytes = [ - (errno << 24) as u8, - (errno << 16) as u8, - (errno << 8) as u8, - (errno << 0) as u8, - ]; - assert!(output.inner_write(bytes).is_ok()); - intrinsics::abort(); + fail(&mut output); }) } } diff --git a/src/librustuv/process.rs b/src/librustuv/process.rs index e1f94d8c4df5a..a0623059bd716 100644 --- a/src/librustuv/process.rs +++ b/src/librustuv/process.rs @@ -58,6 +58,16 @@ impl Process { let ret = with_argv(config.program, config.args, |argv| { with_env(config.env, |envp| { + let mut flags = 0; + if config.uid.is_some() { + flags |= uvll::PROCESS_SETUID; + } + if config.gid.is_some() { + flags |= uvll::PROCESS_SETGID; + } + if config.detach { + flags |= uvll::PROCESS_DETACHED; + } let options = uvll::uv_process_options_t { exit_cb: on_exit, file: unsafe { *argv }, @@ -67,11 +77,11 @@ impl Process { Some(ref cwd) => cwd.with_ref(|p| p), None => ptr::null(), }, - flags: 0, + flags: flags as libc::c_uint, stdio_count: stdio.len() as libc::c_int, stdio: stdio.as_ptr(), - uid: 0, - gid: 0, + uid: config.uid.unwrap_or(0) as uvll::uv_uid_t, + gid: config.gid.unwrap_or(0) as uvll::uv_gid_t, }; let handle = UvHandle::alloc(None::, uvll::UV_PROCESS); diff --git a/src/libstd/io/process.rs b/src/libstd/io/process.rs index b515cd9d31c0e..6540fcd85d3fd 100644 --- a/src/libstd/io/process.rs +++ b/src/libstd/io/process.rs @@ -58,7 +58,21 @@ pub struct ProcessConfig<'a> { /// 0 - stdin /// 1 - stdout /// 2 - stderr - io: &'a [StdioContainer] + io: &'a [StdioContainer], + + /// Sets the child process's user id. This translates to a `setuid` call in + /// the child process. Setting this value on windows will cause the spawn to + /// fail. Failure in the `setuid` call on unix will also cause the spawn to + /// fail. + uid: Option, + + /// Similar to `uid`, but sets the group id of the child process. This has + /// the same semantics as the `uid` field. + gid: Option, + + /// If true, the child process is spawned in a detached state. On unix, this + /// means that the child is the leader of a new process group. + detach: bool, } /// Describes what to do with a standard io stream for a child process. @@ -115,6 +129,36 @@ impl ProcessExit { } } +impl<'a> ProcessConfig<'a> { + /// Creates a new configuration with blanks as all of the defaults. This is + /// useful when using functional struct updates: + /// + /// ```rust + /// use std::io::process::{ProcessConfig, Process}; + /// + /// let config = ProcessConfig { + /// program: "/bin/sh", + /// args: &'static [~"-c", ~"echo hello"], + /// .. ProcessConfig::new() + /// }; + /// + /// let p = Process::new(config); + /// ``` + /// + pub fn new() -> ProcessConfig<'static> { + ProcessConfig { + program: "", + args: &'static [], + env: None, + cwd: None, + io: &'static [], + uid: None, + gid: None, + detach: false, + } + } +} + impl Process { /// Creates a new pipe initialized, but not bound to any particular /// source/destination @@ -175,13 +219,10 @@ mod tests { // FIXME(#10380) #[cfg(unix, not(target_os="android"))] iotest!(fn smoke() { - let io = ~[]; let args = ProcessConfig { program: "/bin/sh", args: &[~"-c", ~"true"], - env: None, - cwd: None, - io: io, + .. ProcessConfig::new() }; let p = Process::new(args); assert!(p.is_ok()); @@ -192,13 +233,9 @@ mod tests { // FIXME(#10380) #[cfg(unix, not(target_os="android"))] iotest!(fn smoke_failure() { - let io = ~[]; let args = ProcessConfig { program: "if-this-is-a-binary-then-the-world-has-ended", - args: &[], - env: None, - cwd: None, - io: io, + .. ProcessConfig::new() }; match Process::new(args) { Ok(..) => fail!(), @@ -209,13 +246,10 @@ mod tests { // FIXME(#10380) #[cfg(unix, not(target_os="android"))] iotest!(fn exit_reported_right() { - let io = ~[]; let args = ProcessConfig { program: "/bin/sh", args: &[~"-c", ~"exit 1"], - env: None, - cwd: None, - io: io, + .. ProcessConfig::new() }; let p = Process::new(args); assert!(p.is_ok()); @@ -225,13 +259,10 @@ mod tests { #[cfg(unix, not(target_os="android"))] iotest!(fn signal_reported_right() { - let io = ~[]; let args = ProcessConfig { program: "/bin/sh", args: &[~"-c", ~"kill -1 $$"], - env: None, - cwd: None, - io: io, + .. ProcessConfig::new() }; let p = Process::new(args); assert!(p.is_ok()); @@ -264,9 +295,8 @@ mod tests { let args = ProcessConfig { program: "/bin/sh", args: &[~"-c", ~"echo foobar"], - env: None, - cwd: None, io: io, + .. ProcessConfig::new() }; assert_eq!(run_output(args), ~"foobar\n"); }) @@ -279,9 +309,9 @@ mod tests { let args = ProcessConfig { program: "/bin/sh", args: &[~"-c", ~"pwd"], - env: None, cwd: cwd, io: io, + .. ProcessConfig::new() }; assert_eq!(run_output(args), ~"/\n"); }) @@ -294,9 +324,8 @@ mod tests { let args = ProcessConfig { program: "/bin/sh", args: &[~"-c", ~"read line; echo $line"], - env: None, - cwd: None, io: io, + .. ProcessConfig::new() }; let mut p = Process::new(args).unwrap(); p.io[0].get_mut_ref().write("foobar".as_bytes()).unwrap(); @@ -306,4 +335,58 @@ mod tests { assert_eq!(out, ~"foobar\n"); }) + // FIXME(#10380) + #[cfg(unix, not(target_os="android"))] + iotest!(fn detach_works() { + let args = ProcessConfig { + program: "/bin/sh", + args: &[~"-c", ~"true"], + detach: true, + .. ProcessConfig::new() + }; + let mut p = Process::new(args).unwrap(); + assert!(p.wait().success()); + }) + + #[cfg(windows)] + iotest!(fn uid_fails_on_windows() { + let args = ProcessConfig { + program: "test", + uid: Some(10), + .. ProcessConfig::new() + }; + assert!(Process::new(args).is_err()); + }) + + // FIXME(#10380) + #[cfg(unix, not(target_os="android"))] + iotest!(fn uid_works() { + use libc; + let args = ProcessConfig { + program: "/bin/sh", + args: &[~"-c", ~"true"], + uid: Some(unsafe { libc::getuid() as uint }), + gid: Some(unsafe { libc::getgid() as uint }), + .. ProcessConfig::new() + }; + let mut p = Process::new(args).unwrap(); + assert!(p.wait().success()); + }) + + // FIXME(#10380) + #[cfg(unix, not(target_os="android"))] + iotest!(fn uid_to_root_fails() { + use libc; + + // if we're already root, this isn't a valid test. Most of the bots run + // as non-root though (android is an exception). + if unsafe { libc::getuid() == 0 } { return } + let args = ProcessConfig { + program: "/bin/ls", + uid: Some(0), + gid: Some(0), + .. ProcessConfig::new() + }; + assert!(Process::new(args).is_err()); + }) } diff --git a/src/libstd/libc.rs b/src/libstd/libc.rs index 39383f993924c..7dc4c692f6319 100644 --- a/src/libstd/libc.rs +++ b/src/libstd/libc.rs @@ -159,7 +159,7 @@ pub use libc::funcs::c95::stdio::{fread, freopen, fseek, fsetpos, ftell}; pub use libc::funcs::c95::stdio::{fwrite, perror, puts, remove, rewind}; pub use libc::funcs::c95::stdio::{setbuf, setvbuf, tmpfile, ungetc}; -pub use libc::funcs::c95::stdlib::{abs, atof, atoi, calloc, exit}; +pub use libc::funcs::c95::stdlib::{abs, atof, atoi, calloc, exit, _exit}; pub use libc::funcs::c95::stdlib::{free, getenv, labs, malloc, rand}; pub use libc::funcs::c95::stdlib::{realloc, srand, strtod, strtol}; pub use libc::funcs::c95::stdlib::{strtoul, system}; @@ -1769,6 +1769,9 @@ pub mod consts { pub static MAX_PROTOCOL_CHAIN: DWORD = 7; pub static WSAPROTOCOL_LEN: DWORD = 255; pub static INVALID_SOCKET: DWORD = !0; + + pub static DETACHED_PROCESS: DWORD = 0x00000008; + pub static CREATE_NEW_PROCESS_GROUP: DWORD = 0x00000200; } pub mod sysconf { } @@ -3340,6 +3343,7 @@ pub mod funcs { pub fn realloc(p: *mut c_void, size: size_t) -> *mut c_void; pub fn free(p: *mut c_void); pub fn exit(status: c_int) -> !; + pub fn _exit(status: c_int) -> !; // Omitted: atexit. pub fn system(s: *c_char) -> c_int; pub fn getenv(s: *c_char) -> *c_char; diff --git a/src/libstd/run.rs b/src/libstd/run.rs index 6f684f23d4760..9ea8f6447dd6a 100644 --- a/src/libstd/run.rs +++ b/src/libstd/run.rs @@ -88,6 +88,20 @@ pub struct ProcessOptions<'a> { * and Process.error() will fail. */ err_fd: Option, + + /// The uid to assume for the child process. For more information, see the + /// documentation in `io::process::ProcessConfig` about this field. + uid: Option, + + /// The gid to assume for the child process. For more information, see the + /// documentation in `io::process::ProcessConfig` about this field. + gid: Option, + + /// Flag as to whether the child process will be the leader of a new process + /// group or not. This allows the parent process to exit while the child is + /// still running. For more information, see the documentation in + /// `io::process::ProcessConfig` about this field. + detach: bool, } impl <'a> ProcessOptions<'a> { @@ -99,6 +113,9 @@ impl <'a> ProcessOptions<'a> { in_fd: None, out_fd: None, err_fd: None, + uid: None, + gid: None, + detach: false, } } } @@ -128,7 +145,9 @@ impl Process { */ pub fn new(prog: &str, args: &[~str], options: ProcessOptions) -> io::IoResult { - let ProcessOptions { env, dir, in_fd, out_fd, err_fd } = options; + let ProcessOptions { + env, dir, in_fd, out_fd, err_fd, uid, gid, detach + } = options; let env = env.as_ref().map(|a| a.as_slice()); let cwd = dir.as_ref().map(|a| a.as_str().unwrap()); fn rtify(fd: Option, input: bool) -> process::StdioContainer { @@ -145,6 +164,9 @@ impl Process { env: env, cwd: cwd, io: rtio, + uid: uid, + gid: gid, + detach: detach, }; process::Process::new(rtconfig).map(|p| Process { inner: p }) } @@ -302,11 +324,10 @@ impl Process { */ pub fn process_status(prog: &str, args: &[~str]) -> io::IoResult { Process::new(prog, args, ProcessOptions { - env: None, - dir: None, in_fd: Some(unsafe { libc::dup(libc::STDIN_FILENO) }), out_fd: Some(unsafe { libc::dup(libc::STDOUT_FILENO) }), - err_fd: Some(unsafe { libc::dup(libc::STDERR_FILENO) }) + err_fd: Some(unsafe { libc::dup(libc::STDERR_FILENO) }), + .. ProcessOptions::new() }).map(|mut p| p.finish()) } @@ -396,11 +417,10 @@ mod tests { let pipe_err = os::pipe(); let mut process = run::Process::new("cat", [], run::ProcessOptions { - dir: None, - env: None, in_fd: Some(pipe_in.input), out_fd: Some(pipe_out.out), - err_fd: Some(pipe_err.out) + err_fd: Some(pipe_err.out), + .. run::ProcessOptions::new() }).unwrap(); os::close(pipe_in.input as int); diff --git a/src/test/run-pass/issue-10626.rs b/src/test/run-pass/issue-10626.rs index 3529551de81f7..94964fbc89bc7 100644 --- a/src/test/run-pass/issue-10626.rs +++ b/src/test/run-pass/issue-10626.rs @@ -33,7 +33,10 @@ pub fn main () { args : &[~"child"], env : None, cwd : None, - io : &[] + io : &[], + uid: None, + gid: None, + detach: false, }; let mut p = process::Process::new(config).unwrap(); diff --git a/src/test/run-pass/process-detach.rs b/src/test/run-pass/process-detach.rs new file mode 100644 index 0000000000000..91f77caf8a3d1 --- /dev/null +++ b/src/test/run-pass/process-detach.rs @@ -0,0 +1,56 @@ +// Copyright 2014 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// ignore-fast +// ignore-win32 +// ignore-android + +// This test ensures that the 'detach' field on processes does the right thing. +// By detaching the child process, they should be put into a separate process +// group. We test this by spawning a detached process, then killing our own +// group with a signal. +// +// Note that the first thing we do is put ourselves in our own process group so +// we don't interfere with other running tests. + +use std::libc; +use std::io::process; +use std::io::signal::{Listener, Interrupt}; + +fn main() { + unsafe { libc::setsid(); } + + let config = process::ProcessConfig { + program : "/bin/sh", + args : &[~"-c", ~"read a"], + io : &[process::CreatePipe(true, false)], + detach: true, + .. process::ProcessConfig::new() + }; + + // we shouldn't die because of an interrupt + let mut l = Listener::new(); + l.register(Interrupt).unwrap(); + + // spawn the child + let mut p = process::Process::new(config).unwrap(); + + // send an interrupt to everyone in our process group + unsafe { libc::funcs::posix88::signal::kill(0, libc::SIGINT); } + + // Wait for the child process to die (terminate it's stdin and the read + // should fail). + drop(p.io[0].take()); + match p.wait() { + process::ExitStatus(..) => {} + process::ExitSignal(..) => fail!() + } +} + From 74b42c646eb90ff267b131fcb2ac8d25428538c5 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Sun, 16 Feb 2014 12:57:40 -0800 Subject: [PATCH 2/2] Upgrade libuv This notably includes joyent/libuv@6f62d62c in order to fix when processes fail to spawn on windows --- src/libuv | 2 +- src/rt/libuv-auto-clean-trigger | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libuv b/src/libuv index fd5308383c575..800b56fe6af21 160000 --- a/src/libuv +++ b/src/libuv @@ -1 +1 @@ -Subproject commit fd5308383c575472edb2163d823dc6670bf59609 +Subproject commit 800b56fe6af21ffd8e56aee8cf12dd758f5bbdf1 diff --git a/src/rt/libuv-auto-clean-trigger b/src/rt/libuv-auto-clean-trigger index ca0809cb0901a..961455b9093be 100644 --- a/src/rt/libuv-auto-clean-trigger +++ b/src/rt/libuv-auto-clean-trigger @@ -1,2 +1,2 @@ # Change the contents of this file to force a full rebuild of libuv -2013-12-23 +2014-02-16