-
Notifications
You must be signed in to change notification settings - Fork 238
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
fix: Prevent fork bomb on Windows #1761
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Thanks for tackling this! Can you comment on the specific trigger condition and the mechanics of how it recurses? Without a good understanding of the overall root cause of the recursion, it's difficult to be confident that we're completely resolving the issue. We're already working around some of the oddities of If that's the case, we may be able to resolve this in a less intrusive way by detecting the CWD is one of the Volta dirs. I worry a little about the extra copying / allocating needed to hold onto the command, args, and env vars through the process. It's a relatively small impact, but this block is on the hot path for every call to any of the shims, so it's one of the most performance-sensitive areas of the app. |
One thought, if the above is accurate for how this recursion starts, would be to check on Windows right before executing if the CWD is one of the Volta directories. If it is, we can do the absolute path resolution and then copy the values (since we’ll need to create a new |
I see that it had tried to remove the Volta directory from the
That's the root cause.
I get your thought here but it does not really help imo by just detecting CWD. It should only find the right executable by only checking from the PATH environment variable we give. (and this is why |
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Btw, I don't quite understand why the infinite recursive call is allowed. I believe there should be a limit of max recursive depth, say 10, and once it exceeds it should halt. This should help to resolve from another perspective. |
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
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.
Here’s a first pass review with some changes requested. @charlespierce may have some more insight on the Windows-specific bits!
crates/volta-core/src/command.rs
Outdated
if let Ok(mut paths) = which::which_in_global(name, Some(&path)) { | ||
if let Some(exe) = paths.next() { | ||
let mut new_command = create_command(exe); | ||
let envs = command |
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.
- Why does this need to filter the environment variables like this, rather than maintaining the same environment variables as the previous version of the command?
- Can you add a comment explaining that here? That will help us remember (and other contributors understand)!
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.
Agreed on adding a comment!
I believe this needs to filter because the output of Command::get_envs
includes None
values for variables that were explicitly removed. I don't think we do any explicit removing of environment variables currently, but possibly what we want to do is mirror the same behavior:
for (key, maybe_value) in command.get_envs() {
match maybe_value {
Some(value) => command.env(key, value),
None => command.env_remove(key),
}
}
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.
because the output of Command::get_envs includes None values for variables that were explicitly removed.
This was the reason to filter out envs, envs with None
values should not be added to the new_command
that's built.
Do you think it's better to use
for (key, maybe_value) in command.get_envs() {
match maybe_value {
Some(value) => command.env(key, value),
None => command.env_remove(key),
}
}
to apply envs to the new_command
or just leave the current implementation?
let envs = command
.get_envs()
.filter_map(|(k, maybe_v)| Some(k).zip(maybe_v))
.collect::<Vec<_>>();
new_command.envs(envs);
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.
Thanks for continuing on this! A few additional requests for cleanup.
Separately, and I haven't totally thought this through, but would it be unreasonable to directly error if one of the commands is run from either of the Volta directories? That might simplify the solution a lot, at the expense of making some specific uses errors when they otherwise wouldn't. However, those cases are currently fork bombs, so it would be an improvement even if it isn't a complete fix.
crates/volta-core/src/command.rs
Outdated
// args_idx 0 1 2.. | ||
let name = args.get(1).expect("should have the name"); | ||
|
||
if let Ok(mut paths) = which::which_in_global(name, Some(&path)) { |
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.
Given that the trigger for this (as I understand it) is the cmd /c
implementation searching the CWD for the binary, even when it's not explicitly on the Path
, would it be unreasonable to gate this reformulation of the Command
on that, rather than doing it every time on Windows?
It's relatively minor, but the which_in_global
is going to necessarily do a decent chunk of File I/O for every command run, which seems excessive for something on the hot path.
ErrorKind::RecursionLimit => write!( | ||
f, | ||
"Recursive call limit reached." | ||
), |
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 have a great suggestion on the top of my head, but is it impossible to create a call-to-action for this error? We have generally tried to always have a CTA included with any error message, so that we guide users to how to fix the problems they run into.
Co-authored-by: Chris Krycho <hello@chriskrycho.com>
Co-authored-by: Chris Krycho <hello@chriskrycho.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Doing some spelunking in our issues (while investigating some other, ostensibly unrelated, Windows issues), and I've come around on this approach. I actually think we might be able to do even better and use the behavior of
Bypassing Additionally, this would solve the linked issue around error messages, because if we can't locate the executable with It also (I think) would solve argument parsing issues like those mentioned in #1791, so this may be an altogether cleaner path forward. Thanks for suggesting and pushing on this @chawyehsu! |
Thanks for the informative response, would love to see improvement to this PR to make it a general solution to linked issues. @charlespierce |
Fix #1741
Before this patch,
volta-shim
(and its aliases) spawns itself in some cases causing recursive call (fork bomb). This is a critical issue but only occurs on Windows.