-
-
Notifications
You must be signed in to change notification settings - Fork 15k
Prevent mutating the global environment pointer in CommandExt::exec and opt to use execve and resolve path manually
#157144
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ use libc::{c_int, pid_t}; | |
| use libc::{gid_t, uid_t}; | ||
|
|
||
| use super::common::*; | ||
| use crate::ffi::CString; | ||
| use crate::io::{self, Error, ErrorKind}; | ||
| use crate::num::NonZero; | ||
| use crate::process::StdioPipes; | ||
|
|
@@ -384,28 +385,101 @@ impl Command { | |
| callback()?; | ||
| } | ||
|
|
||
| // Although we're performing an exec here we may also return with an | ||
| // error from this function (without actually exec'ing) in which case we | ||
| // want to be sure to restore the global environment back to what it | ||
| // once was, ensuring that our temporary override, when free'd, doesn't | ||
| // corrupt our process's environment. | ||
| let mut _reset = None; | ||
| if let Some(envp) = maybe_envp { | ||
| struct Reset(*const *const libc::c_char); | ||
|
|
||
| impl Drop for Reset { | ||
| fn drop(&mut self) { | ||
| unsafe { | ||
| *sys::env::environ() = self.0; | ||
| let file = self.get_program_cstr(); | ||
| let file_as_bytes = file.to_bytes_with_nul(); | ||
| let argv = self.get_argv(); | ||
| let parent_path = crate::env::var("PATH") | ||
| .map(|var| CString::new(var)) | ||
| .unwrap_or(CString::new("/bin:/usr/bin"))?; | ||
| let envp = maybe_envp.map(|envp| envp.as_ptr()).unwrap_or(*sys::env::environ()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably a good idea to hold the environment read lock if inheriting the environment from the parent.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't the environment read lock always held before
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh right, fair enough. That means that the parent
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That's fine, I think I can use |
||
|
|
||
| // This is the value associated with the `PATH` environment variable with colon delimiters | ||
| let mut paths = match maybe_envp { | ||
| Some(envp) => { | ||
| match envp.iter().find(|var| var.to_bytes_with_nul().starts_with(b"PATH=")) { | ||
| // Remove "PATH=" | ||
| Some(p) => &p.to_bytes_with_nul()[5..], | ||
| // Falls back to this if PATH environment variable couldn't be found | ||
| None => c"/bin:/usr/bin".to_bytes_with_nul(), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This default is incorrect. E.g. on GNU/Linux you should use |
||
| } | ||
| } | ||
| None => parent_path.as_bytes_with_nul(), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I recommend moving the environment variable lookup here so that it's only performed when actually needed. |
||
| }; | ||
|
|
||
| // Can't exec on an empty file path | ||
| if file.is_empty() { | ||
| return Err(io::Error::from_raw_os_error(libc::ENOENT)); | ||
| } | ||
| // Path searching does not occur when our file starts with a `/` | ||
| else if file_as_bytes.starts_with(&[b'/']) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This mismatches the POSIX specification, the file is interpreted as path if it contains a slash character in any place. Also, I'd move this check to before all the path stuff, that's redundant if this path is taken. |
||
| libc::execve(file.as_ptr(), argv.as_ptr(), envp); | ||
| } | ||
| // Ensure that our given file does not exceed the limit set by NAME_MAX | ||
| // Note: `file` is a CStr, so it should be guaranteed to have a nul terminated | ||
| // byte (hence no underflow occurs here) | ||
| else if file_as_bytes.len() - 1 >= libc::NAME_MAX as usize { | ||
| return Err(io::Error::from_raw_os_error(libc::ENAMETOOLONG)); | ||
|
Comment on lines
+417
to
+421
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd just leave this check to |
||
| } else { | ||
| let mut got_perm_denied = None; | ||
| while paths != &[b'\0'] { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole logic is equivalent to |
||
| let colon_pos = | ||
| paths.iter().position(|byte| *byte == b':').unwrap_or(paths.len() - 1); | ||
| let dir = &paths[..colon_pos]; | ||
| // Parsed path len from envp + file path len + 1 for separator byte | ||
| let mut binary_path = Vec::with_capacity(colon_pos + file.count_bytes() + 1); | ||
| binary_path.extend_from_slice(dir); | ||
| binary_path.push(b'/'); | ||
| binary_path.extend_from_slice(file.to_bytes()); | ||
| let cstr_path = CString::new(binary_path)?; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This searches for nuls even though none can be present (both |
||
|
|
||
| // Try execing with the path entry | ||
| libc::execve(cstr_path.as_ptr(), argv.as_ptr(), envp); | ||
|
|
||
| let err = io::Error::last_os_error(); | ||
| // File is accessible, but not executable as a file; invoke the shell to interpret | ||
| // it as a script. | ||
| if let Some(err) = err.raw_os_error() | ||
| && err == libc::ENOEXEC | ||
| { | ||
| let mut new_argv = CStringArray::with_capacity(argv.len() + 2); | ||
| new_argv.push(CString::new("/bin/sh")?); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This path is implementation defined – though I imagine it's correct for nearly all platforms. |
||
| new_argv.push(file.to_owned()); | ||
|
|
||
| for arg in argv.iter() { | ||
| new_argv.push(arg.to_owned()); | ||
| } | ||
|
|
||
| libc::execve(new_argv[0].as_ptr(), new_argv.as_ptr(), envp); | ||
| } | ||
|
|
||
| match err.kind() { | ||
| // Record that we got a 'Permission Denied' error in the event that if we | ||
| // find no executable to use, we should report that there was usable executable | ||
| // but we were denied access to it | ||
| io::ErrorKind::PermissionDenied => { | ||
| got_perm_denied = Some(err); | ||
| } | ||
| // Try the next path. Note on `ErrorKind::TimedOut`, unsure if this should | ||
| // return the error or continue with the next path, as #55359, continues the | ||
| // next path, but glibc posix impl of execvp breaks the path parsing loop | ||
| // on this error | ||
| io::ErrorKind::NotFound | io::ErrorKind::TimedOut => {} | ||
| _ => return Err(err), | ||
| } | ||
|
|
||
| paths = &paths[colon_pos + 1..]; | ||
| } | ||
|
|
||
| // At least one failure was due to lack of permissions, so we should | ||
| // report that failure first | ||
| if let Some(got_eaccess) = got_perm_denied { | ||
| return Err(got_eaccess); | ||
| } | ||
|
|
||
| _reset = Some(Reset(*sys::env::environ())); | ||
| *sys::env::environ() = envp.as_ptr(); | ||
| // No paths were executable | ||
| return Err(io::Error::from_raw_os_error(libc::ENOENT)); | ||
| } | ||
|
|
||
| libc::execvp(self.get_program_cstr().as_ptr(), self.get_argv().as_ptr()); | ||
| Err(io::Error::last_os_error()) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really like this duplication of the default path. Can you factor this differently?