#![cfg(all(
feature = "cli",
feature = "embed",
feature = "test-support",
feature = "unix-runtime"
))]
mod support;
use std::time::Duration;
use support::run_shell_with_timeout;
const SHELL_TIMEOUT: Duration = Duration::from_secs(5);
fn assert_process_global_script_succeeds(script: &str, context: &str) {
let output = run_shell_with_timeout("mxsh", &["-s"], script, SHELL_TIMEOUT);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"{context}; stdout:\n{stdout}\nstderr:\n{stderr}"
);
}
fn assert_ulimit_restored_after_isolated_execution(option: &str, low_value: &str) {
let script = format!(
"\
old=$(ulimit {option})
(ulimit {option} {low_value})
after_subshell=$(ulimit {option})
from_substitution=$(ulimit {option} {low_value}; ulimit {option})
after_substitution=$(ulimit {option})
ulimit {option} {low_value} | cat >/dev/null
after_pipeline=$(ulimit {option})
printf 'old=%s after_subshell=%s after_substitution=%s after_pipeline=%s captured=%s\\n' \
\"$old\" \"$after_subshell\" \"$after_substitution\" \"$after_pipeline\" \"$from_substitution\"
test \"$old\" = \"$after_subshell\"
test \"$old\" = \"$after_substitution\"
test \"$old\" = \"$after_pipeline\"
"
);
assert_process_global_script_succeeds(
&script,
&format!("ulimit {option} should be restored after isolated execution"),
);
}
#[test]
fn exec_in_subshell_does_not_replace_parent_shell() {
let script = "(exec /bin/echo hi)\necho after\n";
let output = run_shell_with_timeout("mxsh", &["-s"], script, SHELL_TIMEOUT);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"mxsh failed with status {:?}\nstderr:\n{}",
output.status.code(),
stderr
);
assert_eq!(stdout, "hi\nafter\n");
}
#[test]
fn failed_exec_in_subshell_stops_only_the_subshell() {
let script = "(exec definitely-not-mxsh-command; echo bad)\necho after\n";
let output = run_shell_with_timeout("mxsh", &["-s"], script, SHELL_TIMEOUT);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"parent shell should continue after failed subshell exec; stderr:\n{stderr}"
);
assert_eq!(stdout, "after\n");
assert!(
stderr.contains("definitely-not-mxsh-command"),
"stderr should report the failed exec lookup:\n{stderr}"
);
}
#[test]
fn command_substitution_exec_does_not_replace_parent_shell() {
let script = "VALUE=$(exec /bin/echo hi)\nprintf '<%s>\\n' \"$VALUE\"\necho after\n";
let output = run_shell_with_timeout("mxsh", &["-s"], script, SHELL_TIMEOUT);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"mxsh failed with status {:?}\nstderr:\n{}",
output.status.code(),
stderr
);
assert_eq!(stdout, "<hi>\nafter\n");
}
#[test]
fn umask_changes_in_subshells_and_command_substitutions_do_not_leak() {
let script = "\
old=$(umask)
(umask 077)
after_subshell=$(umask)
from_substitution=$(umask 077)
after_substitution=$(umask)
printf 'old=%s after_subshell=%s after_substitution=%s captured=%s\\n' \
\"$old\" \"$after_subshell\" \"$after_substitution\" \"$from_substitution\"
test \"$old\" = \"$after_subshell\"
test \"$old\" = \"$after_substitution\"
";
let output = run_shell_with_timeout("mxsh", &["-s"], script, SHELL_TIMEOUT);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"umask should be restored after isolated execution; stdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[test]
fn ulimit_changes_in_isolated_execution_do_not_leak() {
assert_ulimit_restored_after_isolated_execution("-n", "32");
assert_ulimit_restored_after_isolated_execution("-f", "0");
}
#[test]
fn process_global_changes_through_local_functions_do_not_leak() {
let script = "\
old=$(ulimit -n)
(f() { ulimit -n 32; }; f)
after_subshell=$(ulimit -n)
from_substitution=$(f() { ulimit -n 32; ulimit -n; }; f)
after_substitution=$(ulimit -n)
printf 'old=%s after_subshell=%s after_substitution=%s captured=%s\\n' \
\"$old\" \"$after_subshell\" \"$after_substitution\" \"$from_substitution\"
test \"$old\" = \"$after_subshell\"
test \"$old\" = \"$after_substitution\"
test \"$from_substitution\" = 32
";
assert_process_global_script_succeeds(
script,
"process-global changes through locally defined functions should be restored",
);
}
#[test]
fn nested_process_global_pipeline_in_command_substitution_does_not_deadlock() {
let script = "\
old=$(ulimit -n)
from_substitution=$(ulimit -n 32; ulimit -n 32 | cat >/dev/null; ulimit -n)
after_substitution=$(ulimit -n)
printf 'old=%s after_substitution=%s captured=%s\\n' \
\"$old\" \"$after_substitution\" \"$from_substitution\"
test \"$old\" = \"$after_substitution\"
test \"$from_substitution\" = 32
";
assert_process_global_script_succeeds(
script,
"nested process-global pipeline in command substitution should not deadlock",
);
}
#[test]
fn hard_ulimit_changes_in_isolated_execution_do_not_leak() {
assert_ulimit_restored_after_isolated_execution("-SHn", "32");
assert_ulimit_restored_after_isolated_execution("-SHf", "0");
}
#[test]
fn trap_in_subshell_does_not_steal_parent_signal_trap() {
let script = "\
trap 'echo parent-trap' INT
(trap 'echo child-trap' INT)
kill -INT $$
echo after
";
let output = run_shell_with_timeout("mxsh", &["-s"], script, SHELL_TIMEOUT);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"mxsh failed with status {:?}\nstderr:\n{}",
output.status.code(),
stderr
);
assert_eq!(stdout, "parent-trap\nafter\n");
}