-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @aturon (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
I think both issues will manifest on Windows as well, but I made no changes there. The added regression tests will likely cause failures on Windows. |
// to that point and report an error via a `Result` rather than by | ||
// panicking. | ||
#[derive(Debug, Clone)] | ||
pub enum ValidatedCString { |
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.
In the case that an invalid string is passed down, we don't necessarily need to store the original data, so perhaps this could just store a flag in Command
as to whether something with a nul byte in it has been passed in? That way once we hit spawn
we can just check the flag and return an error appropriately.
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 mostly kept it so the Debug
impl on std::process::Command
would display something. I can change this though.
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.
Ah that's a good point. The Debug
impl would probably be fine to start including something like <string-with-nul>
or something like that instead of the actual value, however. It seems relatively complicated to define a type like this just for a Debug
implementation that's unlikely to ever be seen.
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.
Maybe (ab)use Option
for this?
pub struct Command {
program: Option<CString>,
args: Vec<Optrion<ValidatedCString>>,
env: Option<HashMap<Option<CString>, Option<ValidatedCString>>>,
cwd: Option<Option<CString>>,
uid: Option<uid_t>,
gid: Option<gid_t>,
session_leader: bool,
saw_nul: bool,
}
The Debug
impl could then use unwrap_or("<string-with-nul>")
.
This is a bit weird for env, as the hash map would lose context of how many vars were nulful. The cwd would be a bit unwieldy with the double Option
...
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.
@alexcrichton any thoughts on how best to handle this?
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.
Oh I wouldn't change the internal structure of Command
much, just add a flag indicating that something with a nul byte was passed in. The Debug
implementation only shows the program and its arguments, so if we really want we can store CString::new("<string-with-nul>").unwrap()
in those places, but other than that we don't need to track the information elsewhere.
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.
Ok, so that means dropping this whole ValidatedCString
thing, right? Shall I close this and open an new PR skipping that altogether?
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 also don't have a good grasp on what Windows does here. I think my tests on std::process
will fail there, but without seeing the failures I'm not sure what to do to fix it.
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.
Nah keeping this PR is fine (just pushing the changes to it).
I do think that the tests will fail on Windows as well, but it should be pretty straightforward (e.g. checking encode_wide()
for zeros) and relatively the same implementation as Unix.
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've changed the unix version. I'll try to make changes for Windows, but it'll be kind of fumbling in the dark.
I marked this as a regression since it changes behavior, but I'm in favor. |
Argh I missed the warnings and didn't realise they were errors in the build. Just pushed something that should pass. There are still a couple of open discussion points though. |
} | ||
fn init_env_map(&mut self) { | ||
if self.env.is_none() { | ||
self.env = Some(env::vars_os().collect()); | ||
// Will not add NULs to env: preexisting environment will not contain any. | ||
self.env = Some(env::vars_os().map(|(k, v)| (k, v)).collect()); |
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 think that this needs to change, right?
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.
erm yeah that's a non-perfect refactor of an intermediate stage I had.... time to apply map id => id
...
Looks good to me! I think the Windows implementation will just need to be tweaked and this should be good to go. If you want to test things out it and you're on Unix you may be able to use this helper script I have to at least ensure the standard library itself compiles, I suspect this is a situation where "when it compiles it works" :) |
Thanks for the script! |
Sorry for letting this go for so long. I kept putting off learning about |
@@ -43,13 +43,25 @@ fn mk_key(s: &OsStr) -> OsString { | |||
}) | |||
} | |||
|
|||
fn ensure_no_nuls<T: AsRef<OsStr>>(str: T) -> io::Result<T> { | |||
let has_nul = { | |||
let bytes = str.as_ref().as_inner().inner.as_inner(); |
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.
Ah can this actually use the .encode_wide()
method? That's what'll be passed down to the OS anyway, and is a little more reliably than looking at the internal bytes.
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.
The environment block is built up using .extend()
, and I wanted to avoid the extra allocation and copy of the keys and values that would be necessary to check the .encode_wide()
output. See https://github.com/rust-lang/rust/pull/31056/files#diff-9a0a769432651d9c59644e0a8c7f887eR353
We can avoid the repeated allocation by reusing a buffer for the conversion, but we'd still pay the extra copy cost. I'm happy to do this however you prefer, though.
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.
Oh I see what you mean. Ignore above!
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.
Done!
Thanks @kamalmarhubi! Looks good to me modulo one nit and I'd be fine sending to bors after. |
Yay! I'm really excited to see what the bors experience is like, so I'll try and keep this moving. :-) |
Thanks! Can you squash the commits down into one as well? (sorry forgot to check that last time) |
b3d4b4e
to
59d070c
Compare
Done, and rebased. |
@bors: r+ 59d070c0d684926de73fe0400096b63091798de7 |
⌛ Testing commit 59d070c with merge 28c6780... |
💔 Test failed - auto-win-gnu-64-nopt-t |
This reports an error at the point of calling `Command::spawn()` or one of its equivalents. Fixes rust-lang#30858 Fixes rust-lang#30862
59d070c
to
7c64bf1
Compare
Fixed test and squashed. Diff: kamalmarhubi/rust@59d070c...7c64bf1 |
Urg pushed the wrong thing. |
Actually no, I pushed the right thing I just can't get the compare url to display as I'd like. Here's the diff: diff --git a/src/libstd/sys/windows/process.rs b/src/libstd/sys/windows/process.rs
index 758044c..61cf28b 100644
--- a/src/libstd/sys/windows/process.rs
+++ b/src/libstd/sys/windows/process.rs
@@ -419,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!( |
This reports an error at the point of calling
Command::spawn()
or one ofits equivalents.
Fixes #30858
Fixes #30862