Merge pull request #6105 from cre4ture/fix/findings_for_env_string_args

Fix/findings for env string args
This commit is contained in:
Sylvestre Ledru 2024-03-23 10:17:14 +01:00 committed by GitHub
commit 06e80c7af1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

391
src/uu/env/src/env.rs vendored
View file

@ -292,11 +292,12 @@ impl EnvAppData {
Ok(all_args)
}
#[allow(clippy::cognitive_complexity)]
fn run_env(&mut self, original_args: impl uucore::Args) -> UResult<()> {
fn parse_arguments(
&mut self,
original_args: impl uucore::Args,
) -> Result<(Vec<OsString>, clap::ArgMatches), Box<dyn UError>> {
let original_args: Vec<OsString> = original_args.collect();
let args = self.process_all_string_arguments(&original_args)?;
let app = uu_app();
let matches = app
.try_get_matches_from(args)
@ -316,6 +317,11 @@ impl EnvAppData {
}
}
})?;
Ok((original_args, matches))
}
fn run_env(&mut self, original_args: impl uucore::Args) -> UResult<()> {
let (original_args, matches) = self.parse_arguments(original_args)?;
let did_debug_printing_before = self.do_debug_printing; // could have been done already as part of the "-vS" string parsing
let do_debug_printing = self.do_debug_printing || matches.get_flag("debug");
@ -323,202 +329,227 @@ impl EnvAppData {
debug_print_args(&original_args);
}
let ignore_env = matches.get_flag("ignore-environment");
let line_ending = LineEnding::from_zero_flag(matches.get_flag("null"));
let running_directory = matches.get_one::<OsString>("chdir").map(|s| s.as_os_str());
let files = match matches.get_many::<OsString>("file") {
Some(v) => v.map(|s| s.as_os_str()).collect(),
None => Vec::with_capacity(0),
};
let unsets = match matches.get_many::<OsString>("unset") {
Some(v) => v.map(|s| s.as_os_str()).collect(),
None => Vec::with_capacity(0),
};
let mut opts = make_options(&matches)?;
let mut opts = Options {
ignore_env,
line_ending,
running_directory,
files,
unsets,
sets: vec![],
program: vec![],
};
// change directory
if let Some(d) = opts.running_directory {
match env::set_current_dir(d) {
Ok(()) => d,
Err(error) => {
return Err(USimpleError::new(
125,
format!("cannot change directory to {}: {error}", d.quote()),
));
}
};
}
let mut begin_prog_opts = false;
if let Some(mut iter) = matches.get_many::<OsString>("vars") {
// read NAME=VALUE arguments (and up to a single program argument)
while !begin_prog_opts {
if let Some(opt) = iter.next() {
if opt == "-" {
opts.ignore_env = true;
} else {
begin_prog_opts = parse_name_value_opt(&mut opts, opt)?;
}
} else {
break;
}
}
// read any leftover program arguments
for opt in iter {
parse_program_opt(&mut opts, opt)?;
}
}
// GNU env tests this behavior
if opts.program.is_empty() && running_directory.is_some() {
return Err(UUsageError::new(
125,
"must specify command with --chdir (-C)".to_string(),
));
}
apply_change_directory(&opts)?;
// NOTE: we manually set and unset the env vars below rather than using Command::env() to more
// easily handle the case where no command is given
// remove all env vars if told to ignore presets
if opts.ignore_env {
for (ref name, _) in env::vars_os() {
env::remove_var(name);
}
}
apply_removal_of_all_env_vars(&opts);
// load .env-style config file prior to those given on the command-line
load_config_file(&mut opts)?;
// unset specified env vars
for name in &opts.unsets {
let native_name = NativeStr::new(name);
if name.is_empty()
|| native_name.contains(&'\0').unwrap()
|| native_name.contains(&'=').unwrap()
{
return Err(USimpleError::new(
125,
format!("cannot unset {}: Invalid argument", name.quote()),
));
}
apply_unset_env_vars(&opts)?;
env::remove_var(name);
}
// set specified env vars
for (name, val) in &opts.sets {
/*
* set_var panics if name is an empty string
* set_var internally calls setenv (on unix at least), while GNU env calls putenv instead.
*
* putenv returns successfully if provided with something like "=a" and modifies the environ
* variable to contain "=a" inside it, effectively modifying the process' current environment
* to contain a malformed string in it. Using GNU's implementation, the command `env =a`
* prints out the malformed string and even invokes the child process with that environment.
* This can be seen by using `env -i =a env` or `env -i =a cat /proc/self/environ`
*
* POSIX.1-2017 doesn't seem to mention what to do if the string is malformed (at least
* not in "Chapter 8, Environment Variables" or in the definition for environ and various
* exec*'s or in the description of env in the "Shell & Utilities" volume).
*
* It also doesn't specify any checks for putenv before modifying the environ variable, which
* is likely why glibc doesn't do so. However, the first set_var argument cannot point to
* an empty string or a string containing '='.
*
* There is no benefit in replicating GNU's env behavior, since it will only modify the
* environment in weird ways
*/
if name.is_empty() {
show_warning!("no name specified for value {}", val.quote());
continue;
}
env::set_var(name, val);
}
apply_specified_env_vars(&opts);
if opts.program.is_empty() {
// no program provided, so just dump all env vars to stdout
print_env(opts.line_ending);
} else {
// we need to execute a command
let prog = Cow::from(opts.program[0]);
let args = &opts.program[1..];
if do_debug_printing {
eprintln!("executable: {}", prog.quote());
for (i, arg) in args.iter().enumerate() {
eprintln!("arg[{}]: {}", i, arg.quote());
}
}
/*
* On Unix-like systems Command::status either ends up calling either fork or posix_spawnp
* (which ends up calling clone). Keep using the current process would be ideal, but the
* standard library contains many checks and fail-safes to ensure the process ends up being
* created. This is much simpler than dealing with the hassles of calling execvp directly.
*/
match process::Command::new(&*prog).args(args).status() {
Ok(exit) if !exit.success() => {
#[cfg(unix)]
if let Some(exit_code) = exit.code() {
return Err(exit_code.into());
} else {
// `exit.code()` returns `None` on Unix when the process is terminated by a signal.
// See std::os::unix::process::ExitStatusExt for more information. This prints out
// the interrupted process and the signal it received.
let signal_code = exit.signal().unwrap();
let signal = Signal::try_from(signal_code).unwrap();
// We have to disable any handler that's installed by default.
// This ensures that we exit on this signal.
// For example, `SIGSEGV` and `SIGBUS` have default handlers installed in Rust.
// We ignore the errors because there is not much we can do if that fails anyway.
// SAFETY: The function is unsafe because installing functions is unsafe, but we are
// just defaulting to default behavior and not installing a function. Hence, the call
// is safe.
let _ = unsafe {
sigaction(
signal,
&SigAction::new(
SigHandler::SigDfl,
SaFlags::empty(),
SigSet::all(),
),
)
};
let _ = raise(signal);
}
#[cfg(not(unix))]
return Err(exit.code().unwrap().into());
}
Err(ref err)
if (err.kind() == io::ErrorKind::NotFound)
|| (err.kind() == io::ErrorKind::InvalidInput) =>
{
return Err(self.make_error_no_such_file_or_dir(prog.deref()));
}
Err(e) => {
uucore::show_error!("unknown error: {:?}", e);
return Err(126.into());
}
Ok(_) => (),
}
return self.run_program(opts, do_debug_printing);
}
Ok(())
}
fn run_program(
&mut self,
opts: Options<'_>,
do_debug_printing: bool,
) -> Result<(), Box<dyn UError>> {
let prog = Cow::from(opts.program[0]);
let args = &opts.program[1..];
if do_debug_printing {
eprintln!("executable: {}", prog.quote());
for (i, arg) in args.iter().enumerate() {
eprintln!("arg[{}]: {}", i, arg.quote());
}
}
// we need to execute a command
/*
* On Unix-like systems Command::status either ends up calling either fork or posix_spawnp
* (which ends up calling clone). Keep using the current process would be ideal, but the
* standard library contains many checks and fail-safes to ensure the process ends up being
* created. This is much simpler than dealing with the hassles of calling execvp directly.
*/
match process::Command::new(&*prog).args(args).status() {
Ok(exit) if !exit.success() => {
#[cfg(unix)]
if let Some(exit_code) = exit.code() {
return Err(exit_code.into());
} else {
// `exit.code()` returns `None` on Unix when the process is terminated by a signal.
// See std::os::unix::process::ExitStatusExt for more information. This prints out
// the interrupted process and the signal it received.
let signal_code = exit.signal().unwrap();
let signal = Signal::try_from(signal_code).unwrap();
// We have to disable any handler that's installed by default.
// This ensures that we exit on this signal.
// For example, `SIGSEGV` and `SIGBUS` have default handlers installed in Rust.
// We ignore the errors because there is not much we can do if that fails anyway.
// SAFETY: The function is unsafe because installing functions is unsafe, but we are
// just defaulting to default behavior and not installing a function. Hence, the call
// is safe.
let _ = unsafe {
sigaction(
signal,
&SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::all()),
)
};
let _ = raise(signal);
}
return Err(exit.code().unwrap().into());
}
Err(ref err)
if (err.kind() == io::ErrorKind::NotFound)
|| (err.kind() == io::ErrorKind::InvalidInput) =>
{
return Err(self.make_error_no_such_file_or_dir(prog.deref()));
}
Err(e) => {
uucore::show_error!("unknown error: {:?}", e);
return Err(126.into());
}
Ok(_) => (),
}
Ok(())
}
}
fn apply_removal_of_all_env_vars(opts: &Options<'_>) {
// remove all env vars if told to ignore presets
if opts.ignore_env {
for (ref name, _) in env::vars_os() {
env::remove_var(name);
}
}
}
fn make_options(matches: &clap::ArgMatches) -> UResult<Options<'_>> {
let ignore_env = matches.get_flag("ignore-environment");
let line_ending = LineEnding::from_zero_flag(matches.get_flag("null"));
let running_directory = matches.get_one::<OsString>("chdir").map(|s| s.as_os_str());
let files = match matches.get_many::<OsString>("file") {
Some(v) => v.map(|s| s.as_os_str()).collect(),
None => Vec::with_capacity(0),
};
let unsets = match matches.get_many::<OsString>("unset") {
Some(v) => v.map(|s| s.as_os_str()).collect(),
None => Vec::with_capacity(0),
};
let mut opts = Options {
ignore_env,
line_ending,
running_directory,
files,
unsets,
sets: vec![],
program: vec![],
};
let mut begin_prog_opts = false;
if let Some(mut iter) = matches.get_many::<OsString>("vars") {
// read NAME=VALUE arguments (and up to a single program argument)
while !begin_prog_opts {
if let Some(opt) = iter.next() {
if opt == "-" {
opts.ignore_env = true;
} else {
begin_prog_opts = parse_name_value_opt(&mut opts, opt)?;
}
} else {
break;
}
}
// read any leftover program arguments
for opt in iter {
parse_program_opt(&mut opts, opt)?;
}
}
Ok(opts)
}
fn apply_unset_env_vars(opts: &Options<'_>) -> Result<(), Box<dyn UError>> {
for name in &opts.unsets {
let native_name = NativeStr::new(name);
if name.is_empty()
|| native_name.contains(&'\0').unwrap()
|| native_name.contains(&'=').unwrap()
{
return Err(USimpleError::new(
125,
format!("cannot unset {}: Invalid argument", name.quote()),
));
}
env::remove_var(name);
}
Ok(())
}
fn apply_change_directory(opts: &Options<'_>) -> Result<(), Box<dyn UError>> {
// GNU env tests this behavior
if opts.program.is_empty() && opts.running_directory.is_some() {
return Err(UUsageError::new(
125,
"must specify command with --chdir (-C)".to_string(),
));
}
if let Some(d) = opts.running_directory {
match env::set_current_dir(d) {
Ok(()) => d,
Err(error) => {
return Err(USimpleError::new(
125,
format!("cannot change directory to {}: {error}", d.quote()),
));
}
};
}
Ok(())
}
fn apply_specified_env_vars(opts: &Options<'_>) {
// set specified env vars
for (name, val) in &opts.sets {
/*
* set_var panics if name is an empty string
* set_var internally calls setenv (on unix at least), while GNU env calls putenv instead.
*
* putenv returns successfully if provided with something like "=a" and modifies the environ
* variable to contain "=a" inside it, effectively modifying the process' current environment
* to contain a malformed string in it. Using GNU's implementation, the command `env =a`
* prints out the malformed string and even invokes the child process with that environment.
* This can be seen by using `env -i =a env` or `env -i =a cat /proc/self/environ`
*
* POSIX.1-2017 doesn't seem to mention what to do if the string is malformed (at least
* not in "Chapter 8, Environment Variables" or in the definition for environ and various
* exec*'s or in the description of env in the "Shell & Utilities" volume).
*
* It also doesn't specify any checks for putenv before modifying the environ variable, which
* is likely why glibc doesn't do so. However, the first set_var argument cannot point to
* an empty string or a string containing '='.
*
* There is no benefit in replicating GNU's env behavior, since it will only modify the
* environment in weird ways
*/
if name.is_empty() {
show_warning!("no name specified for value {}", val.quote());
continue;
}
env::set_var(name, val);
}
}
#[uucore::main]