diff -Nru rust-sudo-rs-0.2.1/.cargo_vcs_info.json rust-sudo-rs-0.2.2/.cargo_vcs_info.json --- rust-sudo-rs-0.2.1/.cargo_vcs_info.json 1970-01-01 00:00:01.000000000 +0000 +++ rust-sudo-rs-0.2.2/.cargo_vcs_info.json 1970-01-01 00:00:01.000000000 +0000 @@ -1,6 +1,6 @@ { "git": { - "sha1": "195d6f851904ab00357f040eef35e31abaebbe1b" + "sha1": "f3171451e551cedee60bffb91e910fa5346bdb94" }, "path_in_vcs": "" } \ No newline at end of file diff -Nru rust-sudo-rs-0.2.1/CHANGELOG.md rust-sudo-rs-0.2.2/CHANGELOG.md --- rust-sudo-rs-0.2.1/CHANGELOG.md 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/CHANGELOG.md 2006-07-24 01:21:28.000000000 +0000 @@ -1,5 +1,18 @@ # Changelog +## [0.2.2] - 2024-02-02 + +### Changed +- Several changes to the code to improve type safety +- Improved error message when a PTY cannot be opened +- Improved portability of the PAM bindings +- su: improved parsing of su command line options +- Add path information to parse errors originating from included files + +### Fixed +- Fixed a panic with large messages written to the syslog +- sudo: respect `--login` regardless of the presence of `--chdir` + ## [0.2.1] - 2023-09-21 ### Changed @@ -98,6 +111,7 @@ - Use canonicalized paths for the executed binaries - Simplified CLI help to only display supported actions +[0.2.2]: https://github.com/memorysafety/sudo-rs/compare/v0.2.1...v0.2.2 [0.2.1]: https://github.com/memorysafety/sudo-rs/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/memorysafety/sudo-rs/compare/v0.2.0-dev.20230711...v0.2.0 [0.2.0-dev.20230711]: https://github.com/memorysafety/sudo-rs/compare/v0.2.0-dev.20230703...v0.2.0-dev.20230711 diff -Nru rust-sudo-rs-0.2.1/Cargo.lock rust-sudo-rs-0.2.2/Cargo.lock --- rust-sudo-rs-0.2.1/Cargo.lock 1970-01-01 00:00:01.000000000 +0000 +++ rust-sudo-rs-0.2.2/Cargo.lock 1970-01-01 00:00:01.000000000 +0000 @@ -16,9 +16,9 @@ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "log" @@ -38,7 +38,7 @@ [[package]] name = "sudo-rs" -version = "0.2.1" +version = "0.2.2" dependencies = [ "glob", "libc", diff -Nru rust-sudo-rs-0.2.1/Cargo.toml rust-sudo-rs-0.2.2/Cargo.toml --- rust-sudo-rs-0.2.1/Cargo.toml 1970-01-01 00:00:01.000000000 +0000 +++ rust-sudo-rs-0.2.2/Cargo.toml 1970-01-01 00:00:01.000000000 +0000 @@ -13,7 +13,7 @@ edition = "2021" rust-version = "1.70" name = "sudo-rs" -version = "0.2.1" +version = "0.2.2" publish = true default-run = "sudo" description = "A memory safe implementation of sudo and su." diff -Nru rust-sudo-rs-0.2.1/Cargo.toml.orig rust-sudo-rs-0.2.2/Cargo.toml.orig --- rust-sudo-rs-0.2.1/Cargo.toml.orig 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/Cargo.toml.orig 2006-07-24 01:21:28.000000000 +0000 @@ -1,7 +1,7 @@ [package] name = "sudo-rs" description = "A memory safe implementation of sudo and su." -version = "0.2.1" +version = "0.2.2" license = "Apache-2.0 OR MIT" edition = "2021" repository = "https://github.com/memorysafety/sudo-rs" diff -Nru rust-sudo-rs-0.2.1/README.md rust-sudo-rs-0.2.2/README.md --- rust-sudo-rs-0.2.1/README.md 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/README.md 2006-07-24 01:21:28.000000000 +0000 @@ -4,7 +4,7 @@ ## Status of this project -Sudo-rs is being developed further; features you might expect form original sudo +Sudo-rs is being developed further; features you might expect from original sudo may still be unimplemented or not planned. If there is an important one you need, please request it using the issue tracker. If you encounter any usability bugs, also please report them on the [issue tracker](https://github.com/memorysafety/sudo-rs/issues). @@ -23,6 +23,16 @@ install the most recent version through [rustup]. You also need the C development files for PAM (`libpam0g-dev` on Debian, `pam-devel` on Fedora). +On Ubuntu or Debian-based systems, use the following command to install the PAM development library: +``` +sudo apt-get install libpam0g-dev +``` + +On CentOS or Red Hat-based systems, you can use the following command: +``` +sudo yum install pam-devel +``` + With dependencies installed, building sudo-rs is a simple matter of: ``` cargo build --release diff -Nru rust-sudo-rs-0.2.1/SECURITY.md rust-sudo-rs-0.2.2/SECURITY.md --- rust-sudo-rs-0.2.1/SECURITY.md 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/SECURITY.md 2006-07-24 01:21:28.000000000 +0000 @@ -1,24 +1,20 @@ ------BEGIN PGP SIGNED MESSAGE----- -Hash: SHA256 - -Security policy -=============== +# Security policy **Do not report security vulnerabilities through public GitHub issues.** Instead, you can report them using [our security page](https://github.com/memorysafety/sudo-rs/security). Alternatively, you can also send them -by email to security+sudo@tweedegolf.com. You can encrypt your mail using GnuPG if you want. Use the GPG key with fingerprint +by email to security+sudo@tweedegolf.com. You can encrypt your email using GnuPG if you want. Use the GPG key with fingerprint [C2E4 CAC4 B122 25DE 1C3B B1C9 289D 0820 03D0 1E95](https://keys.openpgp.org/search?q=C2E4CAC4B12225DE1C3BB1C9289D082003D01E95). Include as much of the following information: - * Type of issue (e.g. buffer overflow overflow, privilege escalation, etc.) - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue + * Type of issue (e.g. buffer overflow, privilege escalation, etc). + * The location of the affected source code (tag/branch/commit or direct URL). + * Any special configuration required to reproduce the issue. * The Linux distribution affected. - * Step-by-step instructions to reproduce the issue - * Impact of the issue, including how an attacker might exploit the issue + * Step-by-step instructions to reproduce the issue. + * Impact of the issue, including how an attacker might exploit the issue. -If you have found a bug that also exists in original sudo (which, although unlikely, means it is a very serious issue), you **must** +If you have found a bug that also exists in the original sudo (which, although unlikely, means it is a very serious issue), you **must** also follow the steps at https://www.sudo.ws/security/policy/ ## Preferred Languages @@ -27,15 +23,5 @@ ## Disclosure Policy Like original sudo, we adhere to the principle of [Coordinated Vulnerability Disclosure](https://vuls.cert.org/confluence/display/CVD/Executive+Summary). -Security Advisories -=================== -Security advisories will be published [on GitHub](https://github.com/memorysafety/sudo-rs/security/advisories) -and possibly through other channels. ------BEGIN PGP SIGNATURE----- - -iJMEARYIADsWIQTC5MrEsSIl3hw7sckonQggA9AelQUCZOxufR0cc2VjdXJpdHkr -c3Vkb0B0d2VlZGVnb2xmLmNvbQAKCRAonQggA9AelYxBAQCXNaMcO9IUr8u4RT8j -6ifxmca+MM9nyobBVdAAPaTwKQEA38XwSrRj/TApoZvDPchq8Weszk6Ke1arNQ/a -wZD+KAI= -=oRsJ ------END PGP SIGNATURE----- +# Security Advisories +Security advisories will be published [on GitHub](https://github.com/memorysafety/sudo-rs/security/advisories) and possibly through other channels. diff -Nru rust-sudo-rs-0.2.1/debian/changelog rust-sudo-rs-0.2.2/debian/changelog --- rust-sudo-rs-0.2.1/debian/changelog 2024-01-12 14:01:27.000000000 +0000 +++ rust-sudo-rs-0.2.2/debian/changelog 2024-02-13 21:19:26.000000000 +0000 @@ -1,3 +1,15 @@ +rust-sudo-rs (0.2.2-1) unstable; urgency=medium + + * Package sudo-rs 0.2.2 from crates.io using debcargo 2.6.1 + + [ Blair Noctis ] + * Team upload. + * Package sudo-rs 0.2.1 from crates.io using debcargo 2.6.1 + * Cherry-pick upstream fix for size assert tests + * Disable another test that runs too long + + -- Sylvestre Ledru Tue, 13 Feb 2024 22:19:26 +0100 + rust-sudo-rs (0.2.1-2) unstable; urgency=medium * Package sudo-rs 0.2.1 from crates.io using debcargo 2.6.1 diff -Nru rust-sudo-rs-0.2.1/debian/control rust-sudo-rs-0.2.2/debian/control --- rust-sudo-rs-0.2.1/debian/control 2024-01-12 14:01:27.000000000 +0000 +++ rust-sudo-rs-0.2.2/debian/control 2024-02-13 21:19:26.000000000 +0000 @@ -40,9 +40,9 @@ librust-sudo-rs-0.2-dev (= ${binary:Version}), librust-sudo-rs-0.2+default-dev (= ${binary:Version}), librust-sudo-rs-0.2+dev-dev (= ${binary:Version}), - librust-sudo-rs-0.2.1-dev (= ${binary:Version}), - librust-sudo-rs-0.2.1+default-dev (= ${binary:Version}), - librust-sudo-rs-0.2.1+dev-dev (= ${binary:Version}) + librust-sudo-rs-0.2.2-dev (= ${binary:Version}), + librust-sudo-rs-0.2.2+default-dev (= ${binary:Version}), + librust-sudo-rs-0.2.2+dev-dev (= ${binary:Version}) Description: Memory safe implementation of sudo and su - Rust source code Source code for Debianized Rust crate "sudo-rs" diff -Nru rust-sudo-rs-0.2.1/debian/patches/disable-test-timeout.diff rust-sudo-rs-0.2.2/debian/patches/disable-test-timeout.diff --- rust-sudo-rs-0.2.1/debian/patches/disable-test-timeout.diff 2024-01-12 14:01:27.000000000 +0000 +++ rust-sudo-rs-0.2.2/debian/patches/disable-test-timeout.diff 2024-02-13 21:19:26.000000000 +0000 @@ -2,7 +2,7 @@ =================================================================== --- sudo-rs.orig/src/system/mod.rs +++ sudo-rs/src/system/mod.rs -@@ -799,7 +799,7 @@ mod tests { +@@ -841,7 +841,7 @@ mod tests { .is_err_and(|err| err.raw_os_error() == Some(libc::EBADF)) } @@ -11,7 +11,7 @@ fn close_the_universe() { let ForkResult::Parent(child_pid) = fork().unwrap() else { let should_close = -@@ -830,7 +830,7 @@ mod tests { +@@ -872,7 +872,7 @@ mod tests { assert_eq!(status.exit_status(), Some(0)); } @@ -20,3 +20,16 @@ fn except_stdio_is_fine() { let ForkResult::Parent(child_pid) = fork().unwrap() else { let mut closer = super::FileCloser::new(); +Index: sudo-rs/src/system/term/mod.rs +=================================================================== +--- sudo-rs.orig/src/system/term/mod.rs ++++ sudo-rs/src/system/term/mod.rs +@@ -226,7 +226,7 @@ mod tests { + assert!(path.starts_with("/dev/pts/")); + } + +- #[test] ++ //#[test] + fn tcsetpgrp_and_tcgetpgrp_are_consistent() { + // Create a socket so the child can send us a byte if successful. + let (mut rx, mut tx) = UnixStream::pair().unwrap(); diff -Nru rust-sudo-rs-0.2.1/debian/tests/control rust-sudo-rs-0.2.2/debian/tests/control --- rust-sudo-rs-0.2.1/debian/tests/control 2024-01-12 14:01:27.000000000 +0000 +++ rust-sudo-rs-0.2.2/debian/tests/control 2024-02-13 21:19:26.000000000 +0000 @@ -1,19 +1,19 @@ -Test-Command: /usr/share/cargo/bin/cargo-auto-test sudo-rs 0.2.1 --all-targets --all-features +Test-Command: /usr/share/cargo/bin/cargo-auto-test sudo-rs 0.2.2 --all-targets --all-features Features: test-name=rust-sudo-rs:@ Depends: dh-cargo (>= 18), libpam-dev, procps, librust-pretty-assertions-1+default-dev (>= 1.2.1-~~), @ Restrictions: allow-stderr, skip-not-installable -Test-Command: /usr/share/cargo/bin/cargo-auto-test sudo-rs 0.2.1 --all-targets +Test-Command: /usr/share/cargo/bin/cargo-auto-test sudo-rs 0.2.2 --all-targets Features: test-name=librust-sudo-rs-dev:default Depends: dh-cargo (>= 18), libpam-dev, procps, librust-pretty-assertions-1+default-dev (>= 1.2.1-~~), @ Restrictions: allow-stderr, skip-not-installable -Test-Command: /usr/share/cargo/bin/cargo-auto-test sudo-rs 0.2.1 --all-targets --no-default-features --features dev +Test-Command: /usr/share/cargo/bin/cargo-auto-test sudo-rs 0.2.2 --all-targets --no-default-features --features dev Features: test-name=librust-sudo-rs-dev:dev Depends: dh-cargo (>= 18), libpam-dev, procps, librust-pretty-assertions-1+default-dev (>= 1.2.1-~~), @ Restrictions: allow-stderr, skip-not-installable -Test-Command: /usr/share/cargo/bin/cargo-auto-test sudo-rs 0.2.1 --all-targets --no-default-features +Test-Command: /usr/share/cargo/bin/cargo-auto-test sudo-rs 0.2.2 --all-targets --no-default-features Features: test-name=librust-sudo-rs-dev: Depends: dh-cargo (>= 18), libpam-dev, procps, librust-pretty-assertions-1+default-dev (>= 1.2.1-~~), @ Restrictions: allow-stderr, skip-not-installable Binary files /tmp/tmppjz6zq5i/hNnGj9Lc29/rust-sudo-rs-0.2.1/docs/audit/audit-report-sudo-rs.pdf and /tmp/tmppjz6zq5i/Oj5CiINn8p/rust-sudo-rs-0.2.2/docs/audit/audit-report-sudo-rs.pdf differ diff -Nru rust-sudo-rs-0.2.1/docs/man/su.1.md rust-sudo-rs-0.2.2/docs/man/su.1.md --- rust-sudo-rs-0.2.1/docs/man/su.1.md 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/docs/man/su.1.md 2006-07-24 01:21:28.000000000 +0000 @@ -1,5 +1,5 @@ # NAME diff -Nru rust-sudo-rs-0.2.1/docs/man/sudo.8.md rust-sudo-rs-0.2.2/docs/man/sudo.8.md --- rust-sudo-rs-0.2.1/docs/man/sudo.8.md 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/docs/man/sudo.8.md 2006-07-24 01:21:28.000000000 +0000 @@ -1,5 +1,5 @@ # NAME @@ -58,10 +58,10 @@ the current session. The next time sudo-rs is run, authentication will take place if the policy requires it. - When used in conjuction with a *command* or an option that may require a + When used in conjunction with a *command* or an option that may require a password, this option will cause sudo-rs to ignore the user's session record. As a result, authentication will take place if the policy requires - it. When used in conjuction with a *command* no invalidation of existing + it. When used in conjunction with a *command* no invalidation of existing session records will take place. `-n`, `--non-interactive` diff -Nru rust-sudo-rs-0.2.1/docs/man/visudo.8.md rust-sudo-rs-0.2.2/docs/man/visudo.8.md --- rust-sudo-rs-0.2.1/docs/man/visudo.8.md 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/docs/man/visudo.8.md 2006-07-24 01:21:28.000000000 +0000 @@ -1,5 +1,5 @@ # NAME diff -Nru rust-sudo-rs-0.2.1/docs/sudo-cve.md rust-sudo-rs-0.2.2/docs/sudo-cve.md --- rust-sudo-rs-0.2.1/docs/sudo-cve.md 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/docs/sudo-cve.md 2006-07-24 01:21:28.000000000 +0000 @@ -15,7 +15,7 @@ | CVE-2004-1051 [^4] | | https://www.sudo.ws/security/advisories/bash_functions/ | | CVE-2005-1119 [^5] | | Corrupt arbitrary files via a symlink attack | | CVE-2005-1993 [^6] | | https://www.sudo.ws/security/advisories/path_race/ | -| CVE-2005-4890 [^7] | | TTY hijacking when a priviliged user uses sudo to run unprivileged commands | +| CVE-2005-4890 [^7] | | TTY hijacking when a privileged user uses sudo to run unprivileged commands | | - [^9] | | https://www.sudo.ws/security/advisories/cmnd_alias_negation/ | | CVE-2010-1646 [^10] | | https://www.sudo.ws/security/advisories/secure_path/ | | CVE-2010-2956 [^11] | | https://www.sudo.ws/security/advisories/runas_group/ | @@ -32,7 +32,7 @@ [^1]: All our path checks should only ever be done with absolute paths [^2]: We try to take care to only expose relevant information to the user -[^3]: Our usage of Rust should mostly prevent heap corruption bugs from occuring +[^3]: Our usage of Rust should mostly prevent heap corruption bugs from occurring [^4]: env_reset is always enabled in sudo-rs, additionally we apply filtering to several variables to prevent any additional attack paths [^5]: - diff -Nru rust-sudo-rs-0.2.1/src/cli/help.rs rust-sudo-rs-0.2.2/src/cli/help.rs --- rust-sudo-rs-0.2.1/src/cli/help.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/cli/help.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -pub const USAGE_MSG: &str = "\ -usage: sudo [-u user] [-g group] [-D directory] [-knS] [-i | -s] - sudo -h | -K | -k | -V"; - -const DESCRIPTOR: &str = "sudo - run commands as another user"; - -const HELP_MSG: &str = "Options: - -D, --chdir=directory change the working directory before running command - -g, --group=group run command as the specified group name or ID - -h, --help display help message and exit - -i, --login run login shell as the target user; a command may also be - specified - -K, --remove-timestamp remove timestamp file completely - -k, --reset-timestamp invalidate timestamp file - for longer format - -n, --non-interactive non-interactive mode, no prompts are used - -S, --stdin read password from standard input - -s, --shell run shell as the target user; a command may also be specified - -u, --user=user run command (or edit file) as specified user name or ID - -v, --validate update user's timestamp without running a command - -V, --version display version information and exit - -- stop processing command line arguments"; - -pub fn long_help_message() -> String { - format!("{DESCRIPTOR}\n{USAGE_MSG}\n{HELP_MSG}") -} diff -Nru rust-sudo-rs-0.2.1/src/cli/mod.rs rust-sudo-rs-0.2.2/src/cli/mod.rs --- rust-sudo-rs-0.2.1/src/cli/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/cli/mod.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,370 +0,0 @@ -#![forbid(unsafe_code)] - -use std::path::PathBuf; - -pub mod help; - -#[cfg(test)] -mod tests; - -#[derive(Debug, Default, PartialEq, Clone)] -pub enum SudoAction { - #[default] - Help, - Version, - Validate, - RemoveTimestamp, - ResetTimestamp, - Run(Vec), - List(Vec), - Edit(Vec), -} - -#[derive(Debug, Default, PartialEq, Clone)] -pub struct SudoOptions { - pub background: bool, - pub chroot: Option, - pub directory: Option, - pub group: Option, - pub host: Option, - pub login: bool, - pub non_interactive: bool, - pub other_user: Option, - pub preserve_env: Vec, - pub preserve_groups: bool, - pub shell: bool, - pub stdin: bool, - pub user: Option, - // additional environment - pub env_var_list: Vec<(String, String)>, - // resulting action enum - pub action: SudoAction, - // actions - edit: bool, - help: bool, - list: List, - remove_timestamp: bool, - pub reset_timestamp: bool, - validate: bool, - version: bool, - // arguments passed straight through, either seperated by -- or just trailing. - external_args: Vec, -} - -#[derive(Default, Debug, Clone, PartialEq)] -enum List { - #[default] - None, - Once, - Verbose, -} - -enum SudoArg { - Flag(String), - Argument(String, String), - Environment(String, String), - Rest(Vec), -} - -impl SudoOptions { - const TAKES_ARGUMENT_SHORT: &'static [char] = &['D', 'E', 'g', 'h', 'R', 'U', 'u']; - const TAKES_ARGUMENT: &'static [&'static str] = &[ - "chdir", - "preserve-env", - "group", - "host", - "chroot", - "other-user", - "user", - ]; - - /// argument assignments and shorthand options preprocessing - fn normalize_arguments(iter: I) -> Result, String> - where - I: IntoIterator, - { - // the first argument is the sudo command - so we can skip it - let mut arg_iter = iter.into_iter().skip(1); - let mut processed: Vec = vec![]; - - while let Some(arg) = arg_iter.next() { - match arg.as_str() { - "--" => { - processed.push(SudoArg::Rest(arg_iter.collect())); - break; - } - long_arg if long_arg.starts_with("--") => { - if long_arg.contains('=') { - // convert assignment to normal tokens - let (key, value) = long_arg.split_once('=').unwrap(); - // only accept arguments when one is expected - if !Self::TAKES_ARGUMENT.contains(&&key[2..]) { - Err(format!("'{}' does not take any arguments", key))?; - } - processed.push(SudoArg::Argument(key.to_string(), value.to_string())); - } else if Self::TAKES_ARGUMENT.contains(&&long_arg[2..]) { - if let Some(next) = arg_iter.next() { - processed.push(SudoArg::Argument(arg, next)); - } else { - Err(format!("'{}' expects an argument", &long_arg))?; - } - } else { - processed.push(SudoArg::Flag(arg)); - } - } - short_arg if short_arg.starts_with('-') => { - // split combined shorthand options - for (n, char) in short_arg.trim_start_matches('-').chars().enumerate() { - let flag = format!("-{char}"); - // convert option argument to seperate segment - if Self::TAKES_ARGUMENT_SHORT.contains(&char) { - let rest = short_arg[(n + 2)..].trim().to_string(); - // assignment syntax is not accepted for shorthand arguments - if rest.starts_with('=') { - Err("invalid option '='")?; - } - if !rest.is_empty() { - processed.push(SudoArg::Argument(flag, rest)); - } else if let Some(next) = arg_iter.next() { - processed.push(SudoArg::Argument(flag, next)); - } else if char == 'h' { - // short version of --help has no arguments - processed.push(SudoArg::Flag(flag)); - } else { - Err(format!("'-{}' expects an argument", char))?; - } - break; - } else { - processed.push(SudoArg::Flag(flag)); - } - } - } - env_var if SudoOptions::try_to_env_var(env_var).is_some() => { - let (key, value) = SudoOptions::try_to_env_var(env_var).unwrap(); - processed.push(SudoArg::Environment(key, value)); - } - _argument => { - let mut rest = vec![arg]; - rest.extend(arg_iter); - processed.push(SudoArg::Rest(rest)); - break; - } - } - } - - Ok(processed) - } - - /// try to parse and environment variable assignment - fn try_to_env_var(arg: &str) -> Option<(String, String)> { - if let Some((name, value)) = arg.split_once('=').and_then(|(name, value)| { - name.chars() - .all(|c| c.is_alphanumeric() || c == '_') - .then_some((name, value)) - }) { - Some((name.to_owned(), value.to_owned())) - } else { - None - } - } - - /// parse command line arguments from the environment and handle errors - pub fn from_env() -> Result { - Self::try_parse_from(std::env::args()) - } - - /// from the arguments resolve which action should be performed - fn resolve_action(&mut self) { - if self.help { - self.action = SudoAction::Help; - } else if self.version { - self.action = SudoAction::Version; - } else if self.remove_timestamp { - self.action = SudoAction::RemoveTimestamp; - } else if self.reset_timestamp && self.external_args.is_empty() { - self.action = SudoAction::ResetTimestamp; - } else if self.validate { - self.action = SudoAction::Validate; - } else if self.list != List::None { - self.action = SudoAction::List(std::mem::take(self.external_args.as_mut())); - } else if self.edit { - let args: Vec = std::mem::take(self.external_args.as_mut()); - let args = args.into_iter().map(PathBuf::from).collect(); - self.action = SudoAction::Edit(args); - } else { - self.action = SudoAction::Run(std::mem::take(self.external_args.as_mut())); - } - } - - /// verify that the passed arguments are valid given the action and there are no conflicts - fn validate(&self) -> Result<(), String> { - // conflicting arguments - if self.remove_timestamp && self.reset_timestamp { - Err("conflicting arguments '--remove-timestamp' and '--reset-timestamp'")?; - } - - // check arguments for validate action - if matches!(self.action, SudoAction::Validate) - && (self.background - || self.preserve_groups - || self.login - || self.shell - || !self.preserve_env.is_empty() - || self.other_user.is_some() - || self.directory.is_some() - || self.chroot.is_some()) - { - Err("invalid argument found for '--validate'")?; - } - - // check arguments for list action - if let SudoAction::List(command_args) = &self.action { - // when present, `-u` must be accompanied by a command - let has_command = !command_args.is_empty(); - let valid_user_flag = self.user.is_none() || has_command; - - if self.background - || self.preserve_groups - || self.login - || !valid_user_flag - || self.shell - || !self.preserve_env.is_empty() - || self.directory.is_some() - || self.chroot.is_some() - { - Err("invalid argument found for '--list'")?; - } - } - - // check arguments for edit action - if matches!(self.action, SudoAction::Edit(_)) - && (self.background - || self.preserve_groups - || self.login - || self.shell - || self.other_user.is_some() - || !self.preserve_env.is_empty()) - { - Err("invalid argument found for '--edit'")?; - } - - Ok(()) - } - - /// parse an iterator over command line arguments - pub fn try_parse_from(iter: I) -> Result - where - I: IntoIterator, - T: Into + Clone, - { - let mut options: SudoOptions = SudoOptions::default(); - let arg_iter = Self::normalize_arguments(iter.into_iter().map(Into::into))? - .into_iter() - .peekable(); - - for arg in arg_iter { - match arg { - SudoArg::Flag(flag) => match flag.as_str() { - "-b" | "--background" => { - options.background = true; - } - "-e" | "--edit" => { - options.edit = true; - } - "-H" | "--set-home" => { - // this option is ignored, since it is the default for sudo-rs; but accept - // it for backwards compatibility reasons - } - "-h" | "--help" => { - options.help = true; - } - "-i" | "--login" => { - options.login = true; - } - "-K" | "--remove-timestamp" => { - options.remove_timestamp = true; - } - "-k" | "--reset-timestamp" => { - options.reset_timestamp = true; - } - "-l" | "--list" => match options.list { - List::None => options.list = List::Once, - List::Once => options.list = List::Verbose, - List::Verbose => {} - }, - "-n" | "--non-interactive" => { - options.non_interactive = true; - } - "-P" | "--preserve-groups" => { - options.preserve_groups = true; - } - "-S" | "--stdin" => { - options.stdin = true; - } - "-s" | "--shell" => { - options.shell = true; - } - "-V" | "--version" => { - options.version = true; - } - "-v" | "--validate" => { - options.validate = true; - } - _option => { - Err("invalid option provided")?; - } - }, - SudoArg::Argument(option, value) => match option.as_str() { - "-D" | "--chdir" => { - options.directory = Some(PathBuf::from(value)); - } - "-E" | "--preserve-env" => { - options.preserve_env = value.split(',').map(str::to_string).collect() - } - "-g" | "--group" => { - options.group = Some(value); - } - "-h" | "--host" => { - options.host = Some(value); - } - "-R" | "--chroot" => { - options.chroot = Some(PathBuf::from(value)); - } - "-U" | "--other-user" => { - options.other_user = Some(value); - } - "-u" | "--user" => { - options.user = Some(value); - } - _option => { - Err("invalid option provided")?; - } - }, - SudoArg::Environment(key, value) => { - options.env_var_list.push((key, value)); - } - SudoArg::Rest(rest) => { - options.external_args = rest; - } - } - } - - options.resolve_action(); - options.validate()?; - - Ok(options) - } - - pub fn verbose_list_mode(&self) -> bool { - matches!(self.list, List::Verbose) - } - - #[cfg(test)] - pub fn args(self) -> Vec { - match self.action { - SudoAction::Run(args) => args, - SudoAction::List(args) => args, - _ => vec![], - } - } -} diff -Nru rust-sudo-rs-0.2.1/src/cli/tests.rs rust-sudo-rs-0.2.2/src/cli/tests.rs --- rust-sudo-rs-0.2.1/src/cli/tests.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/cli/tests.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,383 +0,0 @@ -use std::path::PathBuf; - -use super::{SudoAction, SudoOptions}; -use pretty_assertions::assert_eq; - -/// Passing '-E' with a variable fails -#[test] -fn short_preserve_env_with_var_fails() { - let cmd = SudoOptions::try_parse_from(["sudo", "-E=variable"]); - assert!(cmd.is_err()) -} - -/// Passing '--preserve-env' with an argument fills 'preserve_env', 'short_preserve_env' stays 'false' -#[test] -fn preserve_env_with_var() { - let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env=some_argument"]).unwrap(); - assert_eq!(cmd.preserve_env, vec!["some_argument"]); -} - -/// Passing '--preserve-env' with several arguments fills 'preserve_env', 'short_preserve_env' stays 'false' -#[test] -fn preserve_env_with_several_vars() { - let cmd = SudoOptions::try_parse_from([ - "sudo", - "--preserve-env=some_argument,another_argument,a_third_one", - ]) - .unwrap(); - assert_eq!( - cmd.preserve_env, - vec!["some_argument", "another_argument", "a_third_one"] - ); -} - -/// Catch env variable that is given without hyphens in 'VAR=value' form in env_var_list. -/// external_args stay empty. -#[test] -fn env_variable() { - let cmd = SudoOptions::try_parse_from(["sudo", "ENV=with_a_value"]).unwrap(); - assert_eq!( - cmd.env_var_list, - vec![("ENV".to_owned(), "with_a_value".to_owned())] - ); - assert!(cmd.args().is_empty()); -} - -/// Catch several env variablse that are given without hyphens in 'VAR=value' form in env_var_list. -/// external_args stay empty. -#[test] -fn several_env_variables() { - let cmd = SudoOptions::try_parse_from([ - "sudo", - "ENV=with_a_value", - "another_var=otherval", - "more=this_is_a_val", - ]) - .unwrap(); - assert_eq!( - cmd.env_var_list, - vec![ - ("ENV".to_owned(), "with_a_value".to_owned()), - ("another_var".to_owned(), "otherval".to_owned()), - ("more".to_owned(), "this_is_a_val".to_owned()) - ] - ); - assert!(cmd.args().is_empty()); -} - -/// Mix env variables and trailing arguments that just pass through sudo -/// Divided by hyphens. -#[test] -fn mix_env_variables_with_trailing_args_divided_by_hyphens() { - let cmd = SudoOptions::try_parse_from(["sudo", "env=var", "--", "external=args", "something"]) - .unwrap(); - assert_eq!(cmd.env_var_list, vec![("env".to_owned(), "var".to_owned())]); - assert_eq!(cmd.args(), vec!["external=args", "something"]); -} - -/// Mix env variables and trailing arguments that just pass through sudo -/// Divided by known flag. -#[test] -fn mix_env_variables_with_trailing_args_divided_by_known_flag() { - let cmd = SudoOptions::try_parse_from(["sudo", "-b", "external=args", "something"]).unwrap(); - assert_eq!( - cmd.env_var_list, - vec![("external".to_owned(), "args".to_owned())] - ); - assert!(cmd.background); - assert_eq!(cmd.args(), vec!["something"]); -} - -/// Catch trailing arguments that just pass through sudo -/// but look like a known flag. -#[test] -fn trailing_args_followed_by_known_flag() { - let cmd = - SudoOptions::try_parse_from(["sudo", "args", "followed_by", "known_flag", "-b"]).unwrap(); - assert!(!cmd.background); - assert_eq!(cmd.args(), vec!["args", "followed_by", "known_flag", "-b"]); -} - -/// Catch trailing arguments that just pass through sudo -/// but look like a known flag, divided by hyphens. -#[test] -fn trailing_args_hyphens_known_flag() { - let cmd = SudoOptions::try_parse_from([ - "sudo", - "--", - "trailing", - "args", - "followed_by", - "known_flag", - "-b", - ]) - .unwrap(); - assert!(!cmd.background); - assert_eq!( - cmd.args(), - vec!["trailing", "args", "followed_by", "known_flag", "-b"] - ); -} - -/// Check that the first environment variable declaration before any command is not treated as part -/// of the command. -#[test] -fn first_trailing_env_var_is_not_an_external_arg() { - let cmd = SudoOptions::try_parse_from(["sudo", "FOO=1", "command", "BAR=2"]).unwrap(); - assert_eq!(cmd.env_var_list, vec![("FOO".to_owned(), "1".to_owned()),]); - assert_eq!( - cmd.action, - SudoAction::Run(["command", "BAR=2"].map(String::from).to_vec()) - ); -} - -#[test] -fn trailing_env_vars_are_external_args() { - let cmd = SudoOptions::try_parse_from([ - "sudo", "FOO=1", "-b", "BAR=2", "command", "BAZ=3", "arg", "FOOBAR=4", "command", "arg", - "BARBAZ=5", - ]) - .unwrap(); - assert!(cmd.background); - assert_eq!( - cmd.env_var_list, - vec![ - ("FOO".to_owned(), "1".to_owned()), - ("BAR".to_owned(), "2".to_owned()) - ] - ); - assert_eq!( - cmd.args(), - vec!["command", "BAZ=3", "arg", "FOOBAR=4", "command", "arg", "BARBAZ=5"] - ); -} - -#[test] -fn single_env_var_declaration() { - let cmd = SudoOptions::try_parse_from(["sudo", "FOO=1", "command"]).unwrap(); - assert_eq!(cmd.env_var_list, vec![("FOO".to_owned(), "1".to_owned())]); - assert_eq!(cmd.args(), vec!["command"]); -} - -#[test] -fn shorthand_with_argument() { - let cmd = SudoOptions::try_parse_from(["sudo", "-u", "ferris"]).unwrap(); - assert_eq!(cmd.user.as_deref(), Some("ferris")); -} - -#[test] -fn shorthand_with_direct_argument() { - let cmd = SudoOptions::try_parse_from(["sudo", "-uferris"]).unwrap(); - assert_eq!(cmd.user.as_deref(), Some("ferris")); -} - -#[test] -fn shorthand_without_argument() { - let cmd = SudoOptions::try_parse_from(["sudo", "-u"]); - assert!(cmd.is_err()) -} - -#[test] -fn non_interactive() { - let cmd = SudoOptions::try_parse_from(["sudo", "-n"]).unwrap(); - assert!(cmd.non_interactive); - - let cmd = SudoOptions::try_parse_from(["sudo", "--non-interactive"]).unwrap(); - assert!(cmd.non_interactive); -} - -#[test] -fn preserve_groups() { - let cmd = SudoOptions::try_parse_from(["sudo", "-P"]).unwrap(); - assert!(cmd.preserve_groups); - - let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-groups"]).unwrap(); - assert!(cmd.preserve_groups); -} - -#[test] -fn stdin() { - let cmd = SudoOptions::try_parse_from(["sudo", "-S"]).unwrap(); - assert!(cmd.stdin); - - let cmd = SudoOptions::try_parse_from(["sudo", "--stdin"]).unwrap(); - assert!(cmd.stdin); -} - -#[test] -fn shell() { - let cmd = SudoOptions::try_parse_from(["sudo", "-s"]).unwrap(); - assert!(cmd.shell); - - let cmd = SudoOptions::try_parse_from(["sudo", "--shell"]).unwrap(); - assert!(cmd.shell); -} - -#[test] -fn directory() { - let cmd = SudoOptions::try_parse_from(["sudo", "-D/some/path"]).unwrap(); - assert_eq!(cmd.directory, Some(PathBuf::from("/some/path"))); - - let cmd = SudoOptions::try_parse_from(["sudo", "--chdir", "/some/path"]).unwrap(); - assert_eq!(cmd.directory, Some(PathBuf::from("/some/path"))); - - let cmd = SudoOptions::try_parse_from(["sudo", "--chdir=/some/path"]).unwrap(); - assert_eq!(cmd.directory, Some(PathBuf::from("/some/path"))); -} - -#[test] -fn group() { - let cmd = SudoOptions::try_parse_from(["sudo", "-grustaceans"]).unwrap(); - assert_eq!(cmd.group.as_deref(), Some("rustaceans")); - - let cmd = SudoOptions::try_parse_from(["sudo", "--group", "rustaceans"]).unwrap(); - assert_eq!(cmd.group.as_deref(), Some("rustaceans")); - - let cmd = SudoOptions::try_parse_from(["sudo", "--group=rustaceans"]).unwrap(); - assert_eq!(cmd.group.as_deref(), Some("rustaceans")); -} - -#[test] -fn host() { - let cmd = SudoOptions::try_parse_from(["sudo", "-hlilo"]).unwrap(); - assert_eq!(cmd.host.as_deref(), Some("lilo")); - - let cmd = SudoOptions::try_parse_from(["sudo", "--host", "lilo"]).unwrap(); - assert_eq!(cmd.host.as_deref(), Some("lilo")); - - let cmd = SudoOptions::try_parse_from(["sudo", "--host=lilo"]).unwrap(); - assert_eq!(cmd.host.as_deref(), Some("lilo")); -} - -#[test] -fn chroot() { - let cmd = SudoOptions::try_parse_from(["sudo", "-R/some/path"]).unwrap(); - assert_eq!(cmd.chroot, Some(PathBuf::from("/some/path"))); - - let cmd = SudoOptions::try_parse_from(["sudo", "--chroot", "/some/path"]).unwrap(); - assert_eq!(cmd.chroot, Some(PathBuf::from("/some/path"))); - - let cmd = SudoOptions::try_parse_from(["sudo", "--chroot=/some/path"]).unwrap(); - assert_eq!(cmd.chroot, Some(PathBuf::from("/some/path"))); -} - -#[test] -fn other_user() { - let cmd = SudoOptions::try_parse_from(["sudo", "-Uferris"]).unwrap(); - assert_eq!(cmd.other_user.as_deref(), Some("ferris")); - - let cmd = SudoOptions::try_parse_from(["sudo", "--other-user", "ferris"]).unwrap(); - assert_eq!(cmd.other_user.as_deref(), Some("ferris")); - - let cmd = SudoOptions::try_parse_from(["sudo", "--other-user=ferris"]).unwrap(); - assert_eq!(cmd.other_user.as_deref(), Some("ferris")); -} - -#[test] -fn invalid_option() { - let cmd = SudoOptions::try_parse_from(["sudo", "--wololo"]); - assert!(cmd.is_err()) -} - -#[test] -fn invalid_option_with_argument() { - let cmd = SudoOptions::try_parse_from(["sudo", "--background=yes"]); - assert!(cmd.is_err()) -} - -#[test] -fn no_argument_provided() { - let cmd = SudoOptions::try_parse_from(["sudo", "--user"]); - assert!(cmd.is_err()) -} - -#[test] -fn login() { - let cmd = SudoOptions::try_parse_from(["sudo", "-i"]).unwrap(); - assert!(cmd.login); - - let cmd = SudoOptions::try_parse_from(["sudo", "--login"]).unwrap(); - assert!(cmd.login); -} - -#[test] -fn edit() { - let cmd = SudoOptions::try_parse_from(["sudo", "-e"]).unwrap(); - assert_eq!(cmd.action, SudoAction::Edit(vec![])); - - let cmd = SudoOptions::try_parse_from(["sudo", "--edit"]).unwrap(); - assert_eq!(cmd.action, SudoAction::Edit(vec![])); -} - -#[test] -fn help() { - let cmd = SudoOptions::try_parse_from(["sudo", "-h"]).unwrap(); - assert_eq!(cmd.action, SudoAction::Help); - - let cmd = SudoOptions::try_parse_from(["sudo", "-bh"]).unwrap(); - assert_eq!(cmd.action, SudoAction::Help); - - let cmd = SudoOptions::try_parse_from(["sudo", "--help"]).unwrap(); - assert_eq!(cmd.action, SudoAction::Help); -} - -#[test] -fn conflicting_arguments() { - let cmd = SudoOptions::try_parse_from(["sudo", "-K", "-k"]); - assert!(cmd.is_err()); - - let cmd = SudoOptions::try_parse_from(["sudo", "--remove-timestamp", "--reset-timestamp"]); - assert!(cmd.is_err()); - - let cmd = SudoOptions::try_parse_from(["sudo", "-K"]).unwrap(); - assert_eq!(cmd.action, SudoAction::RemoveTimestamp); - - let cmd = SudoOptions::try_parse_from(["sudo", "-k"]).unwrap(); - assert_eq!(cmd.action, SudoAction::ResetTimestamp); -} - -#[test] -fn list() { - let valid: &[&[_]] = &[ - &["sudo", "--list"], - &["sudo", "-l"], - &["sudo", "-l", "true"], - &["sudo", "-l", "-U", "ferris"], - &["sudo", "-l", "-U", "ferris", "true"], - &["sudo", "-l", "-u", "ferris", "true"], - &["sudo", "-l", "-u", "ferris", "-U", "root", "true"], - ]; - - for args in valid { - let cmd = SudoOptions::try_parse_from(args.iter().copied()).unwrap(); - assert!(matches!(cmd.action, SudoAction::List(_))) - } - - let invalid: &[&[_]] = &[ - &["sudo", "-l", "-u", "ferris"], - &["sudo", "-l", "-u", "ferris", "-U", "root"], - ]; - - for args in invalid { - let res = SudoOptions::try_parse_from(args.iter().copied()); - assert!(res.is_err()) - } -} - -#[test] -fn validate() { - let cmd = SudoOptions::try_parse_from(["sudo", "-v"]).unwrap(); - assert_eq!(cmd.action, SudoAction::Validate); - - let cmd = SudoOptions::try_parse_from(["sudo", "--validate"]).unwrap(); - assert_eq!(cmd.action, SudoAction::Validate); -} - -#[test] -fn version() { - let cmd = SudoOptions::try_parse_from(["sudo", "-V"]).unwrap(); - assert_eq!(cmd.action, SudoAction::Version); - - let cmd = SudoOptions::try_parse_from(["sudo", "--version"]).unwrap(); - assert_eq!(cmd.action, SudoAction::Version); -} diff -Nru rust-sudo-rs-0.2.1/src/common/command.rs rust-sudo-rs-0.2.2/src/common/command.rs --- rust-sudo-rs-0.2.1/src/common/command.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/common/command.rs 2006-07-24 01:21:28.000000000 +0000 @@ -63,10 +63,7 @@ arguments = vec!["-c".to_string(), escaped(arguments)] } } else { - command = arguments - .get(0) - .map(|s| s.into()) - .unwrap_or_else(PathBuf::new); + command = arguments.first().map(|s| s.into()).unwrap_or_default(); arguments.remove(0); // remember the original binary name before resolving symlinks; this is not diff -Nru rust-sudo-rs-0.2.1/src/common/context.rs rust-sudo-rs-0.2.2/src/common/context.rs --- rust-sudo-rs-0.2.1/src/common/context.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/common/context.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,19 +1,39 @@ -use crate::cli::{SudoAction, SudoOptions}; use crate::common::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2}; -use crate::system::{hostname, Group, Process, User}; -use std::path::PathBuf; +use crate::system::{Group, Hostname, Process, User}; +use super::resolve::CurrentUser; use super::{ command::CommandAndArguments, - resolve::{resolve_current_user, resolve_launch_and_shell, resolve_target_user_and_group}, - Error, + resolve::{resolve_launch_and_shell, resolve_target_user_and_group}, + Error, SudoPath, SudoString, }; +#[derive(Clone, Copy)] +pub enum ContextAction { + List, + Run, + Validate, +} + +// this is a bit of a hack to keep the existing `Context` API working +pub struct OptionsForContext { + pub chdir: Option, + pub group: Option, + pub login: bool, + pub non_interactive: bool, + pub positional_args: Vec, + pub reset_timestamp: bool, + pub shell: bool, + pub stdin: bool, + pub user: Option, + pub action: ContextAction, +} + #[derive(Debug)] pub struct Context { // cli options pub launch: LaunchType, - pub chdir: Option, + pub chdir: Option, pub command: CommandAndArguments, pub target_user: User, pub target_group: Group, @@ -21,8 +41,8 @@ pub non_interactive: bool, pub use_session_records: bool, // system - pub hostname: String, - pub current_user: User, + pub hostname: Hostname, + pub current_user: CurrentUser, pub process: Process, // policy pub use_pty: bool, @@ -37,23 +57,23 @@ } impl Context { - pub fn build_from_options(sudo_options: SudoOptions, path: String) -> Result { - let hostname = hostname(); - let current_user = resolve_current_user()?; + pub fn build_from_options( + sudo_options: OptionsForContext, + path: String, + ) -> Result { + let hostname = Hostname::resolve(); + let current_user = CurrentUser::resolve()?; let (target_user, target_group) = resolve_target_user_and_group(&sudo_options.user, &sudo_options.group, ¤t_user)?; let (launch, shell) = resolve_launch_and_shell(&sudo_options, ¤t_user, &target_user); let command = match sudo_options.action { - SudoAction::Run(args) => CommandAndArguments::build_from_args(shell, args, &path), - SudoAction::List(args) => { - if args.is_empty() { - // FIXME here and in the `_` arm, `Default` is being used as `Option::None` - Default::default() - } else { - CommandAndArguments::build_from_args(shell, args, &path) - } + ContextAction::Validate | ContextAction::List + if sudo_options.positional_args.is_empty() => + { + // FIXME `Default` is being used as `Option::None` + Default::default() } - _ => Default::default(), + _ => CommandAndArguments::build_from_args(shell, sudo_options.positional_args, &path), }; Ok(Context { @@ -64,7 +84,7 @@ target_group, use_session_records: !sudo_options.reset_timestamp, launch, - chdir: sudo_options.directory, + chdir: sudo_options.chdir, stdin: sudo_options.stdin, non_interactive: sudo_options.non_interactive, process: Process::new(), @@ -75,23 +95,27 @@ #[cfg(test)] mod tests { - use crate::{cli::SudoOptions, system::hostname}; + use crate::{sudo::SudoAction, system::Hostname}; use std::collections::HashMap; use super::Context; #[test] fn test_build_context() { - let options = SudoOptions::try_parse_from(["sudo", "echo", "hello"]).unwrap(); + let options = SudoAction::try_parse_from(["sudo", "echo", "hello"]) + .unwrap() + .try_into_run() + .ok() + .unwrap(); let path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; - let context = Context::build_from_options(options, path.to_string()).unwrap(); + let context = Context::build_from_options(options.into(), path.to_string()).unwrap(); let mut target_environment = HashMap::new(); target_environment.insert("SUDO_USER".to_string(), context.current_user.name.clone()); assert_eq!(context.command.command.to_str().unwrap(), "/usr/bin/echo"); assert_eq!(context.command.arguments, ["hello"]); - assert_eq!(context.hostname, hostname()); + assert_eq!(context.hostname, Hostname::resolve()); assert_eq!(context.target_user.uid, 0); } } diff -Nru rust-sudo-rs-0.2.1/src/common/error.rs rust-sudo-rs-0.2.2/src/common/error.rs --- rust-sudo-rs-0.2.1/src/common/error.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/common/error.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,20 +1,22 @@ -use crate::pam::PamError; +use crate::{pam::PamError, system::Hostname}; use std::{borrow::Cow, fmt, path::PathBuf}; +use super::{SudoPath, SudoString}; + #[derive(Debug)] pub enum Error { Silent, NotAllowed { - username: String, + username: SudoString, command: Cow<'static, str>, - hostname: String, - other_user: Option, + hostname: Hostname, + other_user: Option, }, SelfCheck, CommandNotFound(PathBuf), InvalidCommand(PathBuf), ChDirNotAllowed { - chdir: PathBuf, + chdir: SudoPath, command: PathBuf, }, UserNotFound(String), @@ -23,8 +25,10 @@ Configuration(String), Options(String), Pam(PamError), - IoError(Option, std::io::Error), + Io(Option, std::io::Error), MaxAuthAttempts(usize), + PathValidation(PathBuf), + StringValidation(String), } impl fmt::Display for Error { @@ -60,7 +64,7 @@ Error::Configuration(e) => write!(f, "invalid configuration: {e}"), Error::Options(e) => write!(f, "{e}"), Error::Pam(e) => write!(f, "PAM error: {e}"), - Error::IoError(location, e) => { + Error::Io(location, e) => { if let Some(path) = location { write!(f, "cannot execute '{}': {e}", path.display()) } else { @@ -76,6 +80,12 @@ chdir.display(), command.display() ), + Error::StringValidation(string) => { + write!(f, "invalid string: {string:?}") + } + Error::PathValidation(path) => { + write!(f, "invalid path: {path:?}") + } } } } @@ -88,7 +98,7 @@ impl From for Error { fn from(err: std::io::Error) -> Self { - Error::IoError(None, err) + Error::Io(None, err) } } diff -Nru rust-sudo-rs-0.2.1/src/common/mod.rs rust-sudo-rs-0.2.2/src/common/mod.rs --- rust-sudo-rs-0.2.1/src/common/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/common/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -4,12 +4,16 @@ pub use command::CommandAndArguments; pub use context::Context; pub use error::Error; +pub use path::SudoPath; +pub use string::SudoString; pub mod bin_serde; pub mod command; pub mod context; pub mod error; +mod path; pub mod resolve; +mod string; pub type Environment = HashMap; diff -Nru rust-sudo-rs-0.2.1/src/common/path.rs rust-sudo-rs-0.2.2/src/common/path.rs --- rust-sudo-rs-0.2.1/src/common/path.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/common/path.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,105 @@ +use std::{ + ffi::OsString, + ops, + os::unix::prelude::OsStrExt, + path::{Path, PathBuf}, + str, +}; + +use super::{Error, SudoString}; + +/// A `PathBuf` guaranteed to not contain null bytes and be UTF-8 encoded +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(test, derive(Eq))] +pub struct SudoPath { + inner: String, +} + +impl SudoPath { + pub fn new(path: PathBuf) -> Result { + let bytes = path.as_os_str().as_bytes(); + if bytes.contains(&0) { + return Err(Error::PathValidation(path)); + } + + // check this through a reference so we can return `path` in the error case + if str::from_utf8(bytes).is_err() { + return Err(Error::PathValidation(path)); + } + + Ok(Self { + // NOTE(unwrap): UTF-8 encoding is checked above + inner: path.into_os_string().into_string().unwrap(), + }) + } + + pub fn from_cli_string(cli_string: impl Into) -> Self { + Self::new(cli_string.into().into()) + .expect("strings that come in from CLI should not have interior null bytes") + } + + /// Resolve the use of a '~' that occurs in this `SudoPathBuf`; based on the sudoers context + pub fn expand_tilde_in_path(&self, default_username: &SudoString) -> Result { + if let Some(prefix) = self.inner.strip_prefix('~') { + let (username, relpath) = prefix.split_once('/').unwrap_or((prefix, "")); + + let username = if username.is_empty() { + default_username.clone() + } else { + SudoString::new(username.to_string()).unwrap() + }; + + let home_dir = crate::system::User::from_name(username.as_cstr()) + .ok() + .flatten() + .ok_or(Error::UserNotFound(username.to_string()))? + .home; + let path = home_dir.join(relpath); + + Self::new(path) + } else { + Ok(self.clone()) + } + } +} + +impl From for PathBuf { + fn from(value: SudoPath) -> Self { + value.inner.into() + } +} + +impl AsRef for SudoPath { + fn as_ref(&self) -> &Path { + self.inner.as_ref() + } +} + +impl ops::Deref for SudoPath { + type Target = Path; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl TryFrom for SudoPath { + type Error = Error; + + fn try_from(value: String) -> Result { + Self::new(value.into()) + } +} + +impl From for OsString { + fn from(value: SudoPath) -> Self { + value.inner.into() + } +} + +#[cfg(test)] +impl From<&'_ str> for SudoPath { + fn from(value: &'_ str) -> Self { + Self::new(value.into()).unwrap() + } +} diff -Nru rust-sudo-rs-0.2.1/src/common/resolve.rs rust-sudo-rs-0.2.2/src/common/resolve.rs --- rust-sudo-rs-0.2.1/src/common/resolve.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/common/resolve.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,22 +1,28 @@ -use crate::cli::SudoOptions; use crate::system::{Group, User}; +use core::fmt; use std::{ - env, fs, io, + env, + ffi::CStr, + fs, io, ops, os::unix::prelude::MetadataExt, path::{Path, PathBuf}, str::FromStr, }; -use super::{context::LaunchType, Error}; +use super::SudoString; +use super::{ + context::{LaunchType, OptionsForContext}, + Error, +}; #[derive(PartialEq, Debug)] enum NameOrId<'a, T: FromStr> { - Name(&'a str), + Name(&'a SudoString), Id(T), } impl<'a, T: FromStr> NameOrId<'a, T> { - pub fn parse(input: &'a str) -> Option { + pub fn parse(input: &'a SudoString) -> Option { if input.is_empty() { None } else if let Some(stripped) = input.strip_prefix('#') { @@ -27,14 +33,48 @@ } } -pub fn resolve_current_user() -> Result { - User::real()?.ok_or(Error::UserNotFound("current user".to_string())) +#[derive(Clone)] +pub struct CurrentUser { + inner: User, +} + +impl From for User { + fn from(value: CurrentUser) -> Self { + value.inner + } +} + +impl fmt::Debug for CurrentUser { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("CurrentUser").field(&self.inner).finish() + } +} + +impl ops::Deref for CurrentUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl CurrentUser { + #[cfg(test)] + pub fn fake(user: User) -> Self { + Self { inner: user } + } + + pub fn resolve() -> Result { + Ok(Self { + inner: User::real()?.ok_or(Error::UserNotFound("current user".to_string()))?, + }) + } } type Shell = Option; pub(super) fn resolve_launch_and_shell( - sudo_options: &SudoOptions, + sudo_options: &OptionsForContext, current_user: &User, target_user: &User, ) -> (LaunchType, Shell) { @@ -52,30 +92,22 @@ } pub(crate) fn resolve_target_user_and_group( - target_user_name_or_id: &Option, - target_group_name_or_id: &Option, - current_user: &User, + target_user_name_or_id: &Option, + target_group_name_or_id: &Option, + current_user: &CurrentUser, ) -> Result<(User, Group), Error> { // resolve user name or # to a user let mut target_user = - match NameOrId::parse(target_user_name_or_id.as_deref().unwrap_or_default()) { - Some(NameOrId::Name(name)) => User::from_name(name)?, - Some(NameOrId::Id(uid)) => User::from_uid(uid)?, - _ => None, - }; + resolve_from_name_or_id(target_user_name_or_id, User::from_name, User::from_uid)?; // resolve group name or # to a group let mut target_group = - match NameOrId::parse(target_group_name_or_id.as_deref().unwrap_or_default()) { - Some(NameOrId::Name(name)) => Group::from_name(name)?, - Some(NameOrId::Id(gid)) => Group::from_gid(gid)?, - _ => None, - }; + resolve_from_name_or_id(target_group_name_or_id, Group::from_name, Group::from_gid)?; match (&target_user_name_or_id, &target_group_name_or_id) { // when -g is specified, but -u is not specified default -u to the current user (None, Some(_)) => { - target_user = Some(current_user.clone()); + target_user = Some(current_user.clone().into()); } // when -u is specified but -g is not specified, default -g to the primary group of the specified user (Some(_), None) => { @@ -85,8 +117,8 @@ } // when no -u or -g is specified, default to root:root (None, None) => { - target_user = User::from_name("root")?; - target_group = Group::from_name("root")?; + target_user = User::from_name(cstr!("root"))?; + target_group = Group::from_name(cstr!("root"))?; } _ => {} } @@ -113,6 +145,21 @@ } } +fn resolve_from_name_or_id( + input: &Option, + from_name: impl FnOnce(&CStr) -> Result, E>, + from_id: impl FnOnce(I) -> Result, E>, +) -> Result, E> +where + I: FromStr, +{ + match input.as_ref().and_then(NameOrId::parse) { + Some(NameOrId::Name(name)) => from_name(name.as_cstr()), + Some(NameOrId::Id(id)) => from_id(id), + None => Ok(None), + } +} + /// Check whether a path points to a regular file and any executable flag is set pub(crate) fn is_valid_executable(path: &PathBuf) -> bool { if path.is_file() { @@ -171,39 +218,13 @@ }) } -/// Resolve the use of a '~' that occurs in a PathBuf; based on the sudoers context -pub(crate) fn expand_tilde_in_path( - default_user: &str, - mut path: PathBuf, -) -> Result { - let mut iter = path.iter(); - if let Some(mut user_name) = iter - .next() - .and_then(|s| s.to_str()) - .and_then(|s| s.strip_prefix('~')) - { - if user_name.is_empty() { - user_name = default_user - } - let home_dir = crate::system::User::from_name(user_name) - .ok() - .flatten() - .ok_or(Error::UserNotFound(user_name.to_string()))? - .home; - path = home_dir.join(iter.collect::()) - } - - Ok(path) -} - #[cfg(test)] mod tests { use std::path::PathBuf; - use super::{ - is_valid_executable, resolve_current_user, resolve_path, resolve_target_user_and_group, - NameOrId, - }; + use crate::common::resolve::CurrentUser; + + use super::{is_valid_executable, resolve_path, resolve_target_user_and_group, NameOrId}; #[test] fn test_resolve_path() { @@ -230,16 +251,25 @@ #[test] fn test_name_or_id() { - assert_eq!(NameOrId::::parse(""), None); - assert_eq!(NameOrId::::parse("mies"), Some(NameOrId::Name("mies"))); - assert_eq!(NameOrId::::parse("1337"), Some(NameOrId::Name("1337"))); - assert_eq!(NameOrId::::parse("#1337"), Some(NameOrId::Id(1337))); - assert_eq!(NameOrId::::parse("#-1"), None); + assert_eq!(NameOrId::::parse(&"".into()), None); + assert_eq!( + NameOrId::::parse(&"mies".into()), + Some(NameOrId::Name(&"mies".into())) + ); + assert_eq!( + NameOrId::::parse(&"1337".into()), + Some(NameOrId::Name(&"1337".into())) + ); + assert_eq!( + NameOrId::::parse(&"#1337".into()), + Some(NameOrId::Id(1337)) + ); + assert_eq!(NameOrId::::parse(&"#-1".into()), None); } #[test] fn test_resolve_target_user_and_group() { - let current_user = resolve_current_user().unwrap(); + let current_user = CurrentUser::resolve().unwrap(); // fallback to root let (user, group) = resolve_target_user_and_group(&None, &None, ¤t_user).unwrap(); @@ -247,34 +277,25 @@ assert_eq!(group.name, "root"); // unknown user - let result = resolve_target_user_and_group( - &Some("non_existing_ghost".to_string()), - &None, - ¤t_user, - ); + let result = + resolve_target_user_and_group(&Some("non_existing_ghost".into()), &None, ¤t_user); assert!(result.is_err()); // unknown user - let result = resolve_target_user_and_group( - &None, - &Some("non_existing_ghost".to_string()), - ¤t_user, - ); + let result = + resolve_target_user_and_group(&None, &Some("non_existing_ghost".into()), ¤t_user); assert!(result.is_err()); // fallback to current user when different group specified let (user, group) = - resolve_target_user_and_group(&None, &Some("root".to_string()), ¤t_user).unwrap(); + resolve_target_user_and_group(&None, &Some("root".into()), ¤t_user).unwrap(); assert_eq!(user.name, current_user.name); assert_eq!(group.name, "root"); // fallback to current users group when no group specified - let (user, group) = resolve_target_user_and_group( - &Some(current_user.name.to_string()), - &None, - ¤t_user, - ) - .unwrap(); + let (user, group) = + resolve_target_user_and_group(&Some(current_user.name.clone()), &None, ¤t_user) + .unwrap(); assert_eq!(user.name, current_user.name); assert_eq!(group.gid, current_user.gid); } diff -Nru rust-sudo-rs-0.2.1/src/common/string.rs rust-sudo-rs-0.2.2/src/common/string.rs --- rust-sudo-rs-0.2.1/src/common/string.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/common/string.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,150 @@ +use core::fmt; +use std::{ + ffi::{CStr, OsString}, + ops, +}; + +use crate::common::Error; + +const NULL_BYTE: char = '\0'; +const NULL_BYTE_UTF8_LEN: usize = NULL_BYTE.len_utf8(); + +/// A UTF-8 encoded string with no interior null bytes +/// +/// This type can be converted into a C (null-terminated) string at no cost +#[derive(Clone, PartialEq, Eq)] +pub struct SudoString { + inner: String, +} + +impl SudoString { + pub fn new(mut string: String) -> Result { + if string.as_bytes().contains(&0) { + return Err(Error::StringValidation(string)); + } + + string.push(NULL_BYTE); + + Ok(Self { inner: string }) + } + + pub fn from_cli_string(cli_string: impl Into) -> Self { + Self::new(cli_string.into()) + .expect("strings that come in from CLI should not have interior null bytes") + } + + pub fn as_cstr(&self) -> &CStr { + CStr::from_bytes_with_nul(self.inner.as_bytes()).unwrap() + } + + pub fn as_str(&self) -> &str { + self + } +} + +impl Default for SudoString { + fn default() -> Self { + Self { + inner: NULL_BYTE.into(), + } + } +} + +#[cfg(test)] +impl From<&'_ str> for SudoString { + fn from(value: &'_ str) -> Self { + SudoString::try_from(value.to_string()).unwrap() + } +} + +impl TryFrom for SudoString { + type Error = Error; + + fn try_from(value: String) -> Result { + Self::new(value) + } +} + +impl From for String { + fn from(value: SudoString) -> Self { + let mut s = value.inner; + s.pop(); + s + } +} + +impl From for OsString { + fn from(value: SudoString) -> Self { + let mut s = value.inner; + s.pop(); + OsString::from(s) + } +} + +impl ops::Deref for SudoString { + type Target = str; + + fn deref(&self) -> &Self::Target { + let num_bytes = self.inner.as_bytes().len(); + &self.inner[..num_bytes - NULL_BYTE_UTF8_LEN] + } +} + +impl fmt::Debug for SudoString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s: &str = self; + fmt::Debug::fmt(s, f) + } +} + +impl fmt::Display for SudoString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self) + } +} + +impl PartialEq for SudoString { + fn eq(&self, other: &str) -> bool { + let s: &str = self; + s == other + } +} + +impl PartialEq<&'_ str> for SudoString { + fn eq(&self, other: &&str) -> bool { + let s: &str = self; + s == *other + } +} + +#[cfg(test)] +mod tests { + use std::ffi::CString; + + use super::*; + + #[test] + fn null_byte_is_utf8_encoded_as_a_single_byte() { + assert_eq!(1, NULL_BYTE_UTF8_LEN) + } + + #[test] + fn sanity_check() { + let expected = "hello"; + let s = SudoString::new("hello".to_string()).unwrap(); + assert_eq!(expected, &*s); + } + + #[test] + fn cstr_conversion() { + let expected = "hello"; + let cstr = CString::from_vec_with_nul((expected.to_string() + "\0").into_bytes()).unwrap(); + let s = SudoString::new(expected.to_string()).unwrap(); + assert_eq!(&*cstr, s.as_cstr()); + } + + #[test] + fn rejects_string_that_contains_interior_null() { + assert!(SudoString::new("he\0llo".to_string()).is_err()); + } +} diff -Nru rust-sudo-rs-0.2.1/src/env/environment.rs rust-sudo-rs-0.2.2/src/env/environment.rs --- rust-sudo-rs-0.2.1/src/env/environment.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/env/environment.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,297 +0,0 @@ -use std::{ - collections::{hash_map::Entry, HashSet}, - ffi::{OsStr, OsString}, - os::unix::prelude::OsStrExt, -}; - -use crate::common::{CommandAndArguments, Context, Environment}; -use crate::sudoers::Policy; -use crate::system::PATH_MAX; - -use super::wildcard_match::wildcard_match; - -const PATH_MAILDIR: &str = env!("PATH_MAILDIR"); -const PATH_ZONEINFO: &str = env!("PATH_ZONEINFO"); -const PATH_DEFAULT: &str = env!("SUDO_PATH_DEFAULT"); - -/// check byte slice contains with given byte slice -fn contains_subsequence(haystack: &[u8], needle: &[u8]) -> bool { - haystack - .windows(needle.len()) - .any(|window| window == needle) -} - -/// Formats the command and arguments passed for the SUDO_COMMAND -/// environment variable. Limit the length to 4096 bytes to prevent -/// execve failure for very long argument vectors -fn format_command(command_and_arguments: &CommandAndArguments) -> OsString { - let mut formatted: OsString = command_and_arguments.command.clone().into(); - - for arg in &command_and_arguments.arguments { - if formatted.len() + arg.len() < 4096 { - formatted.push(" "); - formatted.push(arg); - } - } - - formatted -} - -/// Construct sudo-specific environment variables -fn add_extra_env( - context: &Context, - cfg: &impl Policy, - sudo_ps1: Option, - environment: &mut Environment, -) { - // current user - environment.insert("SUDO_COMMAND".into(), format_command(&context.command)); - environment.insert( - "SUDO_UID".into(), - context.current_user.uid.to_string().into(), - ); - environment.insert( - "SUDO_GID".into(), - context.current_user.gid.to_string().into(), - ); - environment.insert("SUDO_USER".into(), context.current_user.name.clone().into()); - // target user - if let Entry::Vacant(entry) = environment.entry("MAIL".into()) { - entry.insert(format!("{PATH_MAILDIR}/{}", context.target_user.name).into()); - } - // The current SHELL variable should determine the shell to run when -s is passed, if none set use passwd entry - environment.insert("SHELL".into(), context.target_user.shell.clone().into()); - // HOME' Set to the home directory of the target user if -i or -H are specified, env_reset or always_set_home are - // set in sudoers, or when the -s option is specified and set_home is set in sudoers. - // Since we always want to do env_reset -> always set HOME - if let Entry::Vacant(entry) = environment.entry("HOME".into()) { - entry.insert(context.target_user.home.clone().into()); - } - - match ( - environment.get(OsStr::new("LOGNAME")), - environment.get(OsStr::new("USER")), - ) { - // Set to the login name of the target user when the -i option is specified, - // when the set_logname option is enabled in sudoers, or when the env_reset option - // is enabled in sudoers (unless LOGNAME is present in the env_keep list). - // Since we always want to do env_reset -> always set these except when present in env - (None, None) => { - environment.insert("LOGNAME".into(), context.target_user.name.clone().into()); - environment.insert("USER".into(), context.target_user.name.clone().into()); - } - // LOGNAME should be set to the same value as USER if the latter is preserved. - (None, Some(user)) => { - environment.insert("LOGNAME".into(), user.clone()); - } - // USER should be set to the same value as LOGNAME if the latter is preserved. - (Some(logname), None) => { - environment.insert("USER".into(), logname.clone()); - } - (Some(_), Some(_)) => {} - } - - // Overwrite PATH when secure_path is set - if let Some(secure_path) = cfg.secure_path() { - // assign path by env path or secure_path configuration - environment.insert("PATH".into(), secure_path.into()); - } - // If the PATH and TERM variables are not preserved from the user's environment, they will be set to default value - if !environment.contains_key(OsStr::new("PATH")) { - // If the PATH variable is not set, it will be set to default value - environment.insert("PATH".into(), PATH_DEFAULT.into()); - } - // If the TERM variable is not preserved from the user's environment, it will be set to default value - if !environment.contains_key(OsStr::new("TERM")) { - environment.insert("TERM".into(), "unknown".into()); - } - // The SUDO_PS1 variable requires special treatment as the PS1 variable must be set in the - // target environment to the same value of SUDO_PS1 if the latter is set. - if let Some(sudo_ps1_value) = sudo_ps1 { - // set PS1 to the SUDO_PS1 value in the target environment - environment.insert("PS1".into(), sudo_ps1_value); - } -} - -/// Check a string only contains printable (non-space) characters -fn is_printable(input: &[u8]) -> bool { - input - .iter() - .all(|c| c.is_ascii_alphanumeric() || c.is_ascii_punctuation()) -} - -/// The TZ variable is considered unsafe if any of the following are true: -/// It consists of a fully-qualified path name, optionally prefixed with a colon (‘:’), that does not match the location of the zoneinfo directory. -/// It contains a .. path element. -/// It contains white space or non-printable characters. -/// It is longer than the value of PATH_MAX. -fn is_safe_tz(value: &[u8]) -> bool { - let check_value = if value.starts_with(&[b':']) { - &value[1..] - } else { - value - }; - - if check_value.starts_with(&[b'/']) { - if !PATH_ZONEINFO.is_empty() { - if !check_value.starts_with(PATH_ZONEINFO.as_bytes()) - || check_value.get(PATH_ZONEINFO.len()) != Some(&b'/') - { - return false; - } - } else { - return false; - } - } - - !contains_subsequence(check_value, "..".as_bytes()) - && is_printable(check_value) - && check_value.len() < PATH_MAX as usize -} - -/// Check whether the needle exists in a haystack, in which the haystack is a list of patterns, possibly containing wildcards -fn in_table(needle: &OsStr, haystack: &HashSet) -> bool { - haystack - .iter() - .any(|pattern| wildcard_match(needle.as_bytes(), pattern.as_bytes())) -} - -/// Determine whether a specific environment variable should be kept -fn should_keep(key: &OsStr, value: &OsStr, cfg: &impl Policy) -> bool { - if value.as_bytes().starts_with("()".as_bytes()) { - return false; - } - - if key == "TZ" { - return in_table(key, cfg.env_keep()) - || (in_table(key, cfg.env_check()) && is_safe_tz(value.as_bytes())); - } - - if in_table(key, cfg.env_check()) { - return !value.as_bytes().iter().any(|c| *c == b'%' || *c == b'/'); - } - - in_table(key, cfg.env_keep()) -} - -/// Construct the final environment from the current one and a sudo context -/// see for the original implementation -/// see for the original documentation -/// -/// The HOME, MAIL, SHELL, LOGNAME and USER environment variables are initialized based on the target user -/// and the SUDO_* variables are set based on the invoking user. -/// -/// Additional variables, such as DISPLAY, PATH and TERM, are preserved from the invoking user's -/// environment if permitted by the env_check, or env_keep options -/// -/// If the PATH and TERM variables are not preserved from the user's environment, they will be set to default value -/// -/// Environment variables with a value beginning with ‘()’ are removed -pub fn get_target_environment( - current_env: Environment, - additional_env: Environment, - context: &Context, - settings: &impl Policy, -) -> Environment { - let mut environment = Environment::default(); - - // retrieve SUDO_PS1 value to set a PS1 value as additional environment - let sudo_ps1 = current_env.get(OsStr::new("SUDO_PS1")).cloned(); - - // variables preserved from the invoking user's environment by the - // env_keep list take precedence over those in the PAM environment - environment.extend(additional_env); - - environment.extend( - current_env - .into_iter() - .filter(|(key, value)| should_keep(key, value, settings)), - ); - - add_extra_env(context, settings, sudo_ps1, &mut environment); - - environment -} - -#[cfg(test)] -mod tests { - use super::{is_safe_tz, should_keep, PATH_ZONEINFO}; - use crate::sudoers::Policy; - use std::{collections::HashSet, ffi::OsStr}; - - struct TestConfiguration { - keep: HashSet, - check: HashSet, - } - - impl Policy for TestConfiguration { - fn env_keep(&self) -> &HashSet { - &self.keep - } - - fn env_check(&self) -> &HashSet { - &self.check - } - - fn secure_path(&self) -> Option { - None - } - - fn use_pty(&self) -> bool { - true - } - } - - #[test] - fn test_filtering() { - let config = TestConfiguration { - keep: HashSet::from(["AAP".to_string(), "NOOT".to_string()]), - check: HashSet::from(["MIES".to_string(), "TZ".to_string()]), - }; - - let check_should_keep = |key: &str, value: &str, expected: bool| { - assert_eq!( - should_keep(OsStr::new(key), OsStr::new(value), &config), - expected, - "{} should {}", - key, - if expected { "be kept" } else { "not be kept" } - ); - }; - - check_should_keep("AAP", "FOO", true); - check_should_keep("MIES", "BAR", true); - check_should_keep("AAP", "()=foo", false); - check_should_keep("TZ", "Europe/Amsterdam", true); - check_should_keep("TZ", "../Europe/Berlin", false); - check_should_keep("MIES", "FOO/BAR", false); - check_should_keep("MIES", "FOO%", false); - } - - #[allow(clippy::useless_format)] - #[allow(clippy::bool_assert_comparison)] - #[test] - fn test_tzinfo() { - assert_eq!(is_safe_tz("Europe/Amsterdam".as_bytes()), true); - assert_eq!( - is_safe_tz(format!("{PATH_ZONEINFO}/Europe/London").as_bytes()), - true - ); - assert_eq!( - is_safe_tz(format!(":{PATH_ZONEINFO}/Europe/Amsterdam").as_bytes()), - true - ); - assert_eq!( - is_safe_tz(format!("/schaap/Europe/Amsterdam").as_bytes()), - false - ); - assert_eq!( - is_safe_tz(format!("{PATH_ZONEINFO}/../Europe/London").as_bytes()), - false - ); - assert_eq!( - is_safe_tz(format!("{PATH_ZONEINFO}/../Europe/London").as_bytes()), - false - ); - } -} diff -Nru rust-sudo-rs-0.2.1/src/env/mod.rs rust-sudo-rs-0.2.2/src/env/mod.rs --- rust-sudo-rs-0.2.1/src/env/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/env/mod.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -#![forbid(unsafe_code)] - -pub mod environment; -pub mod wildcard_match; - -#[cfg(test)] -mod tests; diff -Nru rust-sudo-rs-0.2.1/src/env/tests.rs rust-sudo-rs-0.2.2/src/env/tests.rs --- rust-sudo-rs-0.2.1/src/env/tests.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/env/tests.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,173 +0,0 @@ -use crate::cli::SudoOptions; -use crate::common::{CommandAndArguments, Context, Environment}; -use crate::env::environment::get_target_environment; -use crate::system::{Group, Process, User}; -use std::collections::{HashMap, HashSet}; - -const TESTS: &str = " -> env - FOO=BAR - HOME=/home/test - HOSTNAME=test-ubuntu - LANG=en_US.UTF-8 - LANGUAGE=en_US.UTF-8 - LC_ALL=en_US.UTF-8 - LS_COLORS=cd=40;33;01:*.jpg=01;35:*.mp3=00;36: - PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - PWD=/home/test - SHLVL=0 - TERM=xterm - _=/usr/bin/sudo -> sudo env - HOSTNAME=test-ubuntu - LANG=en_US.UTF-8 - LANGUAGE=en_US.UTF-8 - LC_ALL=en_US.UTF-8 - LS_COLORS=cd=40;33;01:*.jpg=01;35:*.mp3=00;36: - MAIL=/var/mail/root - PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - SHELL=/bin/bash - SUDO_COMMAND=/usr/bin/env - SUDO_GID=1000 - SUDO_UID=1000 - SUDO_USER=test - HOME=/root - LOGNAME=root - USER=root - TERM=xterm -> sudo -u test env - HOSTNAME=test-ubuntu - LANG=en_US.UTF-8 - LANGUAGE=en_US.UTF-8 - LC_ALL=en_US.UTF-8 - LS_COLORS=cd=40;33;01:*.jpg=01;35:*.mp3=00;36: - MAIL=/var/mail/test - PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - SHELL=/bin/sh - SUDO_COMMAND=/usr/bin/env - SUDO_GID=1000 - SUDO_UID=1000 - SUDO_USER=test - HOME=/home/test - LOGNAME=test - USER=test - TERM=xterm -"; - -fn parse_env_commands(input: &str) -> Vec<(&str, Environment)> { - input - .trim() - .split("> ") - .filter(|l| !l.is_empty()) - .map(|e| { - let (cmd, vars) = e.split_once('\n').unwrap(); - - let vars: Environment = vars - .lines() - .map(|line| line.trim().split_once('=').unwrap()) - .map(|(k, v)| (k.into(), v.into())) - .collect(); - - (cmd, vars) - }) - .collect() -} - -fn create_test_context(sudo_options: &SudoOptions) -> Context { - let path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(); - let command = CommandAndArguments::build_from_args(None, sudo_options.clone().args(), &path); - - let current_user = User { - uid: 1000, - gid: 1000, - name: "test".to_string(), - gecos: String::new(), - home: "/home/test".into(), - shell: "/bin/sh".into(), - passwd: String::new(), - groups: vec![], - }; - - let current_group = Group { - gid: 1000, - name: "test".to_string(), - passwd: String::new(), - members: Vec::new(), - }; - - let root_user = User { - uid: 0, - gid: 0, - name: "root".to_string(), - gecos: String::new(), - home: "/root".into(), - shell: "/bin/bash".into(), - passwd: String::new(), - groups: vec![], - }; - - let root_group = Group { - gid: 0, - name: "root".to_string(), - passwd: String::new(), - members: Vec::new(), - }; - - Context { - hostname: "test-ubuntu".to_string(), - command, - current_user: current_user.clone(), - target_user: if sudo_options.user.as_deref() == Some("test") { - current_user - } else { - root_user - }, - target_group: if sudo_options.user.as_deref() == Some("test") { - current_group - } else { - root_group - }, - launch: crate::common::context::LaunchType::Direct, - chdir: sudo_options.directory.clone(), - stdin: sudo_options.stdin, - non_interactive: sudo_options.non_interactive, - process: Process::new(), - use_session_records: false, - use_pty: true, - } -} - -fn environment_to_set(environment: Environment) -> HashSet { - HashSet::from_iter( - environment - .iter() - .map(|(k, v)| format!("{}={}", k.to_str().unwrap(), v.to_str().unwrap())), - ) -} - -#[test] -fn test_environment_variable_filtering() { - let mut parts = parse_env_commands(TESTS); - let initial_env = parts.remove(0).1; - - for (cmd, expected_env) in parts { - let options = SudoOptions::try_parse_from(cmd.split_whitespace()).unwrap(); - let settings = crate::sudoers::Judgement::default(); - let context = create_test_context(&options); - let resulting_env = - get_target_environment(initial_env.clone(), HashMap::new(), &context, &settings); - - let resulting_env = environment_to_set(resulting_env); - let expected_env = environment_to_set(expected_env); - let mut diff = resulting_env - .symmetric_difference(&expected_env) - .collect::>(); - - diff.sort(); - - assert!( - diff.is_empty(), - "\"{cmd}\" results in an environment mismatch:\n{diff:#?}", - ); - } -} diff -Nru rust-sudo-rs-0.2.1/src/env/wildcard_match.rs rust-sudo-rs-0.2.2/src/env/wildcard_match.rs --- rust-sudo-rs-0.2.1/src/env/wildcard_match.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/env/wildcard_match.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,86 +0,0 @@ -/// Match a test input with a pattern -/// Only wildcard characters (*) in the pattern string have a special meaning: they match on zero or more characters -pub(super) fn wildcard_match(test: &[u8], pattern: &[u8]) -> bool { - let mut test_index = 0; - let mut pattern_index = 0; - let mut last_star = None; - - loop { - match (pattern.get(pattern_index), test.get(test_index)) { - (Some(p), Some(t)) => { - if *p == b'*' { - pattern_index += 1; - last_star = Some((test_index, pattern_index)); - } else if p == t { - pattern_index += 1; - test_index += 1; - } else if let Some((t_index, p_index)) = last_star { - test_index = t_index + 1; - pattern_index = p_index; - last_star = Some((test_index, pattern_index)); - } else { - return false; - } - } - (None, Some(_)) => { - if let Some((t_index, p_index)) = last_star { - test_index = t_index + 1; - pattern_index = p_index; - last_star = Some((test_index, pattern_index)); - } else { - return false; - } - } - (Some(b'*'), None) => { - pattern_index += 1; - } - (None, None) => { - return true; - } - _ => { - return false; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::wildcard_match; - - #[test] - fn test_wildcard_match() { - let tests = vec![ - ("foo bar", "foo *", true), - ("foo bar", "foo ba*", true), - ("foo bar", "foo *ar", true), - ("foo bar", "foo *r", true), - ("foo bar", "foo *ab", false), - ("foo bar", "foo r*", false), - ("foo bar", "*oo bar", true), - ("foo bar", "*f* bar", true), - ("foo bar", "*f bar", false), - ("foo ", "foo *", true), - ("foo", "foo *", false), - ("foo", "foo*", true), - ("foo bar", "f*******r", true), - ("foo******bar", "f*r", true), - ("foo********bar", "foo bar", false), - ("#%^$V@#TYH%&rot13%#@$%#$%", "#%^$V@#*t13%#@$%#$%", true), - ("#%^$V@#TYH%&rot13%#@$%#$%", "*%^*%&rot*%#$%", true), - ("#%^$V@#TYH%&rot13%#@$%#$%", "#%^$V@#TYH%&r*%#@$#$%", false), - ("#%^$V@#TYH%&rot13%#@$%#$%", "#%^$V@#*******@$%#$%", true), - ]; - - for (test, pattern, expected) in tests.into_iter() { - assert_eq!( - wildcard_match(test.as_bytes(), pattern.as_bytes()), - expected, - "\"{}\" {} match {}", - test, - if expected { "should" } else { "should not" }, - pattern - ); - } - } -} diff -Nru rust-sudo-rs-0.2.1/src/exec/interface.rs rust-sudo-rs-0.2.2/src/exec/interface.rs --- rust-sudo-rs-0.2.1/src/exec/interface.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/exec/interface.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,14 +1,17 @@ use std::io::{self, ErrorKind}; use std::path::PathBuf; -use crate::common::{context::LaunchType, Context}; -use crate::system::{Group, User}; +use crate::common::SudoPath; +use crate::{ + common::{context::LaunchType, Context}, + system::{Group, User}, +}; pub trait RunOptions { fn command(&self) -> io::Result<&PathBuf>; fn arguments(&self) -> &Vec; fn arg0(&self) -> Option<&PathBuf>; - fn chdir(&self) -> Option<&PathBuf>; + fn chdir(&self) -> Option<&SudoPath>; fn is_login(&self) -> bool; fn user(&self) -> &User; fn requesting_user(&self) -> &User; @@ -34,7 +37,7 @@ self.command.arg0.as_ref() } - fn chdir(&self) -> Option<&PathBuf> { + fn chdir(&self) -> Option<&SudoPath> { self.chdir.as_ref() } diff -Nru rust-sudo-rs-0.2.1/src/exec/mod.rs rust-sudo-rs-0.2.2/src/exec/mod.rs --- rust-sudo-rs-0.2.1/src/exec/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/exec/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -64,20 +64,22 @@ if let Some(arg0) = options.arg0() { command.arg0(arg0); } - // Decide if the pwd should be changed. `--chdir` takes precedence over `-i`. - let path = options.chdir().cloned().or_else(|| { - options.is_login().then(|| { - // signal to the operating system that the command is a login shell by prefixing "-" - let mut process_name = qualified_path - .file_name() - .map(|osstr| osstr.as_bytes().to_vec()) - .unwrap_or_else(Vec::new); - process_name.insert(0, b'-'); - command.arg0(OsStr::from_bytes(&process_name)); - options.user().home.clone() - }) - }); + if options.is_login() { + // signal to the operating system that the command is a login shell by prefixing "-" + let mut process_name = qualified_path + .file_name() + .map(|osstr| osstr.as_bytes().to_vec()) + .unwrap_or_else(Vec::new); + process_name.insert(0, b'-'); + command.arg0(OsStr::from_bytes(&process_name)); + } + + // Decide if the pwd should be changed. `--chdir` takes precedence over `-i`. + let path = options + .chdir() + .cloned() + .or_else(|| options.is_login().then(|| options.user().home.clone())); // set target user and groups set_target_user( diff -Nru rust-sudo-rs-0.2.1/src/exec/use_pty/parent.rs rust-sudo-rs-0.2.2/src/exec/use_pty/parent.rs --- rust-sudo-rs-0.2.1/src/exec/use_pty/parent.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/exec/use_pty/parent.rs 2006-07-24 01:21:28.000000000 +0000 @@ -260,13 +260,13 @@ } fn get_pty() -> io::Result { - let tty_gid = Group::from_name("tty") + let tty_gid = Group::from_name(cstr!("tty")) .unwrap_or(None) .map(|group| group.gid); let pty = Pty::open().map_err(|err| { dev_error!("cannot allocate pty: {err}"); - err + io::Error::new(io::ErrorKind::NotFound, "unable to open pty") })?; let euid = User::effective_uid(); diff -Nru rust-sudo-rs-0.2.1/src/exec/use_pty/pipe/mod.rs rust-sudo-rs-0.2.2/src/exec/use_pty/pipe/mod.rs --- rust-sudo-rs-0.2.1/src/exec/use_pty/pipe/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/exec/use_pty/pipe/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,192 @@ +mod ring_buffer; + +use std::{ + io::{self, Read, Write}, + marker::PhantomData, + os::fd::AsRawFd, +}; + +use crate::exec::event::{EventHandle, EventRegistry, PollEvent, Process}; + +use self::ring_buffer::RingBuffer; + +// A pipe able to stream data bidirectionally between two read-write types. +pub(super) struct Pipe { + left: L, + right: R, + buffer_lr: Buffer, + buffer_rl: Buffer, +} + +impl Pipe { + /// Create a new pipe between two read-write types and register them to be polled. + pub fn new( + left: L, + right: R, + registry: &mut EventRegistry, + f_left: fn(PollEvent) -> T::Event, + f_right: fn(PollEvent) -> T::Event, + ) -> Self { + Self { + buffer_lr: Buffer::new( + registry.register_event(&left, PollEvent::Readable, f_left), + registry.register_event(&right, PollEvent::Writable, f_right), + registry, + ), + buffer_rl: Buffer::new( + registry.register_event(&right, PollEvent::Readable, f_right), + registry.register_event(&left, PollEvent::Writable, f_left), + registry, + ), + left, + right, + } + } + + /// Get a reference to the left side of the pipe. + pub(super) fn left(&self) -> &L { + &self.left + } + + /// Get a mutable reference to the left side of the pipe. + pub(super) fn left_mut(&mut self) -> &mut L { + &mut self.left + } + + /// Get a reference to the right side of the pipe. + pub(super) fn right(&self) -> &R { + &self.right + } + + /// Stop the poll events of this pipe. + pub(super) fn ignore_events(&mut self, registry: &mut EventRegistry) { + self.buffer_lr.read_handle.ignore(registry); + self.buffer_lr.write_handle.ignore(registry); + self.buffer_rl.read_handle.ignore(registry); + self.buffer_rl.write_handle.ignore(registry); + } + + /// Resume the poll events of this pipe + pub(super) fn resume_events(&mut self, registry: &mut EventRegistry) { + self.buffer_lr.read_handle.resume(registry); + self.buffer_lr.write_handle.resume(registry); + self.buffer_rl.read_handle.resume(registry); + self.buffer_rl.write_handle.resume(registry); + } + + /// Handle a poll event for the left side of the pipe. + pub(super) fn on_left_event( + &mut self, + poll_event: PollEvent, + registry: &mut EventRegistry, + ) -> io::Result<()> { + match poll_event { + PollEvent::Readable => self.buffer_lr.read(&mut self.left, registry), + PollEvent::Writable => self.buffer_rl.write(&mut self.left, registry), + } + } + + /// Handle a poll event for the right side of the pipe. + pub(super) fn on_right_event( + &mut self, + poll_event: PollEvent, + registry: &mut EventRegistry, + ) -> io::Result<()> { + match poll_event { + PollEvent::Readable => self.buffer_rl.read(&mut self.right, registry), + PollEvent::Writable => self.buffer_lr.write(&mut self.right, registry), + } + } + + /// Ensure that all the contents of the pipe's internal buffer are written to the left side. + pub(super) fn flush_left(&mut self) -> io::Result<()> { + self.buffer_rl.flush(&mut self.left) + } +} + +/// A buffer that stores the bytes read from `R` before they are written to `W`. +struct Buffer { + internal: RingBuffer, + /// The handle for the event of the reader. + read_handle: EventHandle, + /// The handle for the event of the writer. + write_handle: EventHandle, + marker: PhantomData<(R, W)>, +} + +impl Buffer { + /// Create a new, empty buffer + fn new( + read_handle: EventHandle, + mut write_handle: EventHandle, + registry: &mut EventRegistry, + ) -> Self { + // The buffer is empty, don't write + write_handle.ignore(registry); + + Self { + internal: RingBuffer::new(), + read_handle, + write_handle, + marker: PhantomData, + } + } + + /// Read bytes into the buffer. + /// + /// Calling this function will block until `read` is ready to be read. + fn read( + &mut self, + read: &mut R, + registry: &mut EventRegistry, + ) -> io::Result<()> { + // If the buffer is full, there is nothing to be read. + if self.internal.is_full() { + self.read_handle.ignore(registry); + return Ok(()); + } + + // Read bytes and insert them into the buffer. + let inserted_len = self.internal.insert(read)?; + + // If we inserted something, the buffer is not empty anymore and we can resume writing. + if inserted_len > 0 { + self.write_handle.resume(registry); + } + + Ok(()) + } + + /// Write bytes from the buffer. + /// + /// Calling this function will block until `write` is ready to be written. + fn write( + &mut self, + write: &mut W, + registry: &mut EventRegistry, + ) -> io::Result<()> { + // If the buffer is empty, there is nothing to be written. + if self.internal.is_empty() { + self.write_handle.ignore(registry); + return Ok(()); + } + + // Remove bytes from the buffer and write them. + let removed_len = self.internal.remove(write)?; + + // If we removed something, the buffer is not full anymore and we can resume reading. + if removed_len > 0 { + self.read_handle.resume(registry); + } + + Ok(()) + } + + /// Flush this buffer, ensuring that all the contents of its internal buffer are written. + fn flush(&mut self, write: &mut W) -> io::Result<()> { + // Remove bytes from the buffer and write them. + self.internal.remove(write)?; + + write.flush() + } +} diff -Nru rust-sudo-rs-0.2.1/src/exec/use_pty/pipe/ring_buffer.rs rust-sudo-rs-0.2.2/src/exec/use_pty/pipe/ring_buffer.rs --- rust-sudo-rs-0.2.1/src/exec/use_pty/pipe/ring_buffer.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/exec/use_pty/pipe/ring_buffer.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,288 @@ +use std::io::{self, IoSlice, IoSliceMut, Read, Write}; + +pub(super) struct RingBuffer { + storage: Box<[u8; Self::LEN]>, + // The start index of the non-empty section of the buffer. + start: usize, + // The length of the non-empty section of the buffer. + len: usize, +} + +impl RingBuffer { + /// The size of the internal storage of the ring buffer. + const LEN: usize = 8 * 1024; + + /// Create a new, empty buffer. + pub(super) fn new() -> Self { + Self { + storage: Box::new([0; Self::LEN]), + start: 0, + len: 0, + } + } + + pub(super) fn is_full(&self) -> bool { + self.len == self.storage.len() + } + + pub(super) fn insert(&mut self, read: &mut R) -> io::Result { + let inserted_len = if self.is_empty() { + // Case 1.1. The buffer is empty, meaning that there are two unfilled slices in + // `storage`:`start..` and `..start`. + let (second_slice, first_slice) = self.storage.split_at_mut(self.start); + read.read_vectored(&mut [first_slice, second_slice].map(IoSliceMut::new))? + } else { + let &mut Self { start, len, .. } = self; + let end = start + len; + if end >= self.storage.len() { + // Case 1.2. The buffer is not empty and the filled section wraps around `storage`. + // Meaning that there is only one unfilled slice in `storage`: `end..start`. + let end = end % self.storage.len(); + read.read(&mut self.storage[end..start])? + } else { + // Case 1.3. The buffer is not empty and the filled section is a contiguous slice + // of `storage`. Meaning that there are two unfilled slices in `storage`: `..start` + // and `end..`. + let (mid, first_slice) = self.storage.split_at_mut(end); + let second_slice = &mut mid[..start]; + read.read_vectored(&mut [first_slice, second_slice].map(IoSliceMut::new))? + } + }; + + self.len += inserted_len; + + debug_assert!(self.start < Self::LEN); + debug_assert!(self.len <= Self::LEN); + + Ok(inserted_len) + } + + pub(super) fn is_empty(&self) -> bool { + self.len == 0 + } + + pub(super) fn remove(&mut self, write: &mut W) -> io::Result { + let removed_len = if self.is_full() { + // Case 2.1. The buffer is full, meaning that there are two filled slices in `storage`: + // `start..` and `..start`. + let (second_slice, first_slice) = self.storage.split_at(self.start); + write.write_vectored(&[first_slice, second_slice].map(IoSlice::new))? + } else { + let end = self.start + self.len; + if end >= self.storage.len() { + // Case 2.2. The buffer is not full and the filled section wraps around `storage`. + // Meaning that there are two non-empty slices in `storage`: `start..` and `..end`. + let end = end % self.storage.len(); + let first_slice = &self.storage[self.start..]; + let second_slice = &self.storage[..end]; + write.write_vectored(&[first_slice, second_slice].map(IoSlice::new))? + } else { + // Case 2.3. The buffer is not full and the filled section is a contiguous slice + // of `storage.` Meaning that there is only one filled slice in `storage`: + // `start..end`. + write.write(&self.storage[self.start..end])? + } + }; + + self.start += removed_len; + self.start %= Self::LEN; + + self.len -= removed_len; + + debug_assert!(self.start < Self::LEN); + debug_assert!(self.len <= Self::LEN); + + Ok(removed_len) + } +} + +#[cfg(test)] +mod tests { + use super::RingBuffer; + + #[test] + fn empty_buffer_is_empty() { + let buf = RingBuffer::new(); + + assert!(buf.is_empty()); + } + + #[test] + fn full_buffer_is_full() { + let mut buf = RingBuffer::new(); + + let inserted_len = buf.insert(&mut [0x45; RingBuffer::LEN].as_slice()).unwrap(); + assert_eq!(inserted_len, RingBuffer::LEN); + + assert!(buf.is_full()); + } + + #[test] + fn buffer_is_fifo() { + let mut buf = RingBuffer::new(); + + let expected = (0..=u8::MAX).collect::>(); + let inserted_len = buf.insert(&mut expected.as_slice()).unwrap(); + assert_eq!(inserted_len, expected.len()); + + let mut found = vec![]; + let removed_len = buf.remove(&mut found).unwrap(); + assert_eq!(removed_len, expected.len()); + + assert_eq!(expected, found); + } + + #[test] + fn insert_into_empty_buffer_with_offset() { + const HALF_LEN: usize = RingBuffer::LEN / 2; + let mut buf = RingBuffer::new(); + + // This should leave the buffer empty but with the start field pointing to the middle of + // the buffer. + // ┌───────────────────┐ + // │ │ + // └───────────────────┘ + // ▲ + // │ + // start + buf.insert(&mut [0u8; HALF_LEN].as_slice()).unwrap(); + buf.remove(&mut vec![]).unwrap(); + + assert_eq!(buf.start, HALF_LEN); + assert_eq!(buf.len, 0); + + // Then we fill the first half of the buffer with ones and the second one with twos in a + // single insertion. This tests case 1.1. + // ┌─────────┬─────────┐ + // │ 2 │ 1 │ + // └─────────┴─────────┘ + // ▲ + // │ + // start + let mut expected = vec![1; HALF_LEN]; + expected.extend_from_slice(&[2; HALF_LEN]); + buf.insert(&mut expected.as_slice()).unwrap(); + + assert_eq!(buf.start, HALF_LEN); + assert_eq!(buf.len, RingBuffer::LEN); + + // When we remove all the elements of the buffer we should find them in the same order we + // inserted them. This tests case 2.1. + let mut found = vec![]; + let removed_len = buf.remove(&mut found).unwrap(); + assert_eq!(removed_len, expected.len()); + + assert_eq!(expected, found); + + assert_eq!(buf.start, HALF_LEN); + assert_eq!(buf.len, 0); + } + + #[test] + fn insert_into_non_empty_wrapping_buffer() { + const QUARTER_LEN: usize = RingBuffer::LEN / 4; + let mut buf = RingBuffer::new(); + + // This should leave the buffer empty but with the start field pointing to the middle of + // the buffer. + // ┌───────────────────────┐ + // │ │ + // └───────────────────────┘ + // ▲ + // │ + // start + buf.insert(&mut [0; 2 * QUARTER_LEN].as_slice()).unwrap(); + buf.remove(&mut vec![]).unwrap(); + + assert_eq!(buf.start, 2 * QUARTER_LEN); + assert_eq!(buf.len, 0); + + // Then we fill one quarter of the buffer with ones. This gives us a non-empty buffer whose + // empty section is not contiguous. + // ┌───────────┬─────┬─────┐ + // │ │ 1 │ │ + // └───────────┴─────┴─────┘ + // ▲ + // │ + // start + let mut expected = vec![1; QUARTER_LEN]; + buf.insert(&mut expected.as_slice()).unwrap(); + + assert_eq!(buf.start, 2 * QUARTER_LEN); + assert_eq!(buf.len, QUARTER_LEN); + + // Then we fill one quarter of the buffer with twos and another quarter of the buffer with + // threes in a single insertion. This tests case 1.2. + // ┌─────┬─────┬─────┬─────┐ + // │ 3 │ │ 1 │ 2 │ + // └─────┴─────┴─────┴─────┘ + // ▲ + // │ + // start + let mut second_half = vec![2; QUARTER_LEN]; + second_half.extend_from_slice(&[3; QUARTER_LEN]); + buf.insert(&mut second_half.as_slice()).unwrap(); + + expected.extend(second_half); + + assert_eq!(buf.start, 2 * QUARTER_LEN); + assert_eq!(buf.len, 3 * QUARTER_LEN); + + // When we remove all the elements of the buffer we should find them in the same order we + // inserted them. This tests case 2.2. + let mut found = vec![]; + let removed_len = buf.remove(&mut found).unwrap(); + assert_eq!(removed_len, expected.len()); + + assert_eq!(expected, found); + + assert_eq!(buf.start, QUARTER_LEN); + assert_eq!(buf.len, 0); + } + + #[test] + fn insert_into_non_empty_non_wrapping_buffer() { + const QUARTER_LEN: usize = RingBuffer::LEN / 4; + let mut buf = RingBuffer::new(); + + // We fill one quarter of the buffer with ones. This gives us a non-empty buffer whose + // empty section is contiguous. + // ┌─────┬────────────────┐ + // │ 1 │ │ + // └─────┴────────────────┘ + // ▲ + // │ + // └ start + let mut expected = vec![1; QUARTER_LEN]; + buf.insert(&mut expected.as_slice()).unwrap(); + + assert_eq!(buf.start, 0); + assert_eq!(buf.len, QUARTER_LEN); + + // Then we fill one quarter of the buffer with twos. This tests case 1.3. + // ┌─────┬─────┬──────────┐ + // │ 1 │ 2 │ │ + // └─────┴─────┴──────────┘ + // ▲ + // │ + // └ start + let second_half = vec![2; QUARTER_LEN]; + buf.insert(&mut second_half.as_slice()).unwrap(); + + expected.extend(second_half); + + assert_eq!(buf.start, 0); + assert_eq!(buf.len, 2 * QUARTER_LEN); + + // When we remove all the elements of the buffer we should find them in the same order we + // inserted them. This tests case 2.3. + let mut found = vec![]; + let removed_len = buf.remove(&mut found).unwrap(); + assert_eq!(removed_len, expected.len()); + + assert_eq!(expected, found); + + assert_eq!(buf.start, 2 * QUARTER_LEN); + assert_eq!(buf.len, 0); + } +} diff -Nru rust-sudo-rs-0.2.1/src/exec/use_pty/pipe.rs rust-sudo-rs-0.2.2/src/exec/use_pty/pipe.rs --- rust-sudo-rs-0.2.1/src/exec/use_pty/pipe.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/exec/use_pty/pipe.rs 1970-01-01 00:00:00.000000000 +0000 @@ -1,234 +0,0 @@ -use std::{ - io::{self, Read, Write}, - marker::PhantomData, - os::fd::AsRawFd, -}; - -use crate::exec::event::{EventHandle, EventRegistry, PollEvent, Process}; - -// A pipe able to stream data bidirectionally between two read-write types. -pub(super) struct Pipe { - left: L, - right: R, - buffer_lr: Buffer, - buffer_rl: Buffer, -} - -impl Pipe { - /// Create a new pipe between two read-write types and register them to be polled. - pub fn new( - left: L, - right: R, - registry: &mut EventRegistry, - f_left: fn(PollEvent) -> T::Event, - f_right: fn(PollEvent) -> T::Event, - ) -> Self { - Self { - buffer_lr: Buffer::new( - registry.register_event(&left, PollEvent::Readable, f_left), - registry.register_event(&right, PollEvent::Writable, f_right), - registry, - ), - buffer_rl: Buffer::new( - registry.register_event(&right, PollEvent::Readable, f_right), - registry.register_event(&left, PollEvent::Writable, f_left), - registry, - ), - left, - right, - } - } - - /// Get a reference to the left side of the pipe. - pub(super) fn left(&self) -> &L { - &self.left - } - - /// Get a mutable reference to the left side of the pipe. - pub(super) fn left_mut(&mut self) -> &mut L { - &mut self.left - } - - /// Get a reference to the right side of the pipe. - pub(super) fn right(&self) -> &R { - &self.right - } - - /// Stop the poll events of this pipe. - pub(super) fn ignore_events(&mut self, registry: &mut EventRegistry) { - self.buffer_lr.read_handle.ignore(registry); - self.buffer_lr.write_handle.ignore(registry); - self.buffer_rl.read_handle.ignore(registry); - self.buffer_rl.write_handle.ignore(registry); - } - - /// Resume the poll events of this pipe - pub(super) fn resume_events(&mut self, registry: &mut EventRegistry) { - self.buffer_lr.read_handle.resume(registry); - self.buffer_lr.write_handle.resume(registry); - self.buffer_rl.read_handle.resume(registry); - self.buffer_rl.write_handle.resume(registry); - } - - /// Handle a poll event for the left side of the pipe. - pub(super) fn on_left_event( - &mut self, - poll_event: PollEvent, - registry: &mut EventRegistry, - ) -> io::Result<()> { - match poll_event { - PollEvent::Readable => self.buffer_lr.read(&mut self.left, registry), - PollEvent::Writable => self.buffer_rl.write(&mut self.left, registry), - } - } - - /// Handle a poll event for the right side of the pipe. - pub(super) fn on_right_event( - &mut self, - poll_event: PollEvent, - registry: &mut EventRegistry, - ) -> io::Result<()> { - match poll_event { - PollEvent::Readable => self.buffer_rl.read(&mut self.right, registry), - PollEvent::Writable => self.buffer_lr.write(&mut self.right, registry), - } - } - - /// Ensure that all the contents of the pipe's internal buffer are written to the left side. - pub(super) fn flush_left(&mut self) -> io::Result<()> { - self.buffer_rl.flush(&mut self.left) - } -} - -/// The size of the internal buffer of the pipe. -const BUFSIZE: usize = 6 * 1024; - -/// A buffer that stores the bytes read from `R` before they are written to `W`. -struct Buffer { - buffer: [u8; BUFSIZE], - /// The start of the busy section of the buffer. - start: usize, - /// The end of the busy section of the buffer. - end: usize, - /// The handle for the event of the reader. - read_handle: EventHandle, - /// The handle for the event of the writer. - write_handle: EventHandle, - marker: PhantomData<(R, W)>, -} - -impl Buffer { - /// Create a new, empty buffer - fn new( - read_handle: EventHandle, - mut write_handle: EventHandle, - registry: &mut EventRegistry, - ) -> Self { - // The buffer is empty, don't write - write_handle.ignore(registry); - - Self { - buffer: [0; BUFSIZE], - start: 0, - end: 0, - read_handle, - write_handle, - marker: PhantomData, - } - } - - /// Return true if the buffer is empty. - fn is_empty(&self) -> bool { - self.start == self.end - } - - /// Return true if the buffer is full. - fn is_full(&self) -> bool { - // FIXME: This doesn't really mean that the buffer is full but it cannot be used for writes - // anyway. - self.end == BUFSIZE - } - - /// Read bytes into the buffer. - /// - /// Calling this function will block until `read` is ready to be read. - fn read( - &mut self, - read: &mut R, - registry: &mut EventRegistry, - ) -> io::Result<()> { - // Don't read if the buffer is full. - if self.is_full() { - self.read_handle.ignore(registry); - return Ok(()); - } - - // This is the remaining free section that follows the busy section of the buffer. - let buffer = &mut self.buffer[self.end..]; - - // Read `len` bytes from `read` into the buffer. - let len = read.read(buffer)?; - - // Mark the `len` bytes after the busy section as busy too. - self.end += len; - - // If we read something, the buffer is not empty anymore and we can resume writing. - if len > 0 { - self.write_handle.resume(registry); - } - - Ok(()) - } - - /// Write bytes from the buffer. - /// - /// Calling this function will block until `write` is ready to be written. - fn write( - &mut self, - write: &mut W, - registry: &mut EventRegistry, - ) -> io::Result<()> { - // Don't write if the buffer is empty. - if self.is_empty() { - self.write_handle.ignore(registry); - return Ok(()); - } - - // This is the busy section of the buffer. - let buffer = &self.buffer[self.start..self.end]; - - // Write the first `len` bytes of the busy section to `write`. - let len = write.write(buffer)?; - - if len == buffer.len() { - // If we were able to write all the busy section, we can mark the whole buffer as free. - self.start = 0; - self.end = 0; - } else { - // Otherwise we just free the first `len` bytes of the busy section. - self.start += len; - } - - // If we wrote something, the buffer is not full anymore and we can resume reading. - if len > 0 { - self.read_handle.resume(registry); - } - - Ok(()) - } - - /// Flush this buffer, ensuring that all the contents of its internal buffer are written. - fn flush(&mut self, write: &mut W) -> io::Result<()> { - // This is the busy section of the buffer. - let buffer = &self.buffer[self.start..self.end]; - - // Write the complete busy section to `write`. - write.write_all(buffer)?; - - // If we were able to write all the busy section, we can mark the whole buffer as free. - self.start = 0; - self.end = 0; - - write.flush() - } -} diff -Nru rust-sudo-rs-0.2.1/src/lib.rs rust-sudo-rs-0.2.2/src/lib.rs --- rust-sudo-rs-0.2.1/src/lib.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/lib.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,10 +1,8 @@ #[macro_use] mod macros; -pub(crate) mod cli; pub(crate) mod common; pub(crate) mod cutils; pub(crate) mod defaults; -pub(crate) mod env; pub(crate) mod exec; pub(crate) mod log; pub(crate) mod pam; diff -Nru rust-sudo-rs-0.2.1/src/log/syslog.rs rust-sudo-rs-0.2.2/src/log/syslog.rs --- rust-sudo-rs-0.2.1/src/log/syslog.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/log/syslog.rs 2006-07-24 01:21:28.000000000 +0000 @@ -50,20 +50,17 @@ loop { if self.cursor + message.len() > LIMIT { // floor_char_boundary is currently unstable - let mut mid = LIMIT; - while !message.is_char_boundary(mid) { - mid -= 1; + let mut truncate_boundary = LIMIT - self.cursor; + while !message.is_char_boundary(truncate_boundary) { + truncate_boundary -= 1; } - // index of last whitespace before byte cutoff - let ascii_utf8_len = 1; - mid = message[..mid] + truncate_boundary = message[..truncate_boundary] .rfind(|c: char| c.is_ascii_whitespace()) - .unwrap_or(mid) - + ascii_utf8_len; + .unwrap_or(truncate_boundary); - let left = &message[..mid]; - let right = &message[mid..]; + let left = &message[..truncate_boundary]; + let right = &message[truncate_boundary..]; self.append(left.as_bytes()); self.append(DOTDOTDOT_END); @@ -108,8 +105,10 @@ #[cfg(test)] mod tests { - use super::Syslog; use log::Log; + use std::fmt::Write; + + use super::{SysLogWriter, Syslog, FACILITY}; #[test] fn can_write_to_syslog() { @@ -123,6 +122,15 @@ } #[test] + fn can_handle_multiple_writes() { + let mut writer = SysLogWriter::new(libc::LOG_DEBUG, FACILITY); + + for i in 1..20 { + let _ = write!(writer, "{}", "Test 123 ".repeat(i)); + } + } + + #[test] fn can_truncate_syslog() { let logger = Syslog; let record = log::Record::builder() @@ -130,6 +138,17 @@ .level(log::Level::Info) .build(); + logger.log(&record); + } + + #[test] + fn can_truncate_syslog_with_no_spaces() { + let logger = Syslog; + let record = log::Record::builder() + .args(format_args!("iwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercasesiwillhandlecornercases")) + .level(log::Level::Info) + .build(); + logger.log(&record); } } diff -Nru rust-sudo-rs-0.2.1/src/macros.rs rust-sudo-rs-0.2.2/src/macros.rs --- rust-sudo-rs-0.2.1/src/macros.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/macros.rs 2006-07-24 01:21:28.000000000 +0000 @@ -45,3 +45,15 @@ compiler_error!("do not use `print!`; use the `write!` macro instead") }; } + +macro_rules! cstr { + ($lit:literal) => {{ + // this `const` item produces compile time errors = it performs the checks at compile time + const CS: &'static std::ffi::CStr = + match std::ffi::CStr::from_bytes_until_nul(concat!($lit, "\0").as_bytes()) { + Ok(x) => x, + Err(_) => panic!("string literal did not pass CStr checks"), + }; + CS + }}; +} diff -Nru rust-sudo-rs-0.2.1/src/pam/converse.rs rust-sudo-rs-0.2.2/src/pam/converse.rs --- rust-sudo-rs-0.2.1/src/pam/converse.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/pam/converse.rs 2006-07-24 01:21:28.000000000 +0000 @@ -25,7 +25,7 @@ pub fn from_int(val: libc::c_int) -> Option { use PamMessageStyle::*; - match val as libc::c_uint { + match val { PAM_PROMPT_ECHO_OFF => Some(PromptEchoOff), PAM_PROMPT_ECHO_ON => Some(PromptEchoOn), PAM_ERROR_MSG => Some(ErrorMessage), diff -Nru rust-sudo-rs-0.2.1/src/pam/error.rs rust-sudo-rs-0.2.2/src/pam/error.rs --- rust-sudo-rs-0.2.1/src/pam/error.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/pam/error.rs 2006-07-24 01:21:28.000000000 +0000 @@ -84,7 +84,7 @@ pub(super) fn from_int(errno: libc::c_int) -> PamErrorType { use PamErrorType::*; - match errno as libc::c_uint { + match errno { PAM_SUCCESS => Success, PAM_OPEN_ERR => OpenError, PAM_SYMBOL_ERR => SymbolError, diff -Nru rust-sudo-rs-0.2.1/src/pam/mod.rs rust-sudo-rs-0.2.2/src/pam/mod.rs --- rust-sudo-rs-0.2.1/src/pam/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/pam/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -17,7 +17,6 @@ mod securemem; #[allow(nonstandard_style)] -#[allow(unused)] pub mod sys; pub use converse::{CLIConverser, Converser}; @@ -141,7 +140,7 @@ /// Get the PAM flag value for the silent flag fn silent_flag(&self) -> i32 { if self.silent { - PAM_SILENT as i32 + PAM_SILENT } else { 0 } @@ -152,7 +151,7 @@ if self.allow_null_auth_token { 0 } else { - PAM_DISALLOW_NULL_AUTHTOK as i32 + PAM_DISALLOW_NULL_AUTHTOK } } @@ -197,18 +196,14 @@ pub fn set_user(&mut self, user: &str) -> PamResult<()> { let c_user = CString::new(user)?; pam_err(unsafe { - pam_set_item( - self.pamh, - PAM_USER as i32, - c_user.as_ptr() as *const libc::c_void, - ) + pam_set_item(self.pamh, PAM_USER, c_user.as_ptr() as *const libc::c_void) }) } /// Get the user that is currently active in the PAM handle pub fn get_user(&mut self) -> PamResult { let mut data = std::ptr::null(); - pam_err(unsafe { pam_get_item(self.pamh, PAM_USER as i32, &mut data) })?; + pam_err(unsafe { pam_get_item(self.pamh, PAM_USER, &mut data) })?; // safety check to make sure that we do not ready a null ptr into a cstr if data.is_null() { @@ -224,25 +219,13 @@ /// Set the TTY path for the current TTY that this PAM session started from. pub fn set_tty>(&mut self, tty_path: P) -> PamResult<()> { let data = CString::new(tty_path.as_ref().as_bytes())?; - pam_err(unsafe { - pam_set_item( - self.pamh, - PAM_TTY as i32, - data.as_ptr() as *const libc::c_void, - ) - }) + pam_err(unsafe { pam_set_item(self.pamh, PAM_TTY, data.as_ptr() as *const libc::c_void) }) } // Set the user that requested the actions in this PAM instance. pub fn set_requesting_user(&mut self, user: &str) -> PamResult<()> { let data = CString::new(user.as_bytes())?; - pam_err(unsafe { - pam_set_item( - self.pamh, - PAM_RUSER as i32, - data.as_ptr() as *const libc::c_void, - ) - }) + pam_err(unsafe { pam_set_item(self.pamh, PAM_RUSER, data.as_ptr() as *const libc::c_void) }) } /// Re-initialize the credentials stored in PAM @@ -267,7 +250,7 @@ let mut flags = 0; flags |= self.silent_flag(); if expired_only { - flags |= PAM_CHANGE_EXPIRED_AUTHTOK as i32; + flags |= PAM_CHANGE_EXPIRED_AUTHTOK; } pam_err(unsafe { pam_chauthtok(self.pamh, flags) }) } @@ -363,7 +346,7 @@ unsafe { pam_end( self.pamh, - self.last_pam_status.unwrap_or(PAM_SUCCESS as libc::c_int) | PAM_DATA_SILENT as i32, + self.last_pam_status.unwrap_or(PAM_SUCCESS as libc::c_int) | PAM_DATA_SILENT, ) }; } diff -Nru rust-sudo-rs-0.2.1/src/pam/sys.rs rust-sudo-rs-0.2.2/src/pam/sys.rs --- rust-sudo-rs-0.2.1/src/pam/sys.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/pam/sys.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,86 +1,53 @@ -/* automatically generated by rust-bindgen 0.66.1 */ +/* automatically generated by rust-bindgen 0.66.1, minified by cargo-minify, edited to be portable */ -pub const PAM_SUCCESS: u32 = 0; -pub const PAM_OPEN_ERR: u32 = 1; -pub const PAM_SYMBOL_ERR: u32 = 2; -pub const PAM_SERVICE_ERR: u32 = 3; -pub const PAM_SYSTEM_ERR: u32 = 4; -pub const PAM_BUF_ERR: u32 = 5; -pub const PAM_PERM_DENIED: u32 = 6; -pub const PAM_AUTH_ERR: u32 = 7; -pub const PAM_CRED_INSUFFICIENT: u32 = 8; -pub const PAM_AUTHINFO_UNAVAIL: u32 = 9; -pub const PAM_USER_UNKNOWN: u32 = 10; -pub const PAM_MAXTRIES: u32 = 11; -pub const PAM_NEW_AUTHTOK_REQD: u32 = 12; -pub const PAM_ACCT_EXPIRED: u32 = 13; -pub const PAM_SESSION_ERR: u32 = 14; -pub const PAM_CRED_UNAVAIL: u32 = 15; -pub const PAM_CRED_EXPIRED: u32 = 16; -pub const PAM_CRED_ERR: u32 = 17; -pub const PAM_NO_MODULE_DATA: u32 = 18; -pub const PAM_CONV_ERR: u32 = 19; -pub const PAM_AUTHTOK_ERR: u32 = 20; -pub const PAM_AUTHTOK_RECOVERY_ERR: u32 = 21; -pub const PAM_AUTHTOK_LOCK_BUSY: u32 = 22; -pub const PAM_AUTHTOK_DISABLE_AGING: u32 = 23; -pub const PAM_TRY_AGAIN: u32 = 24; -pub const PAM_IGNORE: u32 = 25; -pub const PAM_ABORT: u32 = 26; -pub const PAM_AUTHTOK_EXPIRED: u32 = 27; -pub const PAM_MODULE_UNKNOWN: u32 = 28; -pub const PAM_BAD_ITEM: u32 = 29; -pub const PAM_CONV_AGAIN: u32 = 30; -pub const PAM_INCOMPLETE: u32 = 31; -pub const PAM_SILENT: u32 = 32768; -pub const PAM_DISALLOW_NULL_AUTHTOK: u32 = 1; -pub const PAM_ESTABLISH_CRED: u32 = 2; -pub const PAM_DELETE_CRED: u32 = 4; -pub const PAM_REINITIALIZE_CRED: u32 = 8; -pub const PAM_REFRESH_CRED: u32 = 16; -pub const PAM_CHANGE_EXPIRED_AUTHTOK: u32 = 32; -pub const PAM_SERVICE: u32 = 1; -pub const PAM_USER: u32 = 2; -pub const PAM_TTY: u32 = 3; -pub const PAM_RHOST: u32 = 4; -pub const PAM_CONV: u32 = 5; -pub const PAM_AUTHTOK: u32 = 6; -pub const PAM_OLDAUTHTOK: u32 = 7; -pub const PAM_RUSER: u32 = 8; -pub const PAM_USER_PROMPT: u32 = 9; -pub const PAM_FAIL_DELAY: u32 = 10; -pub const PAM_XDISPLAY: u32 = 11; -pub const PAM_XAUTHDATA: u32 = 12; -pub const PAM_AUTHTOK_TYPE: u32 = 13; -pub const PAM_DATA_SILENT: u32 = 1073741824; -pub const PAM_PROMPT_ECHO_OFF: u32 = 1; -pub const PAM_PROMPT_ECHO_ON: u32 = 2; -pub const PAM_ERROR_MSG: u32 = 3; -pub const PAM_TEXT_INFO: u32 = 4; -pub const PAM_RADIO_TYPE: u32 = 5; -pub const PAM_BINARY_PROMPT: u32 = 7; -pub const PAM_MAX_NUM_MSG: u32 = 32; -pub const PAM_MAX_MSG_SIZE: u32 = 512; -pub const PAM_MAX_RESP_SIZE: u32 = 512; -pub const PAM_AUTHTOK_RECOVER_ERR: u32 = 21; -pub const PAM_BP_MAX_LENGTH: u32 = 131072; -pub const PAM_BPC_FALSE: u32 = 0; -pub const PAM_BPC_TRUE: u32 = 1; -pub const PAM_BPC_OK: u32 = 1; -pub const PAM_BPC_SELECT: u32 = 2; -pub const PAM_BPC_DONE: u32 = 3; -pub const PAM_BPC_FAIL: u32 = 4; -pub const PAM_BPC_GETENV: u32 = 65; -pub const PAM_BPC_PUTENV: u32 = 66; -pub const PAM_BPC_TEXT: u32 = 67; -pub const PAM_BPC_ERROR: u32 = 68; -pub const PAM_BPC_PROMPT: u32 = 69; -pub const PAM_BPC_PASS: u32 = 70; -pub const PAM_PRELIM_CHECK: u32 = 16384; -pub const PAM_UPDATE_AUTHTOK: u32 = 8192; -pub const PAM_DATA_REPLACE: u32 = 536870912; -pub const PAM_MODUTIL_NGROUPS: u32 = 64; -pub type pam_handle_t = u8; +// NOTE: the tests below test the assumptions about the padding that a C compiler will use on the +// above structs; if these assumptions are incorrect, the tests will fail, but most likely the +// code will still be correct. + +pub const PAM_SUCCESS: libc::c_int = 0; +pub const PAM_OPEN_ERR: libc::c_int = 1; +pub const PAM_SYMBOL_ERR: libc::c_int = 2; +pub const PAM_SERVICE_ERR: libc::c_int = 3; +pub const PAM_SYSTEM_ERR: libc::c_int = 4; +pub const PAM_BUF_ERR: libc::c_int = 5; +pub const PAM_PERM_DENIED: libc::c_int = 6; +pub const PAM_AUTH_ERR: libc::c_int = 7; +pub const PAM_CRED_INSUFFICIENT: libc::c_int = 8; +pub const PAM_AUTHINFO_UNAVAIL: libc::c_int = 9; +pub const PAM_USER_UNKNOWN: libc::c_int = 10; +pub const PAM_MAXTRIES: libc::c_int = 11; +pub const PAM_NEW_AUTHTOK_REQD: libc::c_int = 12; +pub const PAM_ACCT_EXPIRED: libc::c_int = 13; +pub const PAM_SESSION_ERR: libc::c_int = 14; +pub const PAM_CRED_UNAVAIL: libc::c_int = 15; +pub const PAM_CRED_EXPIRED: libc::c_int = 16; +pub const PAM_CRED_ERR: libc::c_int = 17; +pub const PAM_NO_MODULE_DATA: libc::c_int = 18; +pub const PAM_CONV_ERR: libc::c_int = 19; +pub const PAM_AUTHTOK_ERR: libc::c_int = 20; +pub const PAM_AUTHTOK_RECOVERY_ERR: libc::c_int = 21; +pub const PAM_AUTHTOK_LOCK_BUSY: libc::c_int = 22; +pub const PAM_AUTHTOK_DISABLE_AGING: libc::c_int = 23; +pub const PAM_TRY_AGAIN: libc::c_int = 24; +pub const PAM_IGNORE: libc::c_int = 25; +pub const PAM_ABORT: libc::c_int = 26; +pub const PAM_AUTHTOK_EXPIRED: libc::c_int = 27; +pub const PAM_MODULE_UNKNOWN: libc::c_int = 28; +pub const PAM_BAD_ITEM: libc::c_int = 29; +pub const PAM_SILENT: libc::c_int = 32768; +pub const PAM_DISALLOW_NULL_AUTHTOK: libc::c_int = 1; +pub const PAM_REINITIALIZE_CRED: libc::c_int = 8; +pub const PAM_CHANGE_EXPIRED_AUTHTOK: libc::c_int = 32; +pub const PAM_USER: libc::c_int = 2; +pub const PAM_TTY: libc::c_int = 3; +pub const PAM_RUSER: libc::c_int = 8; +pub const PAM_DATA_SILENT: libc::c_int = 1073741824; +pub const PAM_PROMPT_ECHO_OFF: libc::c_int = 1; +pub const PAM_PROMPT_ECHO_ON: libc::c_int = 2; +pub const PAM_ERROR_MSG: libc::c_int = 3; +pub const PAM_TEXT_INFO: libc::c_int = 4; +pub const PAM_MAX_RESP_SIZE: libc::c_int = 512; +pub type pam_handle_t = libc::c_void; extern "C" { pub fn pam_set_item( pamh: *mut pam_handle_t, @@ -99,17 +66,8 @@ pub fn pam_strerror(pamh: *mut pam_handle_t, errnum: libc::c_int) -> *const libc::c_char; } extern "C" { - pub fn pam_putenv(pamh: *mut pam_handle_t, name_value: *const libc::c_char) -> libc::c_int; -} -extern "C" { - pub fn pam_getenv(pamh: *mut pam_handle_t, name: *const libc::c_char) -> *const libc::c_char; -} -extern "C" { pub fn pam_getenvlist(pamh: *mut pam_handle_t) -> *mut *mut libc::c_char; } -extern "C" { - pub fn pam_fail_delay(pamh: *mut pam_handle_t, musec_delay: libc::c_uint) -> libc::c_int; -} #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct pam_message { @@ -121,18 +79,14 @@ const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( - ::std::mem::size_of::(), - 16usize, - concat!("Size of: ", stringify!(pam_message)) - ); - assert_eq!( ::std::mem::align_of::(), - 8usize, + ::std::mem::align_of::<*mut libc::c_void>(), concat!("Alignment of ", stringify!(pam_message)) ); + let mut offset: usize = 0; assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).msg_style) as usize - ptr as usize }, - 0usize, + offset, concat!( "Offset of field: ", stringify!(pam_message), @@ -140,9 +94,10 @@ stringify!(msg_style) ) ); + offset = aligned_offset::<*const libc::c_char>(offset + ::std::mem::size_of::()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).msg) as usize - ptr as usize }, - 8usize, + offset, concat!( "Offset of field: ", stringify!(pam_message), @@ -150,6 +105,14 @@ stringify!(msg) ) ); + offset = aligned_offset::<*const libc::c_void>( + offset + ::std::mem::size_of::<*const libc::c_char>(), + ); + assert_eq!( + ::std::mem::size_of::(), + offset, + concat!("Size of: ", stringify!(pam_message)) + ); } #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -162,18 +125,14 @@ const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( - ::std::mem::size_of::(), - 16usize, - concat!("Size of: ", stringify!(pam_response)) - ); - assert_eq!( ::std::mem::align_of::(), - 8usize, + ::std::mem::align_of::<*mut libc::c_char>(), concat!("Alignment of ", stringify!(pam_response)) ); + let mut offset: usize = 0; assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).resp) as usize - ptr as usize }, - 0usize, + offset, concat!( "Offset of field: ", stringify!(pam_response), @@ -181,9 +140,10 @@ stringify!(resp) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::<*mut libc::c_char>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).resp_retcode) as usize - ptr as usize }, - 8usize, + offset, concat!( "Offset of field: ", stringify!(pam_response), @@ -191,6 +151,12 @@ stringify!(resp_retcode) ) ); + offset = aligned_offset::<*mut libc::c_void>(offset + ::std::mem::size_of::()); + assert_eq!( + ::std::mem::size_of::(), + offset, + concat!("Size of: ", stringify!(pam_response)) + ); } #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -210,18 +176,14 @@ const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( - ::std::mem::size_of::(), - 16usize, - concat!("Size of: ", stringify!(pam_conv)) - ); - assert_eq!( ::std::mem::align_of::(), - 8usize, + ::std::mem::align_of::<*mut libc::c_void>(), concat!("Alignment of ", stringify!(pam_conv)) ); + let mut offset: usize = 0; assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).conv) as usize - ptr as usize }, - 0usize, + offset, concat!( "Offset of field: ", stringify!(pam_conv), @@ -229,9 +191,11 @@ stringify!(conv) ) ); + offset = + aligned_offset::<*mut libc::c_void>(offset + ::std::mem::size_of::<*mut libc::c_void>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).appdata_ptr) as usize - ptr as usize }, - 8usize, + offset, concat!( "Offset of field: ", stringify!(pam_conv), @@ -239,6 +203,13 @@ stringify!(appdata_ptr) ) ); + offset = + aligned_offset::<*mut libc::c_void>(offset + ::std::mem::size_of::<*mut libc::c_void>()); + assert_eq!( + ::std::mem::size_of::(), + offset, + concat!("Size of: ", stringify!(pam_conv)) + ); } extern "C" { pub fn pam_start( @@ -249,15 +220,6 @@ ) -> libc::c_int; } extern "C" { - pub fn pam_start_confdir( - service_name: *const libc::c_char, - user: *const libc::c_char, - pam_conversation: *const pam_conv, - confdir: *const libc::c_char, - pamh: *mut *mut pam_handle_t, - ) -> libc::c_int; -} -extern "C" { pub fn pam_end(pamh: *mut pam_handle_t, pam_status: libc::c_int) -> libc::c_int; } extern "C" { @@ -282,156 +244,6 @@ pub type __gid_t = libc::c_uint; pub type gid_t = __gid_t; pub type uid_t = __uid_t; -pub type va_list = __builtin_va_list; -extern "C" { - pub fn pam_vsyslog( - pamh: *const pam_handle_t, - priority: libc::c_int, - fmt: *const libc::c_char, - args: *mut __va_list_tag, - ); -} -extern "C" { - pub fn pam_syslog( - pamh: *const pam_handle_t, - priority: libc::c_int, - fmt: *const libc::c_char, - ... - ); -} -extern "C" { - pub fn pam_vprompt( - pamh: *mut pam_handle_t, - style: libc::c_int, - response: *mut *mut libc::c_char, - fmt: *const libc::c_char, - args: *mut __va_list_tag, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_prompt( - pamh: *mut pam_handle_t, - style: libc::c_int, - response: *mut *mut libc::c_char, - fmt: *const libc::c_char, - ... - ) -> libc::c_int; -} -extern "C" { - pub fn pam_get_authtok( - pamh: *mut pam_handle_t, - item: libc::c_int, - authtok: *mut *const libc::c_char, - prompt: *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_get_authtok_noverify( - pamh: *mut pam_handle_t, - authtok: *mut *const libc::c_char, - prompt: *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_get_authtok_verify( - pamh: *mut pam_handle_t, - authtok: *mut *const libc::c_char, - prompt: *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_misc_paste_env( - pamh: *mut pam_handle_t, - user_env: *const *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_misc_drop_env(env: *mut *mut libc::c_char) -> *mut *mut libc::c_char; -} -extern "C" { - pub fn pam_misc_setenv( - pamh: *mut pam_handle_t, - name: *const libc::c_char, - value: *const libc::c_char, - readonly: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_set_data( - pamh: *mut pam_handle_t, - module_data_name: *const libc::c_char, - data: *mut libc::c_void, - cleanup: ::std::option::Option< - unsafe extern "C" fn( - pamh: *mut pam_handle_t, - data: *mut libc::c_void, - error_status: libc::c_int, - ), - >, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_get_data( - pamh: *const pam_handle_t, - module_data_name: *const libc::c_char, - data: *mut *const libc::c_void, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_get_user( - pamh: *mut pam_handle_t, - user: *mut *const libc::c_char, - prompt: *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_sm_authenticate( - pamh: *mut pam_handle_t, - flags: libc::c_int, - argc: libc::c_int, - argv: *mut *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_sm_setcred( - pamh: *mut pam_handle_t, - flags: libc::c_int, - argc: libc::c_int, - argv: *mut *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_sm_acct_mgmt( - pamh: *mut pam_handle_t, - flags: libc::c_int, - argc: libc::c_int, - argv: *mut *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_sm_open_session( - pamh: *mut pam_handle_t, - flags: libc::c_int, - argc: libc::c_int, - argv: *mut *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_sm_close_session( - pamh: *mut pam_handle_t, - flags: libc::c_int, - argc: libc::c_int, - argv: *mut *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_sm_chauthtok( - pamh: *mut pam_handle_t, - flags: libc::c_int, - argc: libc::c_int, - argv: *mut *const libc::c_char, - ) -> libc::c_int; -} #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct passwd { @@ -448,18 +260,14 @@ const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( - ::std::mem::size_of::(), - 48usize, - concat!("Size of: ", stringify!(passwd)) - ); - assert_eq!( ::std::mem::align_of::(), - 8usize, + ::std::mem::align_of::<*mut libc::c_char>(), concat!("Alignment of ", stringify!(passwd)) ); + let mut offset: usize = 0; assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).pw_name) as usize - ptr as usize }, - 0usize, + offset, concat!( "Offset of field: ", stringify!(passwd), @@ -467,9 +275,11 @@ stringify!(pw_name) ) ); + offset = + aligned_offset::<*mut libc::c_char>(offset + ::std::mem::size_of::<*mut libc::c_char>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).pw_passwd) as usize - ptr as usize }, - 8usize, + offset, concat!( "Offset of field: ", stringify!(passwd), @@ -477,9 +287,10 @@ stringify!(pw_passwd) ) ); + offset = aligned_offset::<__uid_t>(offset + ::std::mem::size_of::<*mut libc::c_char>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).pw_uid) as usize - ptr as usize }, - 16usize, + offset, concat!( "Offset of field: ", stringify!(passwd), @@ -487,9 +298,10 @@ stringify!(pw_uid) ) ); + offset = aligned_offset::<__gid_t>(offset + ::std::mem::size_of::<__uid_t>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).pw_gid) as usize - ptr as usize }, - 20usize, + offset, concat!( "Offset of field: ", stringify!(passwd), @@ -497,9 +309,10 @@ stringify!(pw_gid) ) ); + offset = aligned_offset::<*mut libc::c_char>(offset + ::std::mem::size_of::<__gid_t>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).pw_gecos) as usize - ptr as usize }, - 24usize, + offset, concat!( "Offset of field: ", stringify!(passwd), @@ -507,9 +320,11 @@ stringify!(pw_gecos) ) ); + offset = + aligned_offset::<*mut libc::c_char>(offset + ::std::mem::size_of::<*mut libc::c_char>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).pw_dir) as usize - ptr as usize }, - 32usize, + offset, concat!( "Offset of field: ", stringify!(passwd), @@ -517,9 +332,11 @@ stringify!(pw_dir) ) ); + offset = + aligned_offset::<*mut libc::c_char>(offset + ::std::mem::size_of::<*mut libc::c_char>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).pw_shell) as usize - ptr as usize }, - 40usize, + offset, concat!( "Offset of field: ", stringify!(passwd), @@ -527,6 +344,13 @@ stringify!(pw_shell) ) ); + offset = + aligned_offset::<*mut libc::c_void>(offset + ::std::mem::size_of::<*mut libc::c_char>()); + assert_eq!( + ::std::mem::size_of::(), + offset, + concat!("Size of: ", stringify!(passwd)) + ); } #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -541,18 +365,14 @@ const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( - ::std::mem::size_of::(), - 32usize, - concat!("Size of: ", stringify!(group)) - ); - assert_eq!( ::std::mem::align_of::(), - 8usize, + ::std::mem::align_of::<*mut libc::c_char>(), concat!("Alignment of ", stringify!(group)) ); + let mut offset: usize = 0; assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).gr_name) as usize - ptr as usize }, - 0usize, + offset, concat!( "Offset of field: ", stringify!(group), @@ -560,9 +380,11 @@ stringify!(gr_name) ) ); + offset = + aligned_offset::<*mut libc::c_char>(offset + ::std::mem::size_of::<*mut libc::c_char>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).gr_passwd) as usize - ptr as usize }, - 8usize, + offset, concat!( "Offset of field: ", stringify!(group), @@ -570,9 +392,10 @@ stringify!(gr_passwd) ) ); + offset = aligned_offset::<__gid_t>(offset + ::std::mem::size_of::<*mut libc::c_char>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).gr_gid) as usize - ptr as usize }, - 16usize, + offset, concat!( "Offset of field: ", stringify!(group), @@ -580,9 +403,10 @@ stringify!(gr_gid) ) ); + offset = aligned_offset::<*mut libc::c_char>(offset + ::std::mem::size_of::<__gid_t>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).gr_mem) as usize - ptr as usize }, - 24usize, + offset, concat!( "Offset of field: ", stringify!(group), @@ -590,6 +414,13 @@ stringify!(gr_mem) ) ); + offset = + aligned_offset::<*mut libc::c_void>(offset + ::std::mem::size_of::<*mut libc::c_char>()); + assert_eq!( + ::std::mem::size_of::(), + offset, + concat!("Size of: ", stringify!(group)) + ); } #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -609,18 +440,14 @@ const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( - ::std::mem::size_of::(), - 72usize, - concat!("Size of: ", stringify!(spwd)) - ); - assert_eq!( ::std::mem::align_of::(), - 8usize, + ::std::mem::align_of::<*mut libc::c_char>(), concat!("Alignment of ", stringify!(spwd)) ); + let mut offset: usize = 0; assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).sp_namp) as usize - ptr as usize }, - 0usize, + offset, concat!( "Offset of field: ", stringify!(spwd), @@ -628,9 +455,11 @@ stringify!(sp_namp) ) ); + offset = + aligned_offset::<*mut libc::c_char>(offset + ::std::mem::size_of::<*mut libc::c_char>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).sp_pwdp) as usize - ptr as usize }, - 8usize, + offset, concat!( "Offset of field: ", stringify!(spwd), @@ -638,9 +467,10 @@ stringify!(sp_pwdp) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::<*mut libc::c_char>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).sp_lstchg) as usize - ptr as usize }, - 16usize, + offset, concat!( "Offset of field: ", stringify!(spwd), @@ -648,9 +478,10 @@ stringify!(sp_lstchg) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).sp_min) as usize - ptr as usize }, - 24usize, + offset, concat!( "Offset of field: ", stringify!(spwd), @@ -658,9 +489,10 @@ stringify!(sp_min) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).sp_max) as usize - ptr as usize }, - 32usize, + offset, concat!( "Offset of field: ", stringify!(spwd), @@ -668,9 +500,10 @@ stringify!(sp_max) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).sp_warn) as usize - ptr as usize }, - 40usize, + offset, concat!( "Offset of field: ", stringify!(spwd), @@ -678,9 +511,10 @@ stringify!(sp_warn) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).sp_inact) as usize - ptr as usize }, - 48usize, + offset, concat!( "Offset of field: ", stringify!(spwd), @@ -688,9 +522,10 @@ stringify!(sp_inact) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).sp_expire) as usize - ptr as usize }, - 56usize, + offset, concat!( "Offset of field: ", stringify!(spwd), @@ -698,9 +533,10 @@ stringify!(sp_expire) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).sp_flag) as usize - ptr as usize }, - 64usize, + offset, concat!( "Offset of field: ", stringify!(spwd), @@ -708,74 +544,12 @@ stringify!(sp_flag) ) ); -} -extern "C" { - pub fn pam_modutil_getpwnam(pamh: *mut pam_handle_t, user: *const libc::c_char) -> *mut passwd; -} -extern "C" { - pub fn pam_modutil_getpwuid(pamh: *mut pam_handle_t, uid: uid_t) -> *mut passwd; -} -extern "C" { - pub fn pam_modutil_getgrnam(pamh: *mut pam_handle_t, group: *const libc::c_char) -> *mut group; -} -extern "C" { - pub fn pam_modutil_getgrgid(pamh: *mut pam_handle_t, gid: gid_t) -> *mut group; -} -extern "C" { - pub fn pam_modutil_getspnam(pamh: *mut pam_handle_t, user: *const libc::c_char) -> *mut spwd; -} -extern "C" { - pub fn pam_modutil_user_in_group_nam_nam( - pamh: *mut pam_handle_t, - user: *const libc::c_char, - group: *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_modutil_user_in_group_nam_gid( - pamh: *mut pam_handle_t, - user: *const libc::c_char, - group: gid_t, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_modutil_user_in_group_uid_nam( - pamh: *mut pam_handle_t, - user: uid_t, - group: *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_modutil_user_in_group_uid_gid( - pamh: *mut pam_handle_t, - user: uid_t, - group: gid_t, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_modutil_getlogin(pamh: *mut pam_handle_t) -> *const libc::c_char; -} -extern "C" { - pub fn pam_modutil_read( - fd: libc::c_int, - buffer: *mut libc::c_char, - count: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_modutil_write( - fd: libc::c_int, - buffer: *const libc::c_char, - count: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_modutil_audit_write( - pamh: *mut pam_handle_t, - type_: libc::c_int, - message: *const libc::c_char, - retval: libc::c_int, - ) -> libc::c_int; + offset = aligned_offset::<*mut libc::c_void>(offset + ::std::mem::size_of::()); + assert_eq!( + ::std::mem::size_of::(), + offset, + concat!("Size of: ", stringify!(spwd)) + ); } #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -792,18 +566,14 @@ const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( - ::std::mem::size_of::(), - 32usize, - concat!("Size of: ", stringify!(pam_modutil_privs)) - ); - assert_eq!( ::std::mem::align_of::(), - 8usize, + ::std::mem::align_of::<*mut gid_t>(), concat!("Alignment of ", stringify!(pam_modutil_privs)) ); + let mut offset: usize = 0; assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).grplist) as usize - ptr as usize }, - 0usize, + offset, concat!( "Offset of field: ", stringify!(pam_modutil_privs), @@ -811,9 +581,10 @@ stringify!(grplist) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::<*mut gid_t>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).number_of_groups) as usize - ptr as usize }, - 8usize, + offset, concat!( "Offset of field: ", stringify!(pam_modutil_privs), @@ -821,9 +592,10 @@ stringify!(number_of_groups) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).allocated) as usize - ptr as usize }, - 12usize, + offset, concat!( "Offset of field: ", stringify!(pam_modutil_privs), @@ -831,9 +603,10 @@ stringify!(allocated) ) ); + offset = aligned_offset::<__gid_t>(offset + ::std::mem::size_of::()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).old_gid) as usize - ptr as usize }, - 16usize, + offset, concat!( "Offset of field: ", stringify!(pam_modutil_privs), @@ -841,9 +614,10 @@ stringify!(old_gid) ) ); + offset = aligned_offset::<__uid_t>(offset + ::std::mem::size_of::<__gid_t>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).old_uid) as usize - ptr as usize }, - 20usize, + offset, concat!( "Offset of field: ", stringify!(pam_modutil_privs), @@ -851,9 +625,10 @@ stringify!(old_uid) ) ); + offset = aligned_offset::(offset + ::std::mem::size_of::<__uid_t>()); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).is_dropped) as usize - ptr as usize }, - 24usize, + offset, concat!( "Offset of field: ", stringify!(pam_modutil_privs), @@ -861,100 +636,17 @@ stringify!(is_dropped) ) ); -} -extern "C" { - pub fn pam_modutil_drop_priv( - pamh: *mut pam_handle_t, - p: *mut pam_modutil_privs, - pw: *const passwd, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_modutil_regain_priv( - pamh: *mut pam_handle_t, - p: *mut pam_modutil_privs, - ) -> libc::c_int; -} -pub const pam_modutil_redirect_fd_PAM_MODUTIL_IGNORE_FD: pam_modutil_redirect_fd = 0; -pub const pam_modutil_redirect_fd_PAM_MODUTIL_PIPE_FD: pam_modutil_redirect_fd = 1; -pub const pam_modutil_redirect_fd_PAM_MODUTIL_NULL_FD: pam_modutil_redirect_fd = 2; -pub type pam_modutil_redirect_fd = libc::c_uint; -extern "C" { - pub fn pam_modutil_sanitize_helper_fds( - pamh: *mut pam_handle_t, - redirect_stdin: pam_modutil_redirect_fd, - redirect_stdout: pam_modutil_redirect_fd, - redirect_stderr: pam_modutil_redirect_fd, - ) -> libc::c_int; -} -extern "C" { - pub fn pam_modutil_search_key( - pamh: *mut pam_handle_t, - file_name: *const libc::c_char, - key: *const libc::c_char, - ) -> *mut libc::c_char; -} -pub type __builtin_va_list = [__va_list_tag; 1usize]; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct __va_list_tag { - pub gp_offset: libc::c_uint, - pub fp_offset: libc::c_uint, - pub overflow_arg_area: *mut libc::c_void, - pub reg_save_area: *mut libc::c_void, -} -#[test] -fn bindgen_test_layout___va_list_tag() { - const UNINIT: ::std::mem::MaybeUninit<__va_list_tag> = ::std::mem::MaybeUninit::uninit(); - let ptr = UNINIT.as_ptr(); - assert_eq!( - ::std::mem::size_of::<__va_list_tag>(), - 24usize, - concat!("Size of: ", stringify!(__va_list_tag)) - ); - assert_eq!( - ::std::mem::align_of::<__va_list_tag>(), - 8usize, - concat!("Alignment of ", stringify!(__va_list_tag)) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).gp_offset) as usize - ptr as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(__va_list_tag), - "::", - stringify!(gp_offset) - ) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).fp_offset) as usize - ptr as usize }, - 4usize, - concat!( - "Offset of field: ", - stringify!(__va_list_tag), - "::", - stringify!(fp_offset) - ) - ); + offset = aligned_offset::<*mut libc::c_void>(offset + ::std::mem::size_of::()); assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).overflow_arg_area) as usize - ptr as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(__va_list_tag), - "::", - stringify!(overflow_arg_area) - ) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).reg_save_area) as usize - ptr as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(__va_list_tag), - "::", - stringify!(reg_save_area) - ) + ::std::mem::size_of::(), + offset, + concat!("Size of: ", stringify!(pam_modutil_privs)) ); } + +#[cfg(test)] +fn aligned_offset(offset: usize) -> usize { + let offset = offset as isize; + let alignment = ::std::mem::align_of::() as isize; + (offset + (-offset).rem_euclid(alignment)) as usize +} diff -Nru rust-sudo-rs-0.2.1/src/su/cli.rs rust-sudo-rs-0.2.2/src/su/cli.rs --- rust-sudo-rs-0.2.1/src/su/cli.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/su/cli.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,44 +1,247 @@ -use std::path::PathBuf; +use std::{borrow::Cow, mem, path::PathBuf}; -#[derive(Debug, PartialEq)] -pub struct SuOptions { - pub user: String, +use crate::common::SudoString; + +use super::DEFAULT_USER; + +#[cfg_attr(test, derive(Debug, PartialEq))] +pub enum SuAction { + Help(SuHelpOptions), + Version(SuVersionOptions), + Run(SuRunOptions), +} + +impl SuAction { + pub fn from_env() -> Result { + SuOptions::parse_arguments(std::env::args())?.validate() + } + + #[cfg(test)] + pub fn parse_arguments(args: impl IntoIterator) -> Result { + SuOptions::parse_arguments(args)?.validate() + } + + #[cfg(test)] + pub fn try_into_run(self) -> Result { + if let Self::Run(v) = self { + Ok(v) + } else { + Err(self) + } + } +} + +#[cfg_attr(test, derive(Debug, PartialEq))] +pub struct SuHelpOptions {} + +impl TryFrom for SuHelpOptions { + type Error = String; + + fn try_from(mut opts: SuOptions) -> Result { + let help = mem::take(&mut opts.help); + debug_assert!(help); + reject_all("--help", opts)?; + Ok(Self {}) + } +} + +#[cfg_attr(test, derive(Debug, PartialEq))] +pub struct SuVersionOptions {} + +impl TryFrom for SuVersionOptions { + type Error = String; + + fn try_from(mut opts: SuOptions) -> Result { + let version = mem::take(&mut opts.version); + debug_assert!(version); + reject_all("--version", opts)?; + Ok(Self {}) + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct SuRunOptions { + // -c pub command: Option, - pub group: Vec, - pub supp_group: Vec, + // -g + pub group: Vec, + // -l pub login: bool, + // -p pub preserve_environment: bool, + // -s pub shell: Option, + // -G + pub supp_group: Vec, + // -w pub whitelist_environment: Vec, + + pub user: SudoString, pub arguments: Vec, - pub action: SuAction, } -impl Default for SuOptions { +#[cfg(test)] +impl Default for SuRunOptions { fn default() -> Self { Self { - user: "root".to_owned(), command: None, group: vec![], - supp_group: vec![], login: false, preserve_environment: false, shell: None, + supp_group: vec![], whitelist_environment: vec![], + user: DEFAULT_USER.into(), arguments: vec![], - action: SuAction::Run, } } } -#[derive(Debug, PartialEq)] -pub enum SuAction { - Help, - Version, - Run, +impl TryFrom for SuRunOptions { + type Error = String; + + fn try_from(mut opts: SuOptions) -> Result { + let command = mem::take(&mut opts.command); + let group = mem::take(&mut opts.group); + let login = mem::take(&mut opts.login); + let preserve_environment = mem::take(&mut opts.preserve_environment); + // always `true`; cannot be disabled via the CLI + let _pty = mem::take(&mut opts.pty); + let shell = mem::take(&mut opts.shell); + let supp_group = mem::take(&mut opts.supp_group); + let whitelist_environment = mem::take(&mut opts.whitelist_environment); + let mut positional_args = mem::take(&mut opts.positional_args); + + reject_all("run mode", opts)?; + + let user = if positional_args.is_empty() { + DEFAULT_USER.to_string() + } else { + positional_args.remove(0) + }; + let arguments = positional_args; + + Ok(Self { + command, + group, + login, + preserve_environment, + shell, + supp_group, + whitelist_environment, + user: SudoString::try_from(user).map_err(|err| err.to_string())?, + arguments, + }) + } +} + +fn reject_all(context: &str, opts: SuOptions) -> Result<(), String> { + macro_rules! tuple { + ($expr:expr) => { + (&$expr as &dyn IsAbsent, { + let name = concat!("--", stringify!($expr)); + if name.contains('_') { + Cow::Owned(name.replace('_', "-")) + } else { + Cow::Borrowed(name) + } + }) + }; + } + + let SuOptions { + command, + group, + help, + login, + preserve_environment, + pty, + shell, + supp_group, + version, + whitelist_environment, + positional_args, + } = opts; + + let flags = [ + tuple!(command), + tuple!(group), + tuple!(help), + tuple!(login), + tuple!(preserve_environment), + tuple!(pty), + tuple!(shell), + tuple!(supp_group), + tuple!(version), + tuple!(whitelist_environment), + ]; + for (value, name) in flags { + ensure_is_absent(context, value, &name)?; + } + + ensure_is_absent(context, &positional_args, "positional argument")?; + + Ok(()) +} + +fn ensure_is_absent(context: &str, thing: &dyn IsAbsent, name: &str) -> Result<(), String> { + if thing.is_absent() { + Ok(()) + } else { + Err(format!("{context} conflicts with {name}")) + } +} + +trait IsAbsent { + fn is_absent(&self) -> bool; +} + +impl IsAbsent for bool { + fn is_absent(&self) -> bool { + !*self + } +} + +impl IsAbsent for Option { + fn is_absent(&self) -> bool { + self.is_none() + } +} + +impl IsAbsent for Vec { + fn is_absent(&self) -> bool { + self.is_empty() + } } -type OptionSetter = &'static dyn Fn(&mut SuOptions, Option) -> Result<(), String>; +#[derive(Debug, Default, PartialEq)] +struct SuOptions { + // -c + command: Option, + // -g + group: Vec, + // -h + help: bool, + // -l + login: bool, + // -p + preserve_environment: bool, + // -P + pty: bool, + // -s + shell: Option, + // -G + supp_group: Vec, + // -V + version: bool, + // -w + whitelist_environment: Vec, + + positional_args: Vec, +} + +type OptionSetter = fn(&mut SuOptions, Option) -> Result<(), String>; struct SuOption { short: char, @@ -53,149 +256,175 @@ short: 'c', long: "command", takes_argument: true, - set: &|sudo_options, argument| { + set: |sudo_options, argument| { if argument.is_some() { sudo_options.command = argument; + Ok(()) } else { - Err("no command provided")? + Err("no command provided".into()) } - - Ok(()) }, }, SuOption { short: 'g', long: "group", takes_argument: true, - set: &|sudo_options, argument| { + set: |sudo_options, argument| { if let Some(value) = argument { - sudo_options.group.push(value); + sudo_options.group.push(SudoString::from_cli_string(value)); + Ok(()) } else { - Err("no group provided")? + Err("no group provided".into()) } - - Ok(()) }, }, SuOption { short: 'G', long: "supp-group", takes_argument: true, - set: &|sudo_options, argument| { + set: |sudo_options, argument| { if let Some(value) = argument { - sudo_options.supp_group.push(value); + sudo_options + .supp_group + .push(SudoString::from_cli_string(value)); + Ok(()) } else { - Err("no supplementary group provided")? + Err("no supplementary group provided".into()) } - - Ok(()) }, }, SuOption { short: 'l', long: "login", takes_argument: false, - set: &|sudo_options, _| { - sudo_options.login = true; - Ok(()) + set: |sudo_options, _| { + if sudo_options.login { + Err(more_than_once("--login")) + } else { + sudo_options.login = true; + Ok(()) + } }, }, SuOption { short: 'p', long: "preserve-environment", takes_argument: false, - set: &|sudo_options, _| { - sudo_options.preserve_environment = true; - Ok(()) + set: |sudo_options, _| { + if sudo_options.preserve_environment { + Err(more_than_once("--preserve-environment")) + } else { + sudo_options.preserve_environment = true; + Ok(()) + } }, }, SuOption { short: 'm', long: "preserve-environment", takes_argument: false, - set: &|sudo_options, _| { - sudo_options.preserve_environment = true; - Ok(()) + set: |sudo_options, _| { + if sudo_options.preserve_environment { + Err(more_than_once("--preserve-environment")) + } else { + sudo_options.preserve_environment = true; + Ok(()) + } }, }, SuOption { short: 'P', long: "pty", takes_argument: false, - set: &|_sudo_options, _| Ok(()), + set: |sudo_options, _| { + if sudo_options.pty { + Err(more_than_once("--pty")) + } else { + sudo_options.pty = true; + Ok(()) + } + }, }, SuOption { short: 's', long: "shell", takes_argument: true, - set: &|sudo_options, argument| { + set: |sudo_options, argument| { if let Some(path) = argument { sudo_options.shell = Some(PathBuf::from(path)); + Ok(()) } else { - Err("no shell provided")? + Err("no shell provided".into()) } - - Ok(()) }, }, SuOption { short: 'w', long: "whitelist-environment", takes_argument: true, - set: &|sudo_options, argument| { + set: |sudo_options, argument| { if let Some(list) = argument { let values: Vec = list.split(',').map(str::to_string).collect(); sudo_options.whitelist_environment.extend(values); + Ok(()) } else { - Err("no enivronment whitelist provided")? + Err("no environment whitelist provided".into()) } - - Ok(()) }, }, SuOption { short: 'V', long: "version", takes_argument: false, - set: &|sudo_options, _| { - sudo_options.action = SuAction::Version; - Ok(()) + set: |sudo_options, _| { + if sudo_options.version { + Err(more_than_once("--version")) + } else { + sudo_options.version = true; + Ok(()) + } }, }, SuOption { short: 'h', long: "help", takes_argument: false, - set: &|sudo_options, _| { - sudo_options.action = SuAction::Help; - Ok(()) + set: |sudo_options, _| { + if sudo_options.help { + Err(more_than_once("--help")) + } else { + sudo_options.help = true; + Ok(()) + } }, }, ]; - pub fn from_env() -> Result { - let args = std::env::args().collect(); - - Self::parse_arguments(args) - } - /// parse su arguments into SuOptions struct - pub(crate) fn parse_arguments(arguments: Vec) -> Result { + fn parse_arguments(arguments: impl IntoIterator) -> Result { let mut options: SuOptions = SuOptions::default(); let mut arg_iter = arguments.into_iter().skip(1); while let Some(arg) = arg_iter.next() { // - or -l or --login indicates a login shell should be started if arg == "-" { - options.login = true; - // if the argument starts with -- it must be a full length option name - } else if arg.starts_with("--") { + if options.login { + return Err(more_than_once("--login")); + } else { + options.login = true; + } + } else if arg == "--" { + // only positional arguments after this point + options.positional_args.extend(arg_iter); + + break; + + // if the argument starts with -- it must be a full length option name + } else if let Some(unprefixed) = arg.strip_prefix("--") { // parse assignments like '--group=ferris' - if arg.contains('=') { - // convert assignment to normal tokens - let (key, value) = arg.split_once('=').unwrap(); + if let Some((key, value)) = unprefixed.split_once('=') { // lookup the option by name - if let Some(option) = Self::SU_OPTIONS.iter().find(|o| o.long == &key[2..]) { + if let Some(option) = Self::SU_OPTIONS.iter().find(|o| o.long == key) { // the value is already present, when the option does not take any arguments this results in an error if option.takes_argument { (option.set)(&mut options, Some(value.to_string()))?; @@ -206,7 +435,8 @@ Err(format!("unrecognized option '{}'", arg))?; } // lookup the option - } else if let Some(option) = Self::SU_OPTIONS.iter().find(|o| o.long == &arg[2..]) { + } else if let Some(option) = Self::SU_OPTIONS.iter().find(|o| o.long == unprefixed) + { // try to parse an argument when the option needs an argument if option.takes_argument { let next_arg = arg_iter.next(); @@ -217,18 +447,20 @@ } else { Err(format!("unrecognized option '{}'", arg))?; } - } else if arg.starts_with('-') { + } else if let Some(unprefixed) = arg.strip_prefix('-') { // flags can be grouped, so we loop over the the characters - for (n, char) in arg.trim_start_matches('-').chars().enumerate() { + let mut chars = unprefixed.chars(); + while let Some(curr) = chars.next() { // lookup the option - if let Some(option) = Self::SU_OPTIONS.iter().find(|o| o.short == char) { + if let Some(option) = Self::SU_OPTIONS.iter().find(|o| o.short == curr) { // try to parse an argument when one is necessary, either the rest of the current flag group or the next argument + let rest = chars.as_str(); + if option.takes_argument { - let rest = arg[(n + 2)..].trim().to_string(); let next_arg = if rest.is_empty() { arg_iter.next() } else { - Some(rest) + Some(rest.to_string()) }; (option.set)(&mut options, next_arg)?; // stop looping over flags if the current flag takes an argument @@ -238,41 +470,54 @@ (option.set)(&mut options, None)?; } } else { - Err(format!("unrecognized option '{}'", char))?; + Err(format!("unrecognized option '{}'", curr))?; } } } else { - // when no option is provided (starting with - or --) the next argument is interpreted as a username - options.user = arg; - // the rest of the arguments are passed to the shell - options.arguments = arg_iter.collect(); - break; + options.positional_args.push(arg); } } Ok(options) } + + fn validate(self) -> Result { + let action = if self.help { + SuAction::Help(self.try_into()?) + } else if self.version { + SuAction::Version(self.try_into()?) + } else { + SuAction::Run(self.try_into()?) + }; + Ok(action) + } +} + +fn more_than_once(flag: &str) -> String { + format!("argument '{flag}' was provided more than once, but cannot be used multiple times") } #[cfg(test)] mod tests { use std::vec; - use super::{SuAction, SuOptions}; + use super::{SuAction, SuHelpOptions, SuOptions, SuRunOptions, SuVersionOptions}; - fn parse(args: &[&str]) -> SuOptions { + fn parse(args: &[&str]) -> SuAction { let mut args = args.iter().map(|s| s.to_string()).collect::>(); args.insert(0, "/bin/su".to_string()); - SuOptions::parse_arguments(args).unwrap() + SuOptions::parse_arguments(args) + .unwrap() + .validate() + .unwrap() } #[test] - fn it_parses_group() { - let expected = SuOptions { - group: vec!["ferris".to_string()], - ..Default::default() - }; + let expected = SuAction::Run(SuRunOptions { + group: vec!["ferris".into()], + ..<_>::default() + }); assert_eq!(expected, parse(&["-g", "ferris"])); assert_eq!(expected, parse(&["-gferris"])); assert_eq!(expected, parse(&["--group", "ferris"])); @@ -284,10 +529,10 @@ let result = parse(&["--shell", "/bin/bash"]); assert_eq!( result, - SuOptions { + SuAction::Run(SuRunOptions { shell: Some("/bin/bash".into()), - ..Default::default() - } + ..<_>::default() + }) ); } @@ -296,19 +541,19 @@ let result = parse(&["-w", "FOO,BAR"]); assert_eq!( result, - SuOptions { + SuAction::Run(SuRunOptions { whitelist_environment: vec!["FOO".to_string(), "BAR".to_string()], - ..Default::default() - } + ..<_>::default() + }) ); } #[test] fn it_parses_combined_options() { - let expected = SuOptions { + let expected = SuAction::Run(SuRunOptions { login: true, - ..Default::default() - }; + ..<_>::default() + }); assert_eq!(expected, parse(&["-Pl"])); assert_eq!(expected, parse(&["-lP"])); @@ -316,11 +561,11 @@ #[test] fn it_parses_combined_options_and_arguments() { - let expected = SuOptions { + let expected = SuAction::Run(SuRunOptions { login: true, shell: Some("/bin/bash".into()), - ..Default::default() - }; + ..<_>::default() + }); assert_eq!(expected, parse(&["-Pls/bin/bash"])); assert_eq!(expected, parse(&["-Pls", "/bin/bash"])); @@ -332,50 +577,40 @@ #[test] fn it_parses_an_user() { - assert_eq!( - SuOptions { - user: "ferris".to_string(), - ..Default::default() - }, - parse(&["-P", "ferris"]) - ); - - assert_eq!( - SuOptions { - user: "ferris".to_string(), - arguments: vec!["-P".to_string()], - ..Default::default() - }, - parse(&["ferris", "-P"]) - ); + let expected = SuAction::Run(SuRunOptions { + user: "ferris".into(), + ..<_>::default() + }); + assert_eq!(expected, parse(&["-P", "ferris"])); + assert_eq!(expected, parse(&["ferris", "-P"])); } #[test] fn it_parses_arguments() { - let expected = SuOptions { - user: "ferris".to_string(), + let expected = SuAction::Run(SuRunOptions { + user: "ferris".into(), arguments: vec!["script.sh".to_string()], - ..Default::default() - }; + ..<_>::default() + }); assert_eq!(expected, parse(&["-P", "ferris", "script.sh"])); } #[test] fn it_parses_command() { - let expected = SuOptions { + let expected = SuAction::Run(SuRunOptions { command: Some("'echo hi'".to_string()), - ..Default::default() - }; + ..<_>::default() + }); assert_eq!(expected, parse(&["-c", "'echo hi'"])); assert_eq!(expected, parse(&["-c'echo hi'"])); assert_eq!(expected, parse(&["--command", "'echo hi'"])); assert_eq!(expected, parse(&["--command='echo hi'"])); - let expected = SuOptions { + let expected = SuAction::Run(SuRunOptions { command: Some("env".to_string()), - ..Default::default() - }; + ..<_>::default() + }); assert_eq!(expected, parse(&["-c", "env"])); assert_eq!(expected, parse(&["-cenv"])); assert_eq!(expected, parse(&["--command", "env"])); @@ -384,10 +619,10 @@ #[test] fn it_parses_supplementary_group() { - let expected = SuOptions { - supp_group: vec!["ferris".to_string()], - ..Default::default() - }; + let expected = SuAction::Run(SuRunOptions { + supp_group: vec!["ferris".into()], + ..<_>::default() + }); assert_eq!(expected, parse(&["-G", "ferris"])); assert_eq!(expected, parse(&["-Gferris"])); assert_eq!(expected, parse(&["--supp-group", "ferris"])); @@ -396,14 +631,10 @@ #[test] fn it_parses_multiple_supplementary_groups() { - let expected = SuOptions { - supp_group: vec![ - "ferris".to_string(), - "krabbetje".to_string(), - "krabbe".to_string(), - ], - ..Default::default() - }; + let expected = SuAction::Run(SuRunOptions { + supp_group: vec!["ferris".into(), "krabbetje".into(), "krabbe".into()], + ..<_>::default() + }); assert_eq!( expected, parse(&["-G", "ferris", "-G", "krabbetje", "--supp-group", "krabbe"]) @@ -412,10 +643,10 @@ #[test] fn it_parses_login() { - let expected = SuOptions { + let expected = SuAction::Run(SuRunOptions { login: true, - ..Default::default() - }; + ..<_>::default() + }); assert_eq!(expected, parse(&["-"])); assert_eq!(expected, parse(&["-l"])); assert_eq!(expected, parse(&["--login"])); @@ -423,17 +654,17 @@ #[test] fn it_parses_pty() { - let expected = SuOptions::default(); + let expected = SuAction::Run(<_>::default()); assert_eq!(expected, parse(&["-P"])); assert_eq!(expected, parse(&["--pty"])); } #[test] fn it_parses_shell() { - let expected = SuOptions { + let expected = SuAction::Run(SuRunOptions { shell: Some("some-shell".into()), - ..Default::default() - }; + ..<_>::default() + }); assert_eq!(expected, parse(&["-s", "some-shell"])); assert_eq!(expected, parse(&["-ssome-shell"])); assert_eq!(expected, parse(&["--shell", "some-shell"])); @@ -442,10 +673,10 @@ #[test] fn it_parses_whitelist_environment() { - let expected = SuOptions { + let expected = SuAction::Run(SuRunOptions { whitelist_environment: vec!["FOO".to_string(), "BAR".to_string()], - ..Default::default() - }; + ..<_>::default() + }); assert_eq!(expected, parse(&["-w", "FOO,BAR"])); assert_eq!(expected, parse(&["-wFOO,BAR"])); assert_eq!(expected, parse(&["--whitelist-environment", "FOO,BAR"])); @@ -454,21 +685,86 @@ #[test] fn it_parses_help() { - let expected = SuOptions { - action: SuAction::Help, - ..Default::default() - }; + let expected = SuAction::Help(SuHelpOptions {}); assert_eq!(expected, parse(&["-h"])); assert_eq!(expected, parse(&["--help"])); } #[test] fn it_parses_version() { - let expected = SuOptions { - action: SuAction::Version, - ..Default::default() - }; + let expected = SuAction::Version(SuVersionOptions {}); assert_eq!(expected, parse(&["-V"])); assert_eq!(expected, parse(&["--version"])); } + + #[test] + fn short_flag_whitespace() { + let expected = SuAction::Run(SuRunOptions { + group: vec![" ".into()], + ..<_>::default() + }); + assert_eq!(expected, parse(&["-g "])); + } + + #[test] + fn short_flag_whitespace_positional_argument() { + let expected = SuAction::Run(SuRunOptions { + group: vec![" ".into()], + user: "ghost".into(), + ..<_>::default() + }); + assert_eq!(expected, parse(&["-g ", "ghost"])); + } + + #[test] + fn long_flag_equal_whitespace() { + let expected = SuAction::Run(SuRunOptions { + group: vec![" ".into()], + ..<_>::default() + }); + assert_eq!(expected, parse(&["--group= "])); + } + + #[test] + fn flag_after_positional_argument() { + let expected = SuAction::Run(SuRunOptions { + arguments: vec![], + login: true, + user: "ferris".into(), + ..<_>::default() + }); + assert_eq!(expected, parse(&["ferris", "-l"])); + } + + #[test] + fn flags_after_dash() { + let expected = SuAction::Run(SuRunOptions { + command: Some("echo".to_string()), + login: true, + ..<_>::default() + }); + assert_eq!(expected, parse(&["-", "-c", "echo"])); + } + + #[test] + fn only_positional_args_after_dashdash() { + let expected = SuAction::Run(SuRunOptions { + user: "ferris".into(), + arguments: vec!["-c".to_string(), "echo".to_string()], + ..<_>::default() + }); + assert_eq!(expected, parse(&["--", "ferris", "-c", "echo"])); + } + + #[test] + fn repeated_boolean_flag() { + let f = |s: &str| s.to_string(); + + assert!(SuOptions::parse_arguments(["su", "-l", "-l"].map(f)).is_err()); + assert!(SuOptions::parse_arguments(["su", "-", "-l"].map(f)).is_err()); + assert!(SuOptions::parse_arguments(["su", "--login", "-l"].map(f)).is_err()); + + assert!(SuOptions::parse_arguments(["su", "-p", "-p"].map(f)).is_err()); + assert!(SuOptions::parse_arguments(["su", "-p", "--preserve-environment"].map(f)).is_err()); + } } diff -Nru rust-sudo-rs-0.2.1/src/su/context.rs rust-sudo-rs-0.2.2/src/su/context.rs --- rust-sudo-rs-0.2.1/src/su/context.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/su/context.rs 2006-07-24 01:21:28.000000000 +0000 @@ -5,13 +5,13 @@ path::{Path, PathBuf}, }; -use crate::common::resolve::{is_valid_executable, resolve_current_user}; -use crate::common::{error::Error, Environment}; +use crate::common::{error::Error, resolve::CurrentUser, Environment}; +use crate::common::{resolve::is_valid_executable, SudoPath}; use crate::exec::RunOptions; use crate::log::user_warn; use crate::system::{Group, Process, User}; -use super::cli::SuOptions; +use super::cli::SuRunOptions; const VALID_LOGIN_SHELLS_LIST: &str = "/etc/shells"; const FALLBACK_LOGIN_SHELL: &str = "/bin/sh"; @@ -24,10 +24,10 @@ pub(crate) struct SuContext { command: PathBuf, arguments: Vec, - options: SuOptions, + options: SuRunOptions, pub(crate) environment: Environment, user: User, - requesting_user: User, + requesting_user: CurrentUser, group: Group, pub(crate) process: Process, } @@ -46,7 +46,7 @@ } impl SuContext { - pub(crate) fn from_env(options: SuOptions) -> Result { + pub(crate) fn from_env(options: SuRunOptions) -> Result { let process = crate::system::Process::new(); // resolve environment, reset if this is a login @@ -72,11 +72,11 @@ } } - let requesting_user = resolve_current_user()?; + let requesting_user = CurrentUser::resolve()?; // resolve target user - let mut user = User::from_name(&options.user)? - .ok_or_else(|| Error::UserNotFound(options.user.clone()))?; + let mut user = User::from_name(options.user.as_cstr())? + .ok_or_else(|| Error::UserNotFound(options.user.clone().into()))?; // check the current user is root let is_current_root = User::real_uid() == 0; @@ -98,8 +98,8 @@ } for group_name in options.group.iter() { - let primary_group = Group::from_name(group_name)? - .ok_or_else(|| Error::GroupNotFound(group_name.to_owned()))?; + let primary_group = Group::from_name(group_name.as_cstr())? + .ok_or_else(|| Error::GroupNotFound(group_name.clone().into()))?; // last argument is the primary group group = primary_group.clone(); @@ -108,8 +108,8 @@ // add additional group if current user is root for (index, group_name) in options.supp_group.iter().enumerate() { - let supp_group = Group::from_name(group_name)? - .ok_or_else(|| Error::GroupNotFound(group_name.to_owned()))?; + let supp_group = Group::from_name(group_name.as_cstr())? + .ok_or_else(|| Error::GroupNotFound(group_name.clone().into()))?; // set primary group if none was provided if index == 0 && options.group.is_empty() { @@ -178,7 +178,7 @@ if !options.preserve_environment { // extend environment with fixed variables - environment.insert("HOME".into(), user.home.clone().into_os_string()); + environment.insert("HOME".into(), user.home.clone().into()); environment.insert("SHELL".into(), command.clone().into()); environment.insert( "MAIL".into(), @@ -217,7 +217,7 @@ None } - fn chdir(&self) -> Option<&std::path::PathBuf> { + fn chdir(&self) -> Option<&SudoPath> { None } @@ -250,14 +250,20 @@ mod tests { use std::path::PathBuf; - use crate::{common::Error, su::cli::SuOptions}; + use crate::{ + common::Error, + su::cli::{SuAction, SuRunOptions}, + }; use super::SuContext; - fn get_options(args: &[&str]) -> SuOptions { + fn get_options(args: &[&str]) -> SuRunOptions { let mut args = args.iter().map(|s| s.to_string()).collect::>(); args.insert(0, "/bin/su".to_string()); - SuOptions::parse_arguments(args).unwrap() + SuAction::parse_arguments(args) + .unwrap() + .try_into_run() + .unwrap() } #[test] diff -Nru rust-sudo-rs-0.2.1/src/su/mod.rs rust-sudo-rs-0.2.2/src/su/mod.rs --- rust-sudo-rs-0.2.1/src/su/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/su/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -6,14 +6,17 @@ use std::{env, process}; -use cli::{SuAction, SuOptions}; +use cli::SuAction; use context::SuContext; use help::{long_help_message, USAGE_MSG}; +use self::cli::SuRunOptions; + mod cli; mod context; mod help; +const DEFAULT_USER: &str = "root"; const VERSION: &str = env!("CARGO_PKG_VERSION"); fn authenticate( @@ -76,7 +79,7 @@ Ok(pam) } -fn run(options: SuOptions) -> Result<(), Error> { +fn run(options: SuRunOptions) -> Result<(), Error> { // lookup user and build context object let context = SuContext::from_env(options)?; @@ -122,24 +125,24 @@ pub fn main() { crate::log::SudoLogger::new("su: ").into_global_logger(); - let su_options = match SuOptions::from_env() { - Ok(options) => options, + let action = match SuAction::from_env() { + Ok(action) => action, Err(error) => { println_ignore_io_error!("su: {error}\n{USAGE_MSG}"); std::process::exit(1); } }; - match su_options.action { - SuAction::Help => { + match action { + SuAction::Help(_) => { println_ignore_io_error!("{}", long_help_message()); std::process::exit(0); } - SuAction::Version => { + SuAction::Version(_) => { eprintln_ignore_io_error!("su-rs {VERSION}"); std::process::exit(0); } - SuAction::Run => match run(su_options) { + SuAction::Run(options) => match run(options) { Err(Error::CommandNotFound(c)) => { eprintln_ignore_io_error!("su: {}", Error::CommandNotFound(c)); std::process::exit(127); diff -Nru rust-sudo-rs-0.2.1/src/sudo/cli/help.rs rust-sudo-rs-0.2.2/src/sudo/cli/help.rs --- rust-sudo-rs-0.2.1/src/sudo/cli/help.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/cli/help.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,29 @@ +pub const USAGE_MSG: &str = "\ +usage: sudo -h | -K | -k | -V +usage: sudo -v [-knS] [-g group] [-u user] +usage: sudo -l [-knS] [-g group] [-U user] [-u user] [command [arg ...]] +usage: sudo [-knS] [-D directory] [-g group] [-u user] [-i | -s] [command [arg ...]] +usage: sudo -e [-knS] [-D directory] [-g group] [-u user] file ..."; + +const DESCRIPTOR: &str = "sudo - run commands as another user"; + +const HELP_MSG: &str = "Options: + -D, --chdir=directory change the working directory before running command + -g, --group=group run command as the specified group name or ID + -h, --help display help message and exit + -i, --login run login shell as the target user; a command may also be specified + -K, --remove-timestamp remove timestamp file completely + -k, --reset-timestamp invalidate timestamp file + -l, --list list user's privileges or check a specific command; use twice for longer format + -n, --non-interactive non-interactive mode, no prompts are used + -S, --stdin read password from standard input + -s, --shell run shell as the target user; a command may also be specified + -U, --other-user=user in list mode, display privileges for user + -u, --user=user run command (or edit file) as specified user name or ID + -V, --version display version information and exit + -v, --validate update user's timestamp without running a command + -- stop processing command line arguments"; + +pub fn long_help_message() -> String { + format!("{DESCRIPTOR}\n{USAGE_MSG}\n{HELP_MSG}") +} diff -Nru rust-sudo-rs-0.2.1/src/sudo/cli/mod.rs rust-sudo-rs-0.2.2/src/sudo/cli/mod.rs --- rust-sudo-rs-0.2.1/src/sudo/cli/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/cli/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,888 @@ +#![forbid(unsafe_code)] + +use std::{borrow::Cow, mem}; + +use crate::common::context::{ContextAction, OptionsForContext}; +use crate::common::{SudoPath, SudoString}; + +pub mod help; + +#[cfg(test)] +mod tests; + +pub enum SudoAction { + Edit(SudoEditOptions), + Help(SudoHelpOptions), + List(SudoListOptions), + RemoveTimestamp(SudoRemoveTimestampOptions), + ResetTimestamp(SudoResetTimestampOptions), + Run(SudoRunOptions), + Validate(SudoValidateOptions), + Version(SudoVersionOptions), +} + +impl SudoAction { + /// try to parse and environment variable assignment + /// parse command line arguments from the environment and handle errors + pub fn from_env() -> Result { + Self::try_parse_from(std::env::args()) + } + + pub fn try_parse_from(iter: I) -> Result + where + I: IntoIterator, + T: Into + Clone, + { + let opts = SudoOptions::try_parse_from(iter)?; + opts.validate() + } + + #[cfg(test)] + #[must_use] + pub fn is_edit(&self) -> bool { + matches!(self, Self::Edit(..)) + } + + #[cfg(test)] + #[must_use] + pub fn is_help(&self) -> bool { + matches!(self, Self::Help(..)) + } + + #[cfg(test)] + #[must_use] + pub fn is_remove_timestamp(&self) -> bool { + matches!(self, Self::RemoveTimestamp(..)) + } + + #[cfg(test)] + #[must_use] + pub fn is_reset_timestamp(&self) -> bool { + matches!(self, Self::ResetTimestamp(..)) + } + + #[cfg(test)] + #[must_use] + pub fn is_list(&self) -> bool { + matches!(self, Self::List(..)) + } + + #[cfg(test)] + #[must_use] + pub fn is_version(&self) -> bool { + matches!(self, Self::Version(..)) + } + + #[cfg(test)] + #[must_use] + pub fn is_validate(&self) -> bool { + matches!(self, Self::Validate(..)) + } + + #[cfg(test)] + pub fn try_into_run(self) -> Result { + if let Self::Run(v) = self { + Ok(v) + } else { + Err(self) + } + } + + #[cfg(test)] + #[must_use] + pub fn is_run(&self) -> bool { + matches!(self, Self::Run(..)) + } +} + +// sudo -h | -K | -k | -V +pub struct SudoHelpOptions {} + +impl TryFrom for SudoHelpOptions { + type Error = String; + + fn try_from(mut opts: SudoOptions) -> Result { + // see `SudoOptions::validate` + let help = mem::take(&mut opts.help); + debug_assert!(help); + + reject_all("--help", opts)?; + + Ok(Self {}) + } +} + +// sudo -h | -K | -k | -V +pub struct SudoVersionOptions {} + +impl TryFrom for SudoVersionOptions { + type Error = String; + + fn try_from(mut opts: SudoOptions) -> Result { + // see `SudoOptions::validate` + let version = mem::take(&mut opts.version); + debug_assert!(version); + + reject_all("--version", opts)?; + + Ok(Self {}) + } +} + +// sudo -h | -K | -k | -V +pub struct SudoRemoveTimestampOptions {} + +impl TryFrom for SudoRemoveTimestampOptions { + type Error = String; + + fn try_from(mut opts: SudoOptions) -> Result { + // see `SudoOptions::validate` + let remove_timestamp = mem::take(&mut opts.remove_timestamp); + debug_assert!(remove_timestamp); + + reject_all("--remove-timestamp", opts)?; + + Ok(Self {}) + } +} + +// sudo -h | -K | -k | -V +pub struct SudoResetTimestampOptions {} + +impl TryFrom for SudoResetTimestampOptions { + type Error = String; + + fn try_from(mut opts: SudoOptions) -> Result { + // see `SudoOptions::validate` + let reset_timestamp = mem::take(&mut opts.reset_timestamp); + debug_assert!(reset_timestamp); + + reject_all("--reset-timestamp", opts)?; + + Ok(Self {}) + } +} + +// sudo -v [-ABkNnS] [-g group] [-h host] [-p prompt] [-u user] +pub struct SudoValidateOptions { + // -k + pub reset_timestamp: bool, + // -n + pub non_interactive: bool, + // -S + pub stdin: bool, + // -g + pub group: Option, + // -u + pub user: Option, +} + +impl TryFrom for SudoValidateOptions { + type Error = String; + + fn try_from(mut opts: SudoOptions) -> Result { + // see `SudoOptions::validate` + let validate = mem::take(&mut opts.validate); + debug_assert!(validate); + + let reset_timestamp = mem::take(&mut opts.reset_timestamp); + let non_interactive = mem::take(&mut opts.non_interactive); + let stdin = mem::take(&mut opts.stdin); + let group = mem::take(&mut opts.group); + let user = mem::take(&mut opts.user); + + reject_all("--validate", opts)?; + + Ok(Self { + reset_timestamp, + non_interactive, + stdin, + group, + user, + }) + } +} + +// sudo -e [-ABkNnS] [-r role] [-t type] [-C num] [-D directory] [-g group] [-h host] [-p prompt] [-R directory] [-T timeout] [-u user] file ... +pub struct SudoEditOptions { + // -k + pub reset_timestamp: bool, + // -n + pub non_interactive: bool, + // -S + pub stdin: bool, + // -D + pub chdir: Option, + // -g + pub group: Option, + // -u + pub user: Option, + pub positional_args: Vec, +} + +impl TryFrom for SudoEditOptions { + type Error = String; + + fn try_from(mut opts: SudoOptions) -> Result { + // see `SudoOptions::validate` + let edit = mem::take(&mut opts.edit); + debug_assert!(edit); + + let reset_timestamp = mem::take(&mut opts.reset_timestamp); + let non_interactive = mem::take(&mut opts.non_interactive); + let stdin = mem::take(&mut opts.stdin); + let chdir = mem::take(&mut opts.chdir); + let group = mem::take(&mut opts.group); + let user = mem::take(&mut opts.user); + let positional_args = mem::take(&mut opts.positional_args); + + reject_all("--edit", opts)?; + + if positional_args.is_empty() { + return Err("must specify at least one file path".into()); + } + + Ok(Self { + reset_timestamp, + non_interactive, + stdin, + chdir, + group, + user, + positional_args, + }) + } +} + +// sudo -l [-ABkNnS] [-g group] [-h host] [-p prompt] [-U user] [-u user] [command [arg ...]] +pub struct SudoListOptions { + // -l OR -l -l + pub list: List, + + // -k + pub reset_timestamp: bool, + // -n + pub non_interactive: bool, + // -S + pub stdin: bool, + // -g + pub group: Option, + // -U + pub other_user: Option, + // -u + pub user: Option, + + pub positional_args: Vec, +} + +impl TryFrom for SudoListOptions { + type Error = String; + + fn try_from(mut opts: SudoOptions) -> Result { + let list = opts.list.take().unwrap(); + let reset_timestamp = mem::take(&mut opts.reset_timestamp); + let non_interactive = mem::take(&mut opts.non_interactive); + let stdin = mem::take(&mut opts.stdin); + let group = mem::take(&mut opts.group); + let other_user = mem::take(&mut opts.other_user); + let user = mem::take(&mut opts.user); + let positional_args = mem::take(&mut opts.positional_args); + + // when present, `-u` must be accompanied by a command + let has_command = !positional_args.is_empty(); + let valid_user_flag = user.is_none() || has_command; + + if !valid_user_flag { + return Err("'--user' flag must be accompanied by a command".into()); + } + + reject_all("--list", opts)?; + + Ok(Self { + list, + reset_timestamp, + non_interactive, + stdin, + group, + other_user, + user, + positional_args, + }) + } +} + +// sudo [-ABbEHnPS] [-C num] [-D directory] [-g group] [-h host] [-p prompt] [-R directory] [-T timeout] [-u user] [VAR=value] [-i | -s] [command [arg ...]] +pub struct SudoRunOptions { + // -E + pub preserve_env: PreserveEnv, + // -k + pub reset_timestamp: bool, + // -n + pub non_interactive: bool, + // -S + pub stdin: bool, + // -D + pub chdir: Option, + // -g + pub group: Option, + // -u + pub user: Option, + // VAR=value + pub env_var_list: Vec<(String, String)>, + // -i + pub login: bool, + // -s + pub shell: bool, + pub positional_args: Vec, +} + +impl TryFrom for SudoRunOptions { + type Error = String; + + fn try_from(mut opts: SudoOptions) -> Result { + let preserve_env = mem::take(&mut opts.preserve_env); + let reset_timestamp = mem::take(&mut opts.reset_timestamp); + let non_interactive = mem::take(&mut opts.non_interactive); + let stdin = mem::take(&mut opts.stdin); + let chdir = mem::take(&mut opts.chdir); + let group = mem::take(&mut opts.group); + let user = mem::take(&mut opts.user); + let env_var_list = mem::take(&mut opts.env_var_list); + let login = mem::take(&mut opts.login); + let shell = mem::take(&mut opts.shell); + let positional_args = mem::take(&mut opts.positional_args); + + let context = match (login, shell, positional_args.is_empty()) { + (true, false, _) => "--login", + (false, true, _) => "--shell", + (false, false, false) => "command (positional argument)", + + (true, true, _) => return Err("--login conflicts with --shell".into()), + (false, false, true) => { + if cfg!(debug_assertions) { + // see `SudoOptions::validate` + panic!(); + } else { + return Err( + "expected one of: --login, --shell, a command as a positional argument" + .into(), + ); + } + } + }; + + reject_all(context, opts)?; + + Ok(Self { + preserve_env, + reset_timestamp, + non_interactive, + stdin, + chdir, + group, + user, + env_var_list, + login, + shell, + positional_args, + }) + } +} + +#[derive(Default)] +struct SudoOptions { + // -D + chdir: Option, + // -g + group: Option, + // -i + login: bool, + // -n + non_interactive: bool, + // -U + other_user: Option, + // -E + preserve_env: PreserveEnv, + // -s + shell: bool, + // -S + stdin: bool, + // -u + user: Option, + + // additional environment + env_var_list: Vec<(String, String)>, + + /* actions */ + // -e + edit: bool, + // -h + help: bool, + // -l + list: Option, + // -K + remove_timestamp: bool, + // -k + reset_timestamp: bool, + // -v + validate: bool, + // -V + version: bool, + + // arguments passed straight through, either seperated by -- or just trailing. + positional_args: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq)] +pub enum PreserveEnv { + #[default] + Nothing, + Everything, + Only(Vec), +} + +impl PreserveEnv { + #[cfg(test)] + pub fn try_into_only(self) -> Result, Self> { + if let Self::Only(v) = self { + Ok(v) + } else { + Err(self) + } + } + + pub fn is_nothing(&self) -> bool { + matches!(self, Self::Nothing) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum List { + Once, + Verbose, +} + +impl List { + #[must_use] + pub fn is_verbose(&self) -> bool { + matches!(self, Self::Verbose) + } +} + +enum SudoArg { + Flag(String), + Argument(String, String), + Environment(String, String), + Rest(Vec), +} + +impl SudoArg { + const TAKES_ARGUMENT_SHORT: &'static [char] = &['D', 'g', 'h', 'R', 'U', 'u']; + const TAKES_ARGUMENT: &'static [&'static str] = + &["chdir", "group", "host", "chroot", "other-user", "user"]; + + /// argument assignments and shorthand options preprocessing + fn normalize_arguments(iter: I) -> Result, String> + where + I: IntoIterator, + { + // the first argument is the sudo command - so we can skip it + let mut arg_iter = iter.into_iter().skip(1); + let mut processed = vec![]; + + while let Some(arg) = arg_iter.next() { + if arg == "--" { + processed.push(SudoArg::Rest(arg_iter.collect())); + break; + } else if let Some(unprefixed) = arg.strip_prefix("--") { + if let Some((key, value)) = unprefixed.split_once('=') { + // convert assignment to normal tokens + + // only accept arguments when one is expected + // `--preserve-env` is special as it only takes an argument using this `key=value` syntax + if !Self::TAKES_ARGUMENT.contains(&key) && key != "preserve-env" { + Err(format!("'{}' does not take any arguments", key))?; + } + processed.push(SudoArg::Argument("--".to_string() + key, value.to_string())); + } else if Self::TAKES_ARGUMENT.contains(&unprefixed) { + if let Some(next) = arg_iter.next() { + processed.push(SudoArg::Argument(arg, next)); + } else { + Err(format!("'{}' expects an argument", &arg))?; + } + } else { + processed.push(SudoArg::Flag(arg)); + } + } else if let Some(unprefixed) = arg.strip_prefix('-') { + // split combined shorthand options + let mut chars = unprefixed.chars(); + + while let Some(curr) = chars.next() { + let flag = format!("-{curr}"); + // convert option argument to separate segment + if Self::TAKES_ARGUMENT_SHORT.contains(&curr) { + let rest = chars.as_str(); + let next = chars.next(); + + // assignment syntax is not accepted for shorthand arguments + if next == Some('=') { + Err("invalid option '='")?; + } + if next.is_some() { + processed.push(SudoArg::Argument(flag, rest.to_string())); + } else if let Some(next) = arg_iter.next() { + processed.push(SudoArg::Argument(flag, next)); + } else if curr == 'h' { + // short version of --help has no arguments + processed.push(SudoArg::Flag(flag)); + } else { + Err(format!("'-{}' expects an argument", curr))?; + } + break; + } else { + processed.push(SudoArg::Flag(flag)); + } + } + } else if let Some((key, value)) = try_to_env_var(&arg) { + processed.push(SudoArg::Environment(key, value)); + } else { + let mut rest = vec![arg]; + rest.extend(arg_iter); + processed.push(SudoArg::Rest(rest)); + break; + } + } + + Ok(processed) + } +} + +impl SudoOptions { + fn validate(self) -> Result { + let action = if self.help { + SudoAction::Help(self.try_into()?) + } else if self.version { + SudoAction::Version(self.try_into()?) + } else if self.remove_timestamp { + SudoAction::RemoveTimestamp(self.try_into()?) + } else if self.validate { + SudoAction::Validate(self.try_into()?) + } else if self.list.is_some() { + SudoAction::List(self.try_into()?) + } else if self.edit { + SudoAction::Edit(self.try_into()?) + } else { + let is_run = self.login | self.shell | !self.positional_args.is_empty(); + + if is_run { + SudoAction::Run(self.try_into()?) + } else if self.reset_timestamp { + SudoAction::ResetTimestamp(self.try_into()?) + } else { + return Err("expected one of these actions: --help, --version, --remove-timestamp, --validate, --list, --edit, --login, --shell, a command as a positional argument, --reset-timestamp".into()); + } + }; + + Ok(action) + } + + /// parse an iterator over command line arguments + fn try_parse_from(iter: I) -> Result + where + I: IntoIterator, + T: Into + Clone, + { + let mut options = Self::default(); + let arg_iter = SudoArg::normalize_arguments(iter.into_iter().map(Into::into))? + .into_iter() + .peekable(); + + for arg in arg_iter { + match arg { + SudoArg::Flag(flag) => match flag.as_str() { + "-E" | "--preserve-env" => { + options.preserve_env = PreserveEnv::Everything; + } + "-e" | "--edit" => { + options.edit = true; + } + "-H" | "--set-home" => { + // this option is ignored, since it is the default for sudo-rs; but accept + // it for backwards compatibility reasons + } + "-h" | "--help" => { + options.help = true; + } + "-i" | "--login" => { + options.login = true; + } + "-K" | "--remove-timestamp" => { + options.remove_timestamp = true; + } + "-k" | "--reset-timestamp" => { + options.reset_timestamp = true; + } + "-l" | "--list" => match options.list { + None => options.list = Some(List::Once), + Some(List::Once) => options.list = Some(List::Verbose), + Some(List::Verbose) => {} + }, + "-n" | "--non-interactive" => { + options.non_interactive = true; + } + "-S" | "--stdin" => { + options.stdin = true; + } + "-s" | "--shell" => { + options.shell = true; + } + "-V" | "--version" => { + options.version = true; + } + "-v" | "--validate" => { + options.validate = true; + } + _option => { + Err("invalid option provided")?; + } + }, + SudoArg::Argument(option, value) => match option.as_str() { + "-D" | "--chdir" => { + options.chdir = Some(SudoPath::from_cli_string(value)); + } + "-E" | "--preserve-env" => { + let split_value = || value.split(',').map(str::to_string); + match &mut options.preserve_env { + PreserveEnv::Nothing => { + options.preserve_env = PreserveEnv::Only(split_value().collect()) + } + PreserveEnv::Everything => {} + PreserveEnv::Only(list) => list.extend(split_value()), + } + // options.preserve_env = value.split(',').map(str::to_string).collect() + } + "-g" | "--group" => { + options.group = Some(SudoString::from_cli_string(value)); + } + "-U" | "--other-user" => { + options.other_user = Some(SudoString::from_cli_string(value)); + } + "-u" | "--user" => { + options.user = Some(SudoString::from_cli_string(value)); + } + _option => { + Err("invalid option provided")?; + } + }, + SudoArg::Environment(key, value) => { + options.env_var_list.push((key, value)); + } + SudoArg::Rest(rest) => { + options.positional_args = rest; + } + } + } + + Ok(options) + } +} + +fn try_to_env_var(arg: &str) -> Option<(String, String)> { + let (name, value) = arg.split_once('=')?; + + if name.chars().all(|c| c.is_alphanumeric() || c == '_') { + Some((name.to_owned(), value.to_owned())) + } else { + None + } +} + +trait IsAbsent { + fn is_absent(&self) -> bool; +} + +impl IsAbsent for bool { + fn is_absent(&self) -> bool { + !*self + } +} + +impl IsAbsent for Option { + fn is_absent(&self) -> bool { + self.is_none() + } +} + +impl IsAbsent for Vec { + fn is_absent(&self) -> bool { + self.is_empty() + } +} + +impl IsAbsent for PreserveEnv { + fn is_absent(&self) -> bool { + self.is_nothing() + } +} + +fn ensure_is_absent(context: &str, thing: &dyn IsAbsent, name: &str) -> Result<(), String> { + if thing.is_absent() { + Ok(()) + } else { + Err(format!("{context} conflicts with {name}")) + } +} + +fn reject_all(context: &str, opts: SudoOptions) -> Result<(), String> { + macro_rules! tuple { + ($expr:expr) => { + (&$expr as &dyn IsAbsent, { + let name = concat!("--", stringify!($expr)); + if name.contains('_') { + Cow::Owned(name.replace('_', "-")) + } else { + Cow::Borrowed(name) + } + }) + }; + } + + let SudoOptions { + chdir, + group, + login, + non_interactive, + other_user, + preserve_env, + shell, + stdin, + user, + env_var_list, + edit, + help, + list, + remove_timestamp, + reset_timestamp, + validate, + version, + positional_args, + } = opts; + + let flags = [ + tuple!(chdir), + tuple!(edit), + tuple!(group), + tuple!(help), + tuple!(list), + tuple!(login), + tuple!(non_interactive), + tuple!(other_user), + tuple!(preserve_env), + tuple!(remove_timestamp), + tuple!(reset_timestamp), + tuple!(shell), + tuple!(stdin), + tuple!(user), + tuple!(validate), + tuple!(version), + ]; + for (value, name) in flags { + ensure_is_absent(context, value, &name)?; + } + + ensure_is_absent(context, &env_var_list, "environment variable")?; + ensure_is_absent(context, &positional_args, "positional argument")?; + + Ok(()) +} + +impl From for OptionsForContext { + fn from(opts: SudoListOptions) -> Self { + let SudoListOptions { + group, + non_interactive, + positional_args, + reset_timestamp, + stdin, + user, + + list: _, + other_user: _, + } = opts; + + Self { + action: ContextAction::List, + + group, + non_interactive, + positional_args, + reset_timestamp, + stdin, + user, + + chdir: None, + login: false, + shell: false, + } + } +} + +impl From for OptionsForContext { + fn from(opts: SudoValidateOptions) -> Self { + let SudoValidateOptions { + group, + non_interactive, + reset_timestamp, + stdin, + user, + } = opts; + + Self { + action: ContextAction::Validate, + + group, + non_interactive, + reset_timestamp, + stdin, + user, + + chdir: None, + login: false, + positional_args: vec![], + shell: false, + } + } +} + +impl From for OptionsForContext { + fn from(opts: SudoRunOptions) -> Self { + let SudoRunOptions { + chdir, + group, + login, + non_interactive, + positional_args, + reset_timestamp, + shell, + stdin, + user, + + env_var_list: _, + preserve_env: _, + } = opts; + + Self { + action: ContextAction::Run, + + chdir, + group, + login, + non_interactive, + positional_args, + reset_timestamp, + shell, + stdin, + user, + } + } +} diff -Nru rust-sudo-rs-0.2.1/src/sudo/cli/tests.rs rust-sudo-rs-0.2.2/src/sudo/cli/tests.rs --- rust-sudo-rs-0.2.1/src/sudo/cli/tests.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/cli/tests.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,461 @@ +use crate::common::SudoPath; + +use crate::sudo::cli::PreserveEnv; + +use super::{SudoAction, SudoOptions}; +use pretty_assertions::assert_eq; + +/// Passing '-E' with a variable fails +#[test] +fn short_preserve_env_with_var_fails() { + let argss = [["sudo", "-E=variable"], ["sudo", "-Evariable"]]; + + for args in argss { + let res = SudoOptions::try_parse_from(args); + assert!(res.is_err()) + } +} + +/// Passing '--preserve-env' with an argument fills 'preserve_env', 'short_preserve_env' stays 'false' +#[test] +fn preserve_env_with_var() { + let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env=some_argument"]).unwrap(); + assert_eq!( + ["some_argument"], + cmd.preserve_env.try_into_only().unwrap().as_slice(), + ); +} + +/// Passing '--preserve-env' with several arguments fills 'preserve_env', 'short_preserve_env' stays 'false' +#[test] +fn preserve_env_with_several_vars() { + let cmd = SudoOptions::try_parse_from([ + "sudo", + "--preserve-env=some_argument,another_argument,a_third_one", + ]) + .unwrap(); + assert_eq!( + ["some_argument", "another_argument", "a_third_one"], + cmd.preserve_env.try_into_only().unwrap().as_slice(), + ); +} + +#[test] +fn preserve_env_boolean() { + let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env"]).unwrap(); + assert_eq!(cmd.preserve_env, PreserveEnv::Everything); +} + +#[test] +fn preserve_env_boolean_and_list() { + let expected = PreserveEnv::Everything; + let argss = [ + ["sudo", "--preserve-env", "--preserve-env=some_argument"], + ["sudo", "--preserve-env=some_argument", "--preserve-env"], + ]; + + for args in argss { + let cmd = SudoOptions::try_parse_from(args).unwrap(); + assert_eq!(expected, cmd.preserve_env); + } +} + +#[test] +fn preserve_env_repeated() { + let cmd = SudoOptions::try_parse_from([ + "sudo", + "--preserve-env=some_argument", + "--preserve-env=another_argument", + ]) + .unwrap(); + assert_eq!( + ["some_argument", "another_argument"], + cmd.preserve_env.try_into_only().unwrap().as_slice() + ); +} + +// `--preserve-env` only accepts a value with the syntax `--preserve-env=varname` +// so this `--preserve-env` is acting like a boolean flag +#[test] +fn preserve_env_space() { + let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env", "true"]).unwrap(); + + assert_eq!(PreserveEnv::Everything, cmd.preserve_env); + assert_eq!(["true"], cmd.positional_args.as_slice()); +} + +/// Catch env variable that is given without hyphens in 'VAR=value' form in env_var_list. +/// external_args stay empty. +#[test] +fn env_variable() { + let cmd = SudoOptions::try_parse_from(["sudo", "ENV=with_a_value"]).unwrap(); + assert_eq!( + cmd.env_var_list, + vec![("ENV".to_owned(), "with_a_value".to_owned())] + ); + assert!(cmd.positional_args.is_empty()); +} + +/// Catch several env variablse that are given without hyphens in 'VAR=value' form in env_var_list. +/// external_args stay empty. +#[test] +fn several_env_variables() { + let cmd = SudoOptions::try_parse_from([ + "sudo", + "ENV=with_a_value", + "another_var=otherval", + "more=this_is_a_val", + ]) + .unwrap(); + assert_eq!( + cmd.env_var_list, + vec![ + ("ENV".to_owned(), "with_a_value".to_owned()), + ("another_var".to_owned(), "otherval".to_owned()), + ("more".to_owned(), "this_is_a_val".to_owned()) + ] + ); + assert!(cmd.positional_args.is_empty()); +} + +/// Mix env variables and trailing arguments that just pass through sudo +/// Divided by hyphens. +#[test] +fn mix_env_variables_with_trailing_args_divided_by_hyphens() { + let cmd = SudoOptions::try_parse_from(["sudo", "env=var", "--", "external=args", "something"]) + .unwrap(); + assert_eq!(cmd.env_var_list, vec![("env".to_owned(), "var".to_owned())]); + assert_eq!(cmd.positional_args, vec!["external=args", "something"]); +} + +/// Mix env variables and trailing arguments that just pass through sudo +/// Divided by known flag. +#[test] +fn mix_env_variables_with_trailing_args_divided_by_known_flag() { + let cmd = SudoOptions::try_parse_from(["sudo", "-i", "external=args", "something"]).unwrap(); + assert_eq!( + cmd.env_var_list, + vec![("external".to_owned(), "args".to_owned())] + ); + assert!(cmd.login); + assert_eq!(cmd.positional_args, vec!["something"]); +} + +/// Catch trailing arguments that just pass through sudo +/// but look like a known flag. +#[test] +fn trailing_args_followed_by_known_flag() { + let cmd = + SudoOptions::try_parse_from(["sudo", "args", "followed_by", "known_flag", "-i"]).unwrap(); + assert!(!cmd.login); + assert_eq!( + cmd.positional_args, + vec!["args", "followed_by", "known_flag", "-i"] + ); +} + +/// Catch trailing arguments that just pass through sudo +/// but look like a known flag, divided by hyphens. +#[test] +fn trailing_args_hyphens_known_flag() { + let cmd = SudoOptions::try_parse_from([ + "sudo", + "--", + "trailing", + "args", + "followed_by", + "known_flag", + "-i", + ]) + .unwrap(); + assert!(!cmd.login); + assert_eq!( + cmd.positional_args, + vec!["trailing", "args", "followed_by", "known_flag", "-i"] + ); +} + +/// Check that the first environment variable declaration before any command is not treated as part +/// of the command. +#[test] +fn first_trailing_env_var_is_not_an_external_arg() { + let cmd = SudoAction::try_parse_from(["sudo", "FOO=1", "command", "BAR=2"]).unwrap(); + let opts = if let SudoAction::Run(opts) = cmd { + opts + } else { + panic!() + }; + assert_eq!(opts.env_var_list, vec![("FOO".to_owned(), "1".to_owned()),]); + assert_eq!(opts.positional_args, ["command", "BAR=2"],); +} + +#[test] +fn trailing_env_vars_are_external_args() { + let cmd = SudoOptions::try_parse_from([ + "sudo", "FOO=1", "-i", "BAR=2", "command", "BAZ=3", "arg", "FOOBAR=4", "command", "arg", + "BARBAZ=5", + ]) + .unwrap(); + assert!(cmd.login); + assert_eq!( + cmd.env_var_list, + vec![ + ("FOO".to_owned(), "1".to_owned()), + ("BAR".to_owned(), "2".to_owned()) + ] + ); + assert_eq!( + cmd.positional_args, + ["command", "BAZ=3", "arg", "FOOBAR=4", "command", "arg", "BARBAZ=5"] + ); +} + +#[test] +fn single_env_var_declaration() { + let cmd = SudoOptions::try_parse_from(["sudo", "FOO=1", "command"]).unwrap(); + assert_eq!(cmd.env_var_list, vec![("FOO".to_owned(), "1".to_owned())]); + assert_eq!(cmd.positional_args, ["command"]); +} + +#[test] +fn shorthand_with_argument() { + let cmd = SudoOptions::try_parse_from(["sudo", "-u", "ferris"]).unwrap(); + assert_eq!(cmd.user.as_deref(), Some("ferris")); +} + +#[test] +fn shorthand_with_direct_argument() { + let cmd = SudoOptions::try_parse_from(["sudo", "-uferris"]).unwrap(); + assert_eq!(cmd.user.as_deref(), Some("ferris")); +} + +#[test] +fn shorthand_without_argument() { + let cmd = SudoOptions::try_parse_from(["sudo", "-u"]); + assert!(cmd.is_err()) +} + +#[test] +fn non_interactive() { + let cmd = SudoOptions::try_parse_from(["sudo", "-n"]).unwrap(); + assert!(cmd.non_interactive); + + let cmd = SudoOptions::try_parse_from(["sudo", "--non-interactive"]).unwrap(); + assert!(cmd.non_interactive); +} + +#[test] +fn stdin() { + let cmd = SudoOptions::try_parse_from(["sudo", "-S"]).unwrap(); + assert!(cmd.stdin); + + let cmd = SudoOptions::try_parse_from(["sudo", "--stdin"]).unwrap(); + assert!(cmd.stdin); +} + +#[test] +fn shell() { + let cmd = SudoOptions::try_parse_from(["sudo", "-s"]).unwrap(); + assert!(cmd.shell); + + let cmd = SudoOptions::try_parse_from(["sudo", "--shell"]).unwrap(); + assert!(cmd.shell); +} + +#[test] +fn directory() { + let cmd = SudoOptions::try_parse_from(["sudo", "-D/some/path"]).unwrap(); + assert_eq!(cmd.chdir, Some(SudoPath::from("/some/path"))); + + let cmd = SudoOptions::try_parse_from(["sudo", "--chdir", "/some/path"]).unwrap(); + assert_eq!(cmd.chdir, Some(SudoPath::from("/some/path"))); + + let cmd = SudoOptions::try_parse_from(["sudo", "--chdir=/some/path"]).unwrap(); + assert_eq!(cmd.chdir, Some(SudoPath::from("/some/path"))); +} + +#[test] +fn group() { + let cmd = SudoOptions::try_parse_from(["sudo", "-grustaceans"]).unwrap(); + assert_eq!(cmd.group.as_deref(), Some("rustaceans")); + + let cmd = SudoOptions::try_parse_from(["sudo", "--group", "rustaceans"]).unwrap(); + assert_eq!(cmd.group.as_deref(), Some("rustaceans")); + + let cmd = SudoOptions::try_parse_from(["sudo", "--group=rustaceans"]).unwrap(); + assert_eq!(cmd.group.as_deref(), Some("rustaceans")); +} + +#[test] +fn other_user() { + let cmd = SudoOptions::try_parse_from(["sudo", "-Uferris"]).unwrap(); + assert_eq!(cmd.other_user.as_deref(), Some("ferris")); + + let cmd = SudoOptions::try_parse_from(["sudo", "--other-user", "ferris"]).unwrap(); + assert_eq!(cmd.other_user.as_deref(), Some("ferris")); + + let cmd = SudoOptions::try_parse_from(["sudo", "--other-user=ferris"]).unwrap(); + assert_eq!(cmd.other_user.as_deref(), Some("ferris")); +} + +#[test] +fn invalid_option() { + let cmd = SudoOptions::try_parse_from(["sudo", "--wololo"]); + assert!(cmd.is_err()) +} + +#[test] +fn invalid_option_with_argument() { + let cmd = SudoOptions::try_parse_from(["sudo", "--background=yes"]); + assert!(cmd.is_err()) +} + +#[test] +fn no_argument_provided() { + let cmd = SudoOptions::try_parse_from(["sudo", "--user"]); + assert!(cmd.is_err()) +} + +#[test] +fn login() { + let cmd = SudoOptions::try_parse_from(["sudo", "-i"]).unwrap(); + assert!(cmd.login); + + let cmd = SudoOptions::try_parse_from(["sudo", "--login"]).unwrap(); + assert!(cmd.login); +} + +#[test] +fn edit() { + let cmd = SudoAction::try_parse_from(["sudo", "-e", "filepath"]).unwrap(); + assert!(cmd.is_edit()); + + let cmd = SudoAction::try_parse_from(["sudo", "--edit", "filepath"]).unwrap(); + assert!(cmd.is_edit()); + + let res = SudoAction::try_parse_from(["sudo", "--edit"]); + assert!(res.is_err()); +} + +#[test] +fn help() { + let cmd = SudoAction::try_parse_from(["sudo", "-h"]).unwrap(); + assert!(cmd.is_help()); + + let cmd = SudoAction::try_parse_from(["sudo", "-bh"]); + assert!(cmd.is_err()); + + let cmd = SudoAction::try_parse_from(["sudo", "--help"]).unwrap(); + assert!(cmd.is_help()); +} + +#[test] +fn conflicting_arguments() { + let cmd = SudoAction::try_parse_from(["sudo", "-K", "-k"]); + assert!(cmd.is_err()); + + let cmd = SudoAction::try_parse_from(["sudo", "--remove-timestamp", "--reset-timestamp"]); + assert!(cmd.is_err()); + + let cmd = SudoAction::try_parse_from(["sudo", "-K"]).unwrap(); + assert!(cmd.is_remove_timestamp()); + + let cmd = SudoAction::try_parse_from(["sudo", "-k"]).unwrap(); + assert!(cmd.is_reset_timestamp()); +} + +#[test] +fn list() { + let valid: &[&[_]] = &[ + &["sudo", "--list"], + &["sudo", "-l"], + &["sudo", "-l", "true"], + &["sudo", "-l", "-U", "ferris"], + &["sudo", "-l", "-U", "ferris", "true"], + &["sudo", "-l", "-u", "ferris", "true"], + &["sudo", "-l", "-u", "ferris", "-U", "root", "true"], + ]; + + for args in valid { + let cmd = SudoAction::try_parse_from(args.iter().copied()).unwrap(); + assert!(cmd.is_list()); + } + + let invalid: &[&[_]] = &[ + &["sudo", "-l", "-u", "ferris"], + &["sudo", "-l", "-u", "ferris", "-U", "root"], + ]; + + for args in invalid { + let res = SudoAction::try_parse_from(args.iter().copied()); + assert!(res.is_err()) + } +} + +#[test] +fn validate() { + let cmd = SudoAction::try_parse_from(["sudo", "-v"]).unwrap(); + assert!(cmd.is_validate()); + + let cmd = SudoAction::try_parse_from(["sudo", "--validate"]).unwrap(); + assert!(cmd.is_validate()); +} + +#[test] +fn version() { + let cmd = SudoAction::try_parse_from(["sudo", "-V"]).unwrap(); + assert!(cmd.is_version()); + + let cmd = SudoAction::try_parse_from(["sudo", "--version"]).unwrap(); + assert!(cmd.is_version()); +} + +#[test] +fn run_reset_timestamp_command() { + let action = SudoAction::try_parse_from(["sudo", "-k", "true"]) + .unwrap() + .try_into_run() + .ok() + .unwrap(); + assert_eq!(["true"], action.positional_args.as_slice()); + assert!(action.reset_timestamp); +} + +#[test] +fn run_reset_timestamp_login() { + let action = SudoAction::try_parse_from(["sudo", "-k", "-i"]) + .unwrap() + .try_into_run() + .ok() + .unwrap(); + assert!(action.positional_args.is_empty()); + assert!(action.reset_timestamp); + assert!(action.login); +} + +#[test] +fn run_reset_timestamp_shell() { + let action = SudoAction::try_parse_from(["sudo", "-k", "-s"]) + .unwrap() + .try_into_run() + .ok() + .unwrap(); + assert!(action.positional_args.is_empty()); + assert!(action.reset_timestamp); + assert!(action.shell); +} + +#[test] +fn run_no_command() { + assert!(SudoAction::try_parse_from(["sudo", "-u", "root"]).is_err()); +} + +#[test] +fn run_login() { + assert!(SudoAction::try_parse_from(["sudo", "-i"]).unwrap().is_run()); +} + +#[test] +fn run_shell() { + assert!(SudoAction::try_parse_from(["sudo", "-s"]).unwrap().is_run()); +} diff -Nru rust-sudo-rs-0.2.1/src/sudo/env/environment.rs rust-sudo-rs-0.2.2/src/sudo/env/environment.rs --- rust-sudo-rs-0.2.1/src/sudo/env/environment.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/env/environment.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,297 @@ +use std::{ + collections::{hash_map::Entry, HashSet}, + ffi::{OsStr, OsString}, + os::unix::prelude::OsStrExt, +}; + +use crate::common::{CommandAndArguments, Context, Environment}; +use crate::sudoers::Policy; +use crate::system::PATH_MAX; + +use super::wildcard_match::wildcard_match; + +const PATH_MAILDIR: &str = env!("PATH_MAILDIR"); +const PATH_ZONEINFO: &str = env!("PATH_ZONEINFO"); +const PATH_DEFAULT: &str = env!("SUDO_PATH_DEFAULT"); + +/// check byte slice contains with given byte slice +fn contains_subsequence(haystack: &[u8], needle: &[u8]) -> bool { + haystack + .windows(needle.len()) + .any(|window| window == needle) +} + +/// Formats the command and arguments passed for the SUDO_COMMAND +/// environment variable. Limit the length to 4096 bytes to prevent +/// execve failure for very long argument vectors +fn format_command(command_and_arguments: &CommandAndArguments) -> OsString { + let mut formatted: OsString = command_and_arguments.command.clone().into(); + + for arg in &command_and_arguments.arguments { + if formatted.len() + arg.len() < 4096 { + formatted.push(" "); + formatted.push(arg); + } + } + + formatted +} + +/// Construct sudo-specific environment variables +fn add_extra_env( + context: &Context, + cfg: &impl Policy, + sudo_ps1: Option, + environment: &mut Environment, +) { + // current user + environment.insert("SUDO_COMMAND".into(), format_command(&context.command)); + environment.insert( + "SUDO_UID".into(), + context.current_user.uid.to_string().into(), + ); + environment.insert( + "SUDO_GID".into(), + context.current_user.gid.to_string().into(), + ); + environment.insert("SUDO_USER".into(), context.current_user.name.clone().into()); + // target user + if let Entry::Vacant(entry) = environment.entry("MAIL".into()) { + entry.insert(format!("{PATH_MAILDIR}/{}", context.target_user.name).into()); + } + // The current SHELL variable should determine the shell to run when -s is passed, if none set use passwd entry + environment.insert("SHELL".into(), context.target_user.shell.clone().into()); + // HOME' Set to the home directory of the target user if -i or -H are specified, env_reset or always_set_home are + // set in sudoers, or when the -s option is specified and set_home is set in sudoers. + // Since we always want to do env_reset -> always set HOME + if let Entry::Vacant(entry) = environment.entry("HOME".into()) { + entry.insert(context.target_user.home.clone().into()); + } + + match ( + environment.get(OsStr::new("LOGNAME")), + environment.get(OsStr::new("USER")), + ) { + // Set to the login name of the target user when the -i option is specified, + // when the set_logname option is enabled in sudoers, or when the env_reset option + // is enabled in sudoers (unless LOGNAME is present in the env_keep list). + // Since we always want to do env_reset -> always set these except when present in env + (None, None) => { + environment.insert("LOGNAME".into(), context.target_user.name.clone().into()); + environment.insert("USER".into(), context.target_user.name.clone().into()); + } + // LOGNAME should be set to the same value as USER if the latter is preserved. + (None, Some(user)) => { + environment.insert("LOGNAME".into(), user.clone()); + } + // USER should be set to the same value as LOGNAME if the latter is preserved. + (Some(logname), None) => { + environment.insert("USER".into(), logname.clone()); + } + (Some(_), Some(_)) => {} + } + + // Overwrite PATH when secure_path is set + if let Some(secure_path) = cfg.secure_path() { + // assign path by env path or secure_path configuration + environment.insert("PATH".into(), secure_path.into()); + } + // If the PATH and TERM variables are not preserved from the user's environment, they will be set to default value + if !environment.contains_key(OsStr::new("PATH")) { + // If the PATH variable is not set, it will be set to default value + environment.insert("PATH".into(), PATH_DEFAULT.into()); + } + // If the TERM variable is not preserved from the user's environment, it will be set to default value + if !environment.contains_key(OsStr::new("TERM")) { + environment.insert("TERM".into(), "unknown".into()); + } + // The SUDO_PS1 variable requires special treatment as the PS1 variable must be set in the + // target environment to the same value of SUDO_PS1 if the latter is set. + if let Some(sudo_ps1_value) = sudo_ps1 { + // set PS1 to the SUDO_PS1 value in the target environment + environment.insert("PS1".into(), sudo_ps1_value); + } +} + +/// Check a string only contains printable (non-space) characters +fn is_printable(input: &[u8]) -> bool { + input + .iter() + .all(|c| c.is_ascii_alphanumeric() || c.is_ascii_punctuation()) +} + +/// The TZ variable is considered unsafe if any of the following are true: +/// It consists of a fully-qualified path name, optionally prefixed with a colon (‘:’), that does not match the location of the zoneinfo directory. +/// It contains a .. path element. +/// It contains white space or non-printable characters. +/// It is longer than the value of PATH_MAX. +fn is_safe_tz(value: &[u8]) -> bool { + let check_value = if value.starts_with(&[b':']) { + &value[1..] + } else { + value + }; + + if check_value.starts_with(&[b'/']) { + if !PATH_ZONEINFO.is_empty() { + if !check_value.starts_with(PATH_ZONEINFO.as_bytes()) + || check_value.get(PATH_ZONEINFO.len()) != Some(&b'/') + { + return false; + } + } else { + return false; + } + } + + !contains_subsequence(check_value, "..".as_bytes()) + && is_printable(check_value) + && check_value.len() < PATH_MAX as usize +} + +/// Check whether the needle exists in a haystack, in which the haystack is a list of patterns, possibly containing wildcards +fn in_table(needle: &OsStr, haystack: &HashSet) -> bool { + haystack + .iter() + .any(|pattern| wildcard_match(needle.as_bytes(), pattern.as_bytes())) +} + +/// Determine whether a specific environment variable should be kept +fn should_keep(key: &OsStr, value: &OsStr, cfg: &impl Policy) -> bool { + if value.as_bytes().starts_with("()".as_bytes()) { + return false; + } + + if key == "TZ" { + return in_table(key, cfg.env_keep()) + || (in_table(key, cfg.env_check()) && is_safe_tz(value.as_bytes())); + } + + if in_table(key, cfg.env_check()) { + return !value.as_bytes().iter().any(|c| *c == b'%' || *c == b'/'); + } + + in_table(key, cfg.env_keep()) +} + +/// Construct the final environment from the current one and a sudo context +/// see for the original implementation +/// see for the original documentation +/// +/// The HOME, MAIL, SHELL, LOGNAME and USER environment variables are initialized based on the target user +/// and the SUDO_* variables are set based on the invoking user. +/// +/// Additional variables, such as DISPLAY, PATH and TERM, are preserved from the invoking user's +/// environment if permitted by the env_check, or env_keep options +/// +/// If the PATH and TERM variables are not preserved from the user's environment, they will be set to default value +/// +/// Environment variables with a value beginning with ‘()’ are removed +pub fn get_target_environment( + current_env: Environment, + additional_env: Environment, + context: &Context, + settings: &impl Policy, +) -> Environment { + let mut environment = Environment::default(); + + // retrieve SUDO_PS1 value to set a PS1 value as additional environment + let sudo_ps1 = current_env.get(OsStr::new("SUDO_PS1")).cloned(); + + // variables preserved from the invoking user's environment by the + // env_keep list take precedence over those in the PAM environment + environment.extend(additional_env); + + environment.extend( + current_env + .into_iter() + .filter(|(key, value)| should_keep(key, value, settings)), + ); + + add_extra_env(context, settings, sudo_ps1, &mut environment); + + environment +} + +#[cfg(test)] +mod tests { + use super::{is_safe_tz, should_keep, PATH_ZONEINFO}; + use crate::sudoers::Policy; + use std::{collections::HashSet, ffi::OsStr}; + + struct TestConfiguration { + keep: HashSet, + check: HashSet, + } + + impl Policy for TestConfiguration { + fn env_keep(&self) -> &HashSet { + &self.keep + } + + fn env_check(&self) -> &HashSet { + &self.check + } + + fn secure_path(&self) -> Option { + None + } + + fn use_pty(&self) -> bool { + true + } + } + + #[test] + fn test_filtering() { + let config = TestConfiguration { + keep: HashSet::from(["AAP".to_string(), "NOOT".to_string()]), + check: HashSet::from(["MIES".to_string(), "TZ".to_string()]), + }; + + let check_should_keep = |key: &str, value: &str, expected: bool| { + assert_eq!( + should_keep(OsStr::new(key), OsStr::new(value), &config), + expected, + "{} should {}", + key, + if expected { "be kept" } else { "not be kept" } + ); + }; + + check_should_keep("AAP", "FOO", true); + check_should_keep("MIES", "BAR", true); + check_should_keep("AAP", "()=foo", false); + check_should_keep("TZ", "Europe/Amsterdam", true); + check_should_keep("TZ", "../Europe/Berlin", false); + check_should_keep("MIES", "FOO/BAR", false); + check_should_keep("MIES", "FOO%", false); + } + + #[allow(clippy::useless_format)] + #[allow(clippy::bool_assert_comparison)] + #[test] + fn test_tzinfo() { + assert_eq!(is_safe_tz("Europe/Amsterdam".as_bytes()), true); + assert_eq!( + is_safe_tz(format!("{PATH_ZONEINFO}/Europe/London").as_bytes()), + true + ); + assert_eq!( + is_safe_tz(format!(":{PATH_ZONEINFO}/Europe/Amsterdam").as_bytes()), + true + ); + assert_eq!( + is_safe_tz(format!("/schaap/Europe/Amsterdam").as_bytes()), + false + ); + assert_eq!( + is_safe_tz(format!("{PATH_ZONEINFO}/../Europe/London").as_bytes()), + false + ); + assert_eq!( + is_safe_tz(format!("{PATH_ZONEINFO}/../Europe/London").as_bytes()), + false + ); + } +} diff -Nru rust-sudo-rs-0.2.1/src/sudo/env/mod.rs rust-sudo-rs-0.2.2/src/sudo/env/mod.rs --- rust-sudo-rs-0.2.1/src/sudo/env/mod.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/env/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,7 @@ +#![forbid(unsafe_code)] + +pub mod environment; +pub mod wildcard_match; + +#[cfg(test)] +mod tests; diff -Nru rust-sudo-rs-0.2.1/src/sudo/env/tests.rs rust-sudo-rs-0.2.2/src/sudo/env/tests.rs --- rust-sudo-rs-0.2.1/src/sudo/env/tests.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/env/tests.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,182 @@ +use crate::common::resolve::CurrentUser; +use crate::common::{CommandAndArguments, Context, Environment}; +use crate::sudo::{ + cli::{SudoAction, SudoRunOptions}, + env::environment::get_target_environment, +}; +use crate::system::{Group, Hostname, Process, User}; +use std::collections::{HashMap, HashSet}; + +const TESTS: &str = " +> env + FOO=BAR + HOME=/home/test + HOSTNAME=test-ubuntu + LANG=en_US.UTF-8 + LANGUAGE=en_US.UTF-8 + LC_ALL=en_US.UTF-8 + LS_COLORS=cd=40;33;01:*.jpg=01;35:*.mp3=00;36: + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + PWD=/home/test + SHLVL=0 + TERM=xterm + _=/usr/bin/sudo +> sudo env + HOSTNAME=test-ubuntu + LANG=en_US.UTF-8 + LANGUAGE=en_US.UTF-8 + LC_ALL=en_US.UTF-8 + LS_COLORS=cd=40;33;01:*.jpg=01;35:*.mp3=00;36: + MAIL=/var/mail/root + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + SHELL=/bin/bash + SUDO_COMMAND=/usr/bin/env + SUDO_GID=1000 + SUDO_UID=1000 + SUDO_USER=test + HOME=/root + LOGNAME=root + USER=root + TERM=xterm +> sudo -u test env + HOSTNAME=test-ubuntu + LANG=en_US.UTF-8 + LANGUAGE=en_US.UTF-8 + LC_ALL=en_US.UTF-8 + LS_COLORS=cd=40;33;01:*.jpg=01;35:*.mp3=00;36: + MAIL=/var/mail/test + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + SHELL=/bin/sh + SUDO_COMMAND=/usr/bin/env + SUDO_GID=1000 + SUDO_UID=1000 + SUDO_USER=test + HOME=/home/test + LOGNAME=test + USER=test + TERM=xterm +"; + +fn parse_env_commands(input: &str) -> Vec<(&str, Environment)> { + input + .trim() + .split("> ") + .filter(|l| !l.is_empty()) + .map(|e| { + let (cmd, vars) = e.split_once('\n').unwrap(); + + let vars: Environment = vars + .lines() + .map(|line| line.trim().split_once('=').unwrap()) + .map(|(k, v)| (k.into(), v.into())) + .collect(); + + (cmd, vars) + }) + .collect() +} + +fn create_test_context(sudo_options: &SudoRunOptions) -> Context { + let path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(); + let command = + CommandAndArguments::build_from_args(None, sudo_options.positional_args.clone(), &path); + + let current_user = CurrentUser::fake(User { + uid: 1000, + gid: 1000, + + name: "test".into(), + gecos: String::new(), + home: "/home/test".into(), + shell: "/bin/sh".into(), + passwd: String::new(), + groups: vec![], + }); + + let current_group = Group { + gid: 1000, + name: "test".to_string(), + passwd: String::new(), + members: Vec::new(), + }; + + let root_user = User { + uid: 0, + gid: 0, + name: "root".into(), + gecos: String::new(), + home: "/root".into(), + shell: "/bin/bash".into(), + passwd: String::new(), + groups: vec![], + }; + + let root_group = Group { + gid: 0, + name: "root".to_string(), + passwd: String::new(), + members: Vec::new(), + }; + + Context { + hostname: Hostname::fake("test-ubuntu"), + command, + current_user: current_user.clone(), + target_user: if sudo_options.user.as_deref() == Some("test") { + current_user.into() + } else { + root_user + }, + target_group: if sudo_options.user.as_deref() == Some("test") { + current_group + } else { + root_group + }, + launch: crate::common::context::LaunchType::Direct, + chdir: sudo_options.chdir.clone(), + stdin: sudo_options.stdin, + non_interactive: sudo_options.non_interactive, + process: Process::new(), + use_session_records: false, + use_pty: true, + } +} + +fn environment_to_set(environment: Environment) -> HashSet { + HashSet::from_iter( + environment + .iter() + .map(|(k, v)| format!("{}={}", k.to_str().unwrap(), v.to_str().unwrap())), + ) +} + +#[test] +fn test_environment_variable_filtering() { + let mut parts = parse_env_commands(TESTS); + let initial_env = parts.remove(0).1; + + for (cmd, expected_env) in parts { + let options = SudoAction::try_parse_from(cmd.split_whitespace()) + .unwrap() + .try_into_run() + .ok() + .unwrap(); + let settings = crate::sudoers::Judgement::default(); + let context = create_test_context(&options); + let resulting_env = + get_target_environment(initial_env.clone(), HashMap::new(), &context, &settings); + + let resulting_env = environment_to_set(resulting_env); + let expected_env = environment_to_set(expected_env); + let mut diff = resulting_env + .symmetric_difference(&expected_env) + .collect::>(); + + diff.sort(); + + assert!( + diff.is_empty(), + "\"{cmd}\" results in an environment mismatch:\n{diff:#?}", + ); + } +} diff -Nru rust-sudo-rs-0.2.1/src/sudo/env/wildcard_match.rs rust-sudo-rs-0.2.2/src/sudo/env/wildcard_match.rs --- rust-sudo-rs-0.2.1/src/sudo/env/wildcard_match.rs 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/env/wildcard_match.rs 2006-07-24 01:21:28.000000000 +0000 @@ -0,0 +1,86 @@ +/// Match a test input with a pattern +/// Only wildcard characters (*) in the pattern string have a special meaning: they match on zero or more characters +pub(super) fn wildcard_match(test: &[u8], pattern: &[u8]) -> bool { + let mut test_index = 0; + let mut pattern_index = 0; + let mut last_star = None; + + loop { + match (pattern.get(pattern_index), test.get(test_index)) { + (Some(p), Some(t)) => { + if *p == b'*' { + pattern_index += 1; + last_star = Some((test_index, pattern_index)); + } else if p == t { + pattern_index += 1; + test_index += 1; + } else if let Some((t_index, p_index)) = last_star { + test_index = t_index + 1; + pattern_index = p_index; + last_star = Some((test_index, pattern_index)); + } else { + return false; + } + } + (None, Some(_)) => { + if let Some((t_index, p_index)) = last_star { + test_index = t_index + 1; + pattern_index = p_index; + last_star = Some((test_index, pattern_index)); + } else { + return false; + } + } + (Some(b'*'), None) => { + pattern_index += 1; + } + (None, None) => { + return true; + } + _ => { + return false; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::wildcard_match; + + #[test] + fn test_wildcard_match() { + let tests = vec![ + ("foo bar", "foo *", true), + ("foo bar", "foo ba*", true), + ("foo bar", "foo *ar", true), + ("foo bar", "foo *r", true), + ("foo bar", "foo *ab", false), + ("foo bar", "foo r*", false), + ("foo bar", "*oo bar", true), + ("foo bar", "*f* bar", true), + ("foo bar", "*f bar", false), + ("foo ", "foo *", true), + ("foo", "foo *", false), + ("foo", "foo*", true), + ("foo bar", "f*******r", true), + ("foo******bar", "f*r", true), + ("foo********bar", "foo bar", false), + ("#%^$V@#TYH%&rot13%#@$%#$%", "#%^$V@#*t13%#@$%#$%", true), + ("#%^$V@#TYH%&rot13%#@$%#$%", "*%^*%&rot*%#$%", true), + ("#%^$V@#TYH%&rot13%#@$%#$%", "#%^$V@#TYH%&r*%#@$#$%", false), + ("#%^$V@#TYH%&rot13%#@$%#$%", "#%^$V@#*******@$%#$%", true), + ]; + + for (test, pattern, expected) in tests.into_iter() { + assert_eq!( + wildcard_match(test.as_bytes(), pattern.as_bytes()), + expected, + "\"{}\" {} match {}", + test, + if expected { "should" } else { "should not" }, + pattern + ); + } + } +} diff -Nru rust-sudo-rs-0.2.1/src/sudo/mod.rs rust-sudo-rs-0.2.2/src/sudo/mod.rs --- rust-sudo-rs-0.2.1/src/sudo/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,21 +1,27 @@ #![forbid(unsafe_code)] -use crate::cli::{help, SudoAction, SudoOptions}; -use crate::common::{resolve::resolve_current_user, Context, Error}; +use crate::common::resolve::CurrentUser; +use crate::common::{Context, Error}; use crate::log::dev_info; use crate::system::timestamp::RecordScope; use crate::system::User; use crate::system::{time::Duration, timestamp::SessionRecordFile, Process}; +use cli::help; +#[cfg(test)] +pub use cli::SudoAction; +#[cfg(not(test))] +use cli::SudoAction; use pam::PamAuthenticator; use pipeline::{Pipeline, PolicyPlugin}; -use std::env; use std::path::Path; +mod cli; mod diagnostic; +mod env; mod pam; mod pipeline; -const VERSION: &str = env!("CARGO_PKG_VERSION"); +const VERSION: &str = std::env!("CARGO_PKG_VERSION"); fn candidate_sudoers_file() -> &'static Path { let pb_rs: &'static Path = Path::new("/etc/sudoers-rs"); @@ -41,8 +47,14 @@ let (sudoers, syntax_errors) = crate::sudoers::Sudoers::open(sudoers_path) .map_err(|e| Error::Configuration(format!("{e}")))?; - for crate::sudoers::Error(pos, error) in syntax_errors { - diagnostic::diagnostic!("{error}", sudoers_path @ pos); + for crate::sudoers::Error { + source, + location, + message, + } in syntax_errors + { + let path = source.as_deref().unwrap_or(sudoers_path); + diagnostic::diagnostic!("{message}", path @ location); } Ok(sudoers) @@ -54,7 +66,7 @@ context: &Context, ) -> Result { Ok(pre.check( - &context.current_user, + &*context.current_user, &context.hostname, crate::sudoers::Request { user: &context.target_user, @@ -79,45 +91,46 @@ }; // parse cli options - match SudoOptions::from_env() { - Ok(options) => match options.action { - SudoAction::Help => { + match SudoAction::from_env() { + Ok(action) => match action { + SudoAction::Help(_) => { eprintln_ignore_io_error!("{}", help::long_help_message()); std::process::exit(0); } - SudoAction::Version => { + SudoAction::Version(_) => { eprintln_ignore_io_error!("sudo-rs {VERSION}"); std::process::exit(0); } - SudoAction::RemoveTimestamp => { - let user = resolve_current_user()?; + SudoAction::RemoveTimestamp(_) => { + let user = CurrentUser::resolve()?; let mut record_file = - SessionRecordFile::open_for_user(user.uid, Duration::seconds(0))?; + SessionRecordFile::open_for_user(&user, Duration::seconds(0))?; record_file.reset()?; Ok(()) } - SudoAction::ResetTimestamp => { + SudoAction::ResetTimestamp(_) => { if let Some(scope) = RecordScope::for_process(&Process::new()) { - let user = resolve_current_user()?; + let user = CurrentUser::resolve()?; let mut record_file = - SessionRecordFile::open_for_user(user.uid, Duration::seconds(0))?; + SessionRecordFile::open_for_user(&user, Duration::seconds(0))?; record_file.disable(scope, None)?; } Ok(()) } - SudoAction::Validate => pipeline.run_validate(options), - SudoAction::Run(ref cmd) => { + SudoAction::Validate(options) => pipeline.run_validate(options), + SudoAction::Run(options) => { // special case for when no command is given - if cmd.is_empty() && !options.shell && !options.login { + if options.positional_args.is_empty() && !options.shell && !options.login { eprintln_ignore_io_error!("{}", help::USAGE_MSG); std::process::exit(1); } else { pipeline.run(options) } } - SudoAction::List(_) => pipeline.run_list(options), + SudoAction::List(options) => pipeline.run_list(options), SudoAction::Edit(_) => { - unimplemented!(); + eprintln_ignore_io_error!("error: `--edit` flag has not yet been implemented"); + std::process::exit(1); } }, Err(e) => { diff -Nru rust-sudo-rs-0.2.1/src/sudo/pipeline/list.rs rust-sudo-rs-0.2.2/src/sudo/pipeline/list.rs --- rust-sudo-rs-0.2.1/src/sudo/pipeline/list.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/pipeline/list.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,10 +1,9 @@ use std::{borrow::Cow, ops::ControlFlow, path::Path}; use crate::{ - cli::{SudoAction, SudoOptions}, common::{Context, Error}, pam::CLIConverser, - sudo::{pam::PamAuthenticator, SudoersPolicy}, + sudo::{cli::SudoListOptions, pam::PamAuthenticator, SudoersPolicy}, sudoers::{Authorization, ListRequest, Policy, Request, Sudoers}, system::User, }; @@ -12,24 +11,21 @@ use super::{Pipeline, PolicyPlugin}; impl Pipeline> { - pub(in crate::sudo) fn run_list(mut self, cmd_opts: SudoOptions) -> Result<(), Error> { - let verbose_list_mode = cmd_opts.verbose_list_mode(); + pub(in crate::sudo) fn run_list(mut self, cmd_opts: SudoListOptions) -> Result<(), Error> { + let verbose_list_mode = cmd_opts.list.is_verbose(); let other_user = cmd_opts .other_user .as_ref() .map(|username| { - User::from_name(username)?.ok_or_else(|| Error::UserNotFound(username.clone())) + User::from_name(username.as_cstr())? + .ok_or_else(|| Error::UserNotFound(username.clone().into())) }) .transpose()?; - let original_command = if let SudoAction::List(args) = &cmd_opts.action { - args.first().cloned() - } else { - panic!("called `Pipeline::run_list` with a SudoAction other than `List`") - }; + let original_command = cmd_opts.positional_args.first().cloned(); let sudoers = self.policy.init()?; - let context = super::build_context(cmd_opts, &sudoers)?; + let context = super::build_context(cmd_opts.into(), &sudoers)?; if original_command.is_some() && !context.command.resolved { return Err(Error::CommandNotFound(context.command.command)); @@ -83,7 +79,7 @@ target_group: &context.target_group, }; let judgement = - sudoers.check_list_permission(&context.current_user, &context.hostname, list_request); + sudoers.check_list_permission(&*context.current_user, &context.hostname, list_request); match judgement.authorization() { Authorization::Allowed(auth) => { self.auth_and_update_record_file(context, auth)?; diff -Nru rust-sudo-rs-0.2.1/src/sudo/pipeline.rs rust-sudo-rs-0.2.2/src/sudo/pipeline.rs --- rust-sudo-rs-0.2.1/src/sudo/pipeline.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudo/pipeline.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,11 +1,13 @@ use std::ffi::OsStr; use std::process::exit; -use crate::cli::SudoOptions; -use crate::common::{resolve::expand_tilde_in_path, Context, Environment, Error}; -use crate::env::environment; +use super::cli::{SudoRunOptions, SudoValidateOptions}; +use crate::common::context::OptionsForContext; +use crate::common::resolve::CurrentUser; +use crate::common::{Context, Environment, Error}; use crate::exec::{ExecOutput, ExitReason}; use crate::log::{auth_info, auth_warn}; +use crate::sudo::env::environment; use crate::sudo::Duration; use crate::sudoers::{Authorization, AuthorizationAllowed, DirChange, Policy, PreJudgementPolicy}; use crate::system::interface::UserId; @@ -40,9 +42,20 @@ } impl Pipeline { - pub fn run(mut self, cmd_opts: SudoOptions) -> Result<(), Error> { + pub fn run(mut self, cmd_opts: SudoRunOptions) -> Result<(), Error> { + if !cmd_opts.env_var_list.is_empty() { + eprintln_ignore_io_error!( + "warning: CLI-level env var list has not yet been implemented and will be ignored" + ) + } + if !cmd_opts.preserve_env.is_nothing() { + eprintln_ignore_io_error!( + "warning: `--preserve-env` has not yet been implemented and will be ignored" + ) + } + let pre = self.policy.init()?; - let mut context = build_context(cmd_opts, &pre)?; + let mut context = build_context(cmd_opts.into(), &pre)?; let policy = self.policy.judge(pre, &context)?; let authorization = policy.authorization(); @@ -74,7 +87,7 @@ log_command_execution(&context); crate::exec::run_command(&context, target_env) - .map_err(|io_error| Error::IoError(Some(context.command.command), io_error)) + .map_err(|io_error| Error::Io(Some(context.command.command), io_error)) } else { Err(Error::CommandNotFound(context.command.command)) }; @@ -99,9 +112,9 @@ Ok(()) } - pub fn run_validate(mut self, cmd_opts: SudoOptions) -> Result<(), Error> { + pub fn run_validate(mut self, cmd_opts: SudoValidateOptions) -> Result<(), Error> { let pre = self.policy.init()?; - let context = build_context(cmd_opts, &pre)?; + let context = build_context(cmd_opts.into(), &pre)?; match pre.validate_authorization() { Authorization::Forbidden => { @@ -133,7 +146,7 @@ context.use_session_records, scope, context.current_user.uid, - context.current_user.uid, + &context.current_user, prior_validity, ); self.authenticator.init(context)?; @@ -162,20 +175,20 @@ match policy.chdir() { DirChange::Any => {} DirChange::Strict(optdir) => { - if context.chdir.is_some() { + if let Some(chdir) = &context.chdir { return Err(Error::ChDirNotAllowed { - chdir: context.chdir.clone().unwrap(), + chdir: chdir.clone(), command: context.command.command.clone(), }); } else { - context.chdir = optdir.map(std::path::PathBuf::from) + context.chdir = optdir.cloned(); } } } // expand tildes in the path with the users home directory if let Some(dir) = context.chdir.take() { - context.chdir = Some(expand_tilde_in_path(&context.target_user.name, dir)?) + context.chdir = Some(dir.expand_tilde_in_path(&context.target_user.name)?) } // override the default pty behaviour if indicated @@ -187,7 +200,10 @@ } } -fn build_context(cmd_opts: SudoOptions, pre: &dyn PreJudgementPolicy) -> Result { +fn build_context( + cmd_opts: OptionsForContext, + pre: &dyn PreJudgementPolicy, +) -> Result { let secure_path: String = pre .secure_path() .unwrap_or_else(|| std::env::var("PATH").unwrap_or_default()); @@ -201,7 +217,7 @@ use_session_records: bool, record_for: Option, auth_uid: UserId, - current_user: UserId, + current_user: &CurrentUser, prior_validity: Duration, ) -> AuthStatus { if !must_policy_authenticate { diff -Nru rust-sudo-rs-0.2.1/src/sudoers/ast.rs rust-sudo-rs-0.2.2/src/sudoers/ast.rs --- rust-sudo-rs-0.2.1/src/sudoers/ast.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudoers/ast.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,6 +1,7 @@ use super::ast_names::UserFriendly; use super::basic_parser::*; use super::tokens::*; +use crate::common::SudoString; use crate::common::{ HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2, HARDENED_ENUM_VALUE_3, HARDENED_ENUM_VALUE_4, @@ -28,6 +29,15 @@ Qualified::Forbid(item) => Qualified::Allow(item), } } + + #[cfg(test)] + pub fn as_allow(&self) -> Option<&T> { + if let Self::Allow(v) = self { + Some(v) + } else { + None + } + } } /// Type aliases; many items can be replaced by ALL, aliases, and negated. @@ -37,7 +47,7 @@ /// An identifier is a name or a #number #[cfg_attr(test, derive(Clone, Debug, PartialEq, Eq))] pub enum Identifier { - Name(String), + Name(SudoString), ID(u32), } @@ -133,6 +143,51 @@ LineComment = HARDENED_ENUM_VALUE_4, } +impl Sudo { + #[cfg(test)] + pub fn is_spec(&self) -> bool { + matches!(self, Self::Spec(..)) + } + + #[cfg(test)] + pub fn is_decl(&self) -> bool { + matches!(self, Self::Decl(..)) + } + + #[cfg(test)] + pub fn is_line_comment(&self) -> bool { + matches!(self, Self::LineComment) + } + + #[cfg(test)] + pub fn is_include(&self) -> bool { + matches!(self, Self::Include(..)) + } + + #[cfg(test)] + pub fn is_include_dir(&self) -> bool { + matches!(self, Self::IncludeDir(..)) + } + + #[cfg(test)] + pub fn as_include(&self) -> &str { + if let Self::Include(v) = self { + v + } else { + panic!() + } + } + + #[cfg(test)] + pub fn as_spec(&self) -> Option<&PermissionSpec> { + if let Self::Spec(v) = self { + Some(v) + } else { + None + } + } +} + /// grammar: /// ```text /// identifier = name @@ -190,7 +245,7 @@ fn parse_meta( stream: &mut impl CharStream, - embed: impl FnOnce(String) -> T, + embed: impl FnOnce(SudoString) -> T, ) -> Parsed> { if let Some(meta) = try_nonterminal(stream)? { make(match meta { diff -Nru rust-sudo-rs-0.2.1/src/sudoers/mod.rs rust-sudo-rs-0.2.2/src/sudoers/mod.rs --- rust-sudo-rs-0.2.1/src/sudoers/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudoers/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -15,8 +15,8 @@ use crate::common::resolve::resolve_path; use crate::log::auth_warn; -use crate::system::can_execute; use crate::system::interface::{UnixGroup, UnixUser}; +use crate::system::{self, can_execute}; use ast::*; use tokens::*; @@ -25,7 +25,11 @@ /// Export some necessary symbols from modules pub use ast::TextEnum; -pub struct Error(pub Option, pub String); +pub struct Error { + pub source: Option, + pub location: Option, + pub message: String, +} #[derive(Default)] pub struct Sudoers { @@ -77,7 +81,7 @@ pub fn check, Group: UnixGroup>( &self, am_user: &User, - on_host: &str, + on_host: &system::Hostname, request: Request, ) -> Judgement { // exception: if user is root or does not switch users, NOPASSWD is implied @@ -100,7 +104,7 @@ pub fn check_list_permission, Group: UnixGroup>( &self, invoking_user: &User, - hostname: &str, + hostname: &system::Hostname, request: ListRequest, ) -> Judgement { // exception: if user is root or does not switch users, NOPASSWD is implied @@ -142,7 +146,7 @@ fn matching_user_specs<'a: 'b + 'c, 'b: 'c, 'c, User: UnixUser + PartialEq>( &'a self, invoking_user: &'b User, - hostname: &'c str, + hostname: &'c system::Hostname, ) -> impl Iterator, (Tag, &'a Spec))> + 'b> + 'c { let Self { rules, aliases, .. } = self; @@ -166,7 +170,7 @@ pub fn matching_entries<'a, User: UnixUser + PartialEq>( &'a self, invoking_user: &User, - hostname: &str, + hostname: &system::Hostname, ) -> Vec> { // NOTE this method MUST NOT perform any filtering that `Self::check` does not do to // ensure `sudo $command` and `sudo --list` use the same permission checking logic @@ -276,7 +280,7 @@ #[derive(Default)] pub(super) struct AliasTable { user: VecOrd>, - host: VecOrd>, + host: VecOrd>, cmnd: VecOrd>, runas: VecOrd>, } @@ -299,7 +303,7 @@ fn check_permission, Group: UnixGroup>( sudoers: &Sudoers, am_user: &User, - on_host: &str, + on_host: &system::Hostname, request: Request, ) -> Option { let cmdline = (request.command, request.arguments); @@ -431,7 +435,7 @@ fn match_user(user: &impl UnixUser) -> impl Fn(&UserSpecifier) -> bool + '_ { move |spec| match spec { UserSpecifier::User(id) => match_identifier(user, id), - UserSpecifier::Group(Identifier::Name(name)) => user.in_group_by_name(name), + UserSpecifier::Group(Identifier::Name(name)) => user.in_group_by_name(name.as_cstr()), UserSpecifier::Group(Identifier::ID(num)) => user.in_group_by_gid(*num), _ => todo!(), // nonunix-groups, netgroups, etc. } @@ -444,7 +448,7 @@ fn match_group(group: &impl UnixGroup) -> impl Fn(&Identifier) -> bool + '_ { move |id| match id { Identifier::ID(num) => group.as_gid() == *num, - Identifier::Name(name) => group.try_as_name().map_or(false, |s| s == name), + Identifier::Name(name) => group.try_as_name().map_or(false, |s| name == s), } } @@ -587,12 +591,19 @@ } impl Sudoers { - fn include(&mut self, path: &Path, diagnostics: &mut Vec, count: &mut u8) { + fn include( + &mut self, + parent: &Path, + path: &Path, + diagnostics: &mut Vec, + count: &mut u8, + ) { if *count >= INCLUDE_LIMIT { - diagnostics.push(Error( - None, - format!("include file limit reached opening '{}'", path.display()), - )) + diagnostics.push(Error { + source: Some(parent.to_owned()), + location: None, + message: format!("include file limit reached opening '{}'", path.display()), + }) // FIXME: this will cause an error in `visudo` if we open a non-privileged sudoers file // that includes another non-privileged sudoer files. } else { @@ -609,7 +620,11 @@ e.to_string() }; - diagnostics.push(Error(None, message)) + diagnostics.push(Error { + source: Some(parent.to_owned()), + location: None, + message, + }) } } } @@ -641,6 +656,7 @@ } Sudo::Include(path) => self.include( + cur_path, &resolve_relative(cur_path, path), diagnostics, safety_count, @@ -648,18 +664,20 @@ Sudo::IncludeDir(path) => { if path.contains("%h") { - diagnostics.push(Error( - None, - format!("cannot open sudoers file {path}: percent escape %h in includedir is unsupported"))); + diagnostics.push(Error{ + source: Some(cur_path.to_owned()), + location: None, + message: format!("cannot open sudoers file {path}: percent escape %h in includedir is unsupported")}); continue; } let path = resolve_relative(cur_path, path); let Ok(files) = std::fs::read_dir(&path) else { - diagnostics.push(Error( - None, - format!("cannot open sudoers file {}", path.display()), - )); + diagnostics.push(Error { + source: Some(cur_path.to_owned()), + location: None, + message: format!("cannot open sudoers file {}", path.display()), + }); continue; }; let mut safe_files = files @@ -675,14 +693,16 @@ .collect::>(); safe_files.sort(); for file in safe_files { - self.include(file.as_ref(), diagnostics, safety_count) + self.include(cur_path, file.as_ref(), diagnostics, safety_count) } } }, - Err(basic_parser::Status::Fatal(pos, error)) => { - diagnostics.push(Error(Some(pos), error)) - } + Err(basic_parser::Status::Fatal(pos, message)) => diagnostics.push(Error { + source: Some(cur_path.to_owned()), + location: Some(pos), + message, + }), Err(_) => panic!("internal parser error"), } } @@ -756,7 +776,11 @@ impl Visitor<'_, T> { fn complain(&mut self, text: String) { - self.diagnostics.push(Error(None, text)) + self.diagnostics.push(Error { + source: None, + location: None, + message: text, + }) } fn visit(&mut self, pos: usize) { diff -Nru rust-sudo-rs-0.2.1/src/sudoers/policy.rs rust-sudo-rs-0.2.2/src/sudoers/policy.rs --- rust-sudo-rs-0.2.1/src/sudoers/policy.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudoers/policy.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,7 +1,7 @@ use super::Sudoers; use super::Judgement; -use crate::common::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1}; +use crate::common::{SudoPath, HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1}; use crate::system::time::Duration; /// Data types and traits that represent what the "terms and conditions" are after a succesful /// permission check. @@ -9,7 +9,6 @@ /// The trait definitions can be part of some global crate in the future, if we support more /// than just the sudoers file. use std::collections::HashSet; -use std::path::Path; pub trait Policy { fn authorization(&self) -> Authorization { @@ -47,7 +46,7 @@ #[cfg_attr(test, derive(Debug, PartialEq))] #[repr(u32)] pub enum DirChange<'a> { - Strict(Option<&'a Path>) = HARDENED_ENUM_VALUE_0, + Strict(Option<&'a SudoPath>) = HARDENED_ENUM_VALUE_0, Any = HARDENED_ENUM_VALUE_1, } @@ -165,8 +164,8 @@ judge.mod_flag(|tag| tag.cwd = Some(ChDir::Any)); assert_eq!(judge.chdir(), DirChange::Any); judge.mod_flag(|tag| tag.cwd = Some(ChDir::Path("/usr".into()))); - assert_eq!(judge.chdir(), (DirChange::Strict(Some(Path::new("/usr"))))); + assert_eq!(judge.chdir(), (DirChange::Strict(Some(&"/usr".into())))); judge.mod_flag(|tag| tag.cwd = Some(ChDir::Path("/bin".into()))); - assert_eq!(judge.chdir(), (DirChange::Strict(Some(Path::new("/bin"))))); + assert_eq!(judge.chdir(), (DirChange::Strict(Some(&"/bin".into())))); } } diff -Nru rust-sudo-rs-0.2.1/src/sudoers/test/mod.rs rust-sudo-rs-0.2.2/src/sudoers/test/mod.rs --- rust-sudo-rs-0.2.1/src/sudoers/test/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudoers/test/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,3 +1,5 @@ +use std::ffi::CStr; + use super::ast; use super::*; use basic_parser::{parse_eval, parse_lines, parse_string}; @@ -22,8 +24,8 @@ dummy_cksum(self.0) == uid } - fn in_group_by_name(&self, name: &str) -> bool { - self.has_name(name) + fn in_group_by_name(&self, name: &CStr) -> bool { + self.has_name(name.to_str().unwrap()) } fn in_group_by_gid(&self, gid: u32) -> bool { @@ -62,15 +64,22 @@ } // alternative to parse_eval, but goes through sudoer! directly +#[must_use] fn parse_line(s: &str) -> Sudo { sudoer![s].next().unwrap().unwrap() } +/// Returns `None` if a syntax error is encountered +fn try_parse_line(s: &str) -> Option { + parse_lines(&mut [s, ""].join("").chars().peekable()) + .into_iter() + .next()? + .ok() +} + #[test] fn ambiguous_spec() { - let Sudo::Spec(_) = parse_eval::("marc, User_Alias ALL = ALL") else { - todo!() - }; + assert!(parse_eval::("marc, User_Alias ALL = ALL").is_spec()); } #[test] @@ -85,7 +94,7 @@ let (Sudoers { rules,aliases,settings }, _) = analyze(Path::new("/etc/fakesudoers"), sudoer![$($sudo),*]); let cmdvec = $command.split_whitespace().map(String::from).collect::>(); let req = Request { user: $req.0, group: $req.1, command: &realpath(cmdvec[0].as_ref()), arguments: &cmdvec[1..].to_vec() }; - assert_eq!(Sudoers { rules, aliases, settings }.check(&Named($user), $server, req).flags, None); + assert_eq!(Sudoers { rules, aliases, settings }.check(&Named($user), &system::Hostname::fake($server), req).flags, None); } } @@ -94,7 +103,7 @@ let (Sudoers { rules,aliases,settings }, _) = analyze(Path::new("/etc/fakesudoers"), sudoer![$($sudo),*]); let cmdvec = $command.split_whitespace().map(String::from).collect::>(); let req = Request { user: $req.0, group: $req.1, command: &realpath(cmdvec[0].as_ref()), arguments: &cmdvec[1..].to_vec() }; - let result = Sudoers { rules, aliases, settings }.check(&Named($user), $server, req).flags; + let result = Sudoers { rules, aliases, settings }.check(&Named($user), &system::Hostname::fake($server), req).flags; assert!(!result.is_none()); $( let result = result.unwrap(); @@ -342,61 +351,27 @@ #[test] // the overloading of '#' causes a lot of issues fn hashsign_test() { - let Sudo::Spec(_) = parse_line("#42 ALL=ALL") else { - panic!() - }; - let Sudo::Spec(_) = parse_line("ALL ALL=(#42) ALL") else { - panic!() - }; - let Sudo::Spec(_) = parse_line("ALL ALL=(%#42) ALL") else { - panic!() - }; - let Sudo::Spec(_) = parse_line("ALL ALL=(:#42) ALL") else { - panic!() - }; - let Sudo::Decl(_) = parse_line("User_Alias FOO=#42, %#0, #3") else { - panic!() - }; - let Sudo::LineComment = parse_line("") else { - panic!() - }; - let Sudo::LineComment = parse_line("#this is a comment") else { - panic!() - }; - let Sudo::Include(_) = parse_line("#include foo") else { - panic!() - }; - let Sudo::IncludeDir(_) = parse_line("#includedir foo") else { - panic!() - }; - let Sudo::Include(x) = parse_line("#include \"foo bar\"") else { - panic!() - }; - assert_eq!(x, "foo bar"); + assert!(parse_line("#42 ALL=ALL").is_spec()); + assert!(parse_line("ALL ALL=(#42) ALL").is_spec()); + assert!(parse_line("ALL ALL=(%#42) ALL").is_spec()); + assert!(parse_line("ALL ALL=(:#42) ALL").is_spec()); + assert!(parse_line("User_Alias FOO=#42, %#0, #3").is_decl()); + assert!(parse_line("").is_line_comment()); + assert!(parse_line("#this is a comment").is_line_comment()); + assert!(parse_line("#include foo").is_include()); + assert!(parse_line("#includedir foo").is_include_dir()); + assert_eq!("foo bar", parse_line("#include \"foo bar\"").as_include()); // this is fine - let Sudo::LineComment = parse_line("#inlcudedir foo") else { - panic!() - }; - let Sudo::Include(_) = parse_line("@include foo") else { - panic!() - }; - let Sudo::IncludeDir(_) = parse_line("@includedir foo") else { - panic!() - }; - let Sudo::Include(x) = parse_line("@include \"foo bar\"") else { - panic!() - }; - assert_eq!(x, "foo bar"); + assert!(parse_line("#inlcudedir foo").is_line_comment()); + assert!(parse_line("@include foo").is_include()); + assert!(parse_line("@includedir foo").is_include_dir()); + assert_eq!("foo bar", parse_line("@include \"foo bar\"").as_include()); } #[test] fn gh674_at_include_quoted_backslash() { - let Sudo::Include(_) = parse_line(r#"@include "/etc/sudo\ers" "#) else { - panic!() - }; - let Sudo::IncludeDir(_) = parse_line(r#"@includedir "/etc/sudo\ers.d" "#) else { - panic!() - }; + assert!(parse_line(r#"@include "/etc/sudo\ers" "#).is_include()); + assert!(parse_line(r#"@includedir "/etc/sudo\ers.d" "#).is_include_dir()); } #[test] @@ -407,53 +382,44 @@ ); assert_eq!(errs.len(), 1); assert_eq!( - errs[0].1, + errs[0].message, "cannot open sudoers file /etc/%h: percent escape %h in includedir is unsupported" ) } #[test] -#[should_panic] fn hashsign_error() { - let Sudo::Include(_) = parse_line("#include foo bar") else { - todo!() - }; + assert!(parse_line("#include foo bar").is_line_comment()); } #[test] -#[should_panic] fn include_regression() { - let Sudo::Include(_) = parse_line("#4,#include foo") else { - todo!() - }; + assert!(try_parse_line("#4,#include foo").is_none()); } #[test] -#[should_panic] fn nullbyte_regression() { - if let Sudo::Spec(PermissionSpec { .. }) = parse_line("ferris ALL=(ALL:ferris\0) ALL") {}; + assert!(try_parse_line("ferris ALL=(ALL:ferris\0) ALL").is_none()); } #[test] -#[should_panic] fn alias_all_regression() { - parse_line("User_Alias ALL = sudouser"); + assert!(try_parse_line("User_Alias ALL = sudouser").is_none()) } #[test] -#[should_panic] fn defaults_regression() { - parse_line("Defaults .mymachine=ALL"); + assert!(try_parse_line("Defaults .mymachine=ALL").is_none()) } #[test] fn useralias_underscore_regression() { - let Sudo::Spec(x) = parse_line("FOO_BAR ALL=ALL") else { - todo!() - }; - let Qualified::Allow(Meta::Alias(_)) = x.users[0] else { - panic!() - }; + let sudo = parse_line("FOO_BAR ALL=ALL"); + let spec = sudo.as_spec().expect("`Sudo::Spec`"); + assert!(spec.users[0] + .as_allow() + .expect("`Qualified::Allow`") + .is_alias()); } fn test_topo_sort(n: usize) { diff -Nru rust-sudo-rs-0.2.1/src/sudoers/tokens.rs rust-sudo-rs-0.2.2/src/sudoers/tokens.rs --- rust-sudo-rs-0.2.1/src/sudoers/tokens.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/sudoers/tokens.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,15 +1,19 @@ //! Various tokens +use crate::common::{SudoPath, SudoString}; + use super::basic_parser::{Many, Token}; use crate::common::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2}; #[cfg_attr(test, derive(Clone, PartialEq, Eq))] -pub struct Username(pub String); +pub struct Username(pub SudoString); /// A username consists of alphanumeric characters as well as "." and "-", but does not start with an underscore. impl Token for Username { fn construct(text: String) -> Result { - Ok(Username(text)) + SudoString::new(text) + .map_err(|e| e.to_string()) + .map(Username) } fn accept(c: char) -> bool { @@ -84,6 +88,13 @@ Alias(String) = HARDENED_ENUM_VALUE_2, } +impl Meta { + #[cfg(test)] + pub fn is_alias(&self) -> bool { + matches!(self, Self::Alias(..)) + } +} + impl Token for Meta { fn construct(raw: String) -> Result { // `T` may accept whitespace resulting in `raw` having trailing whitespace which would make @@ -318,7 +329,7 @@ #[derive(Clone, PartialEq)] #[cfg_attr(test, derive(Debug, Eq))] pub enum ChDir { - Path(std::path::PathBuf), + Path(SudoPath), Any, } @@ -331,7 +342,9 @@ } else if s.contains('*') { Err("path cannot contain '*'".to_string()) } else { - Ok(ChDir::Path(s.into())) + Ok(ChDir::Path( + SudoPath::try_from(s).map_err(|e| e.to_string())?, + )) } } diff -Nru rust-sudo-rs-0.2.1/src/system/interface.rs rust-sudo-rs-0.2.2/src/system/interface.rs --- rust-sudo-rs-0.2.1/src/system/interface.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/system/interface.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,3 +1,5 @@ +use std::ffi::CStr; + pub type GroupId = libc::gid_t; pub type UserId = libc::uid_t; pub type ProcessId = libc::pid_t; @@ -16,7 +18,7 @@ fn is_root(&self) -> bool { false } - fn in_group_by_name(&self, _name: &str) -> bool { + fn in_group_by_name(&self, _name: &CStr) -> bool { false } fn in_group_by_gid(&self, _gid: GroupId) -> bool { @@ -39,8 +41,8 @@ fn is_root(&self) -> bool { self.has_uid(0) } - fn in_group_by_name(&self, name: &str) -> bool { - if let Ok(Some(group)) = super::Group::from_name(name) { + fn in_group_by_name(&self, name_c: &CStr) -> bool { + if let Ok(Some(group)) = super::Group::from_name(name_c) { self.in_group_by_gid(group.gid) } else { false @@ -67,10 +69,11 @@ use super::*; - fn test_user(user: impl UnixUser, name: &str, uid: libc::uid_t) { + fn test_user(user: impl UnixUser, name_c: &CStr, uid: libc::uid_t) { + let name = name_c.to_str().unwrap(); assert!(user.has_name(name)); assert!(user.has_uid(uid)); - assert!(user.in_group_by_name(name)); + assert!(user.in_group_by_name(name_c)); assert_eq!(user.is_root(), name == "root"); } @@ -82,15 +85,15 @@ #[test] fn test_unix_user() { let user = |name| User::from_name(name).unwrap().unwrap(); - test_user(user("root"), "root", 0); - test_user(user("daemon"), "daemon", 1); + test_user(user(cstr!("root")), cstr!("root"), 0); + test_user(user(cstr!("daemon")), cstr!("daemon"), 1); } #[test] fn test_unix_group() { let group = |name| Group::from_name(name).unwrap().unwrap(); - test_group(group("root"), "root", 0); - test_group(group("daemon"), "daemon", 1); + test_group(group(cstr!("root")), "root", 0); + test_group(group(cstr!("daemon")), "daemon", 1); } #[test] @@ -99,6 +102,6 @@ assert!(!().has_name("root")); assert!(!().has_uid(0)); assert!(!().is_root()); - assert!(!().in_group_by_name("root")); + assert!(!().in_group_by_name(cstr!("root"))); } } diff -Nru rust-sudo-rs-0.2.1/src/system/mod.rs rust-sudo-rs-0.2.2/src/system/mod.rs --- rust-sudo-rs-0.2.1/src/system/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/system/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -1,9 +1,11 @@ +use core::fmt; // TODO: remove unused attribute when system is cleaned up use std::{ collections::BTreeSet, ffi::{c_uint, CStr, CString}, io, mem::MaybeUninit, + ops, os::{ fd::AsRawFd, unix::{self, prelude::OsStrExt}, @@ -12,7 +14,10 @@ str::FromStr, }; -use crate::cutils::*; +use crate::{ + common::{Error, SudoPath, SudoString}, + cutils::*, +}; pub use audit::secure_open; use interface::{DeviceId, GroupId, ProcessId, UserId}; pub use libc::PATH_MAX; @@ -144,25 +149,63 @@ cerr(unsafe { libc::setsid() }) } -pub fn hostname() -> String { - // see `man 2 gethostname` - const MAX_HOST_NAME_SIZE_ACCORDING_TO_SUSV2: libc::c_long = 255; - - // POSIX.1 systems limit hostnames to `HOST_NAME_MAX` bytes - // not including null-byte in the count - let max_hostname_size = - sysconf(libc::_SC_HOST_NAME_MAX).unwrap_or(MAX_HOST_NAME_SIZE_ACCORDING_TO_SUSV2) as usize; - - let buffer_size = max_hostname_size + 1 /* null byte delimiter */ ; - let mut buf = vec![0; buffer_size]; - - match cerr(unsafe { libc::gethostname(buf.as_mut_ptr(), buffer_size) }) { - Ok(_) => unsafe { string_from_ptr(buf.as_ptr()) }, - - // ENAMETOOLONG is returned when hostname is greater than `buffer_size` - Err(_) => { - // but we have chosen a `buffer_size` larger than `max_hostname_size` so no truncation error is possible - panic!("Unexpected error while retrieving hostname, this should not happen"); +#[derive(Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Hostname { + inner: String, +} + +impl fmt::Debug for Hostname { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Hostname").field(&self.inner).finish() + } +} + +impl fmt::Display for Hostname { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.inner) + } +} + +impl ops::Deref for Hostname { + type Target = str; + + fn deref(&self) -> &str { + &self.inner + } +} + +impl Hostname { + #[cfg(test)] + pub fn fake(hostname: &str) -> Self { + Self { + inner: hostname.to_string(), + } + } + + pub fn resolve() -> Self { + // see `man 2 gethostname` + const MAX_HOST_NAME_SIZE_ACCORDING_TO_SUSV2: libc::c_long = 255; + + // POSIX.1 systems limit hostnames to `HOST_NAME_MAX` bytes + // not including null-byte in the count + let max_hostname_size = sysconf(libc::_SC_HOST_NAME_MAX) + .unwrap_or(MAX_HOST_NAME_SIZE_ACCORDING_TO_SUSV2) + as usize; + + let buffer_size = max_hostname_size + 1 /* null byte delimiter */ ; + let mut buf = vec![0; buffer_size]; + + match cerr(unsafe { libc::gethostname(buf.as_mut_ptr(), buffer_size) }) { + Ok(_) => Self { + inner: unsafe { string_from_ptr(buf.as_ptr()) }, + }, + + // ENAMETOOLONG is returned when hostname is greater than `buffer_size` + Err(_) => { + // but we have chosen a `buffer_size` larger than `max_hostname_size` so no truncation error is possible + panic!("Unexpected error while retrieving hostname, this should not happen"); + } } } } @@ -254,9 +297,9 @@ pub struct User { pub uid: UserId, pub gid: GroupId, - pub name: String, + pub name: SudoString, pub gecos: String, - pub home: PathBuf, + pub home: SudoPath, pub shell: PathBuf, pub passwd: String, pub groups: Vec, @@ -266,7 +309,7 @@ /// # Safety /// This function expects `pwd` to be a result from a succesful call to `getpwXXX_r`. /// (It can cause UB if any of `pwd`'s pointed-to strings does not have a null-terminator.) - unsafe fn from_libc(pwd: &libc::passwd) -> User { + unsafe fn from_libc(pwd: &libc::passwd) -> Result { let mut buf_len: libc::c_int = 32; let mut groups_buffer: Vec; @@ -294,19 +337,19 @@ panic!("invalid groups count returned from getgrouplist, this should not happen") }); - User { + Ok(User { uid: pwd.pw_uid, gid: pwd.pw_gid, - name: string_from_ptr(pwd.pw_name), + name: SudoString::new(string_from_ptr(pwd.pw_name))?, gecos: string_from_ptr(pwd.pw_gecos), - home: os_string_from_ptr(pwd.pw_dir).into(), + home: SudoPath::new(os_string_from_ptr(pwd.pw_dir).into())?, shell: os_string_from_ptr(pwd.pw_shell).into(), passwd: string_from_ptr(pwd.pw_passwd), groups: groups_buffer, - } + }) } - pub fn from_uid(uid: UserId) -> std::io::Result> { + pub fn from_uid(uid: UserId) -> Result, Error> { let max_pw_size = sysconf(libc::_SC_GETPW_R_SIZE_MAX).unwrap_or(16_384); let mut buf = vec![0; max_pw_size as usize]; let mut pwd = MaybeUninit::uninit(); @@ -324,7 +367,7 @@ Ok(None) } else { let pwd = unsafe { pwd.assume_init() }; - Ok(Some(unsafe { Self::from_libc(&pwd) })) + unsafe { Self::from_libc(&pwd).map(Some) } } } @@ -344,16 +387,16 @@ unsafe { libc::getgid() } } - pub fn real() -> std::io::Result> { + pub fn real() -> Result, Error> { Self::from_uid(Self::real_uid()) } - pub fn from_name(name: &str) -> std::io::Result> { + pub fn from_name(name_c: &CStr) -> Result, Error> { let max_pw_size = sysconf(libc::_SC_GETPW_R_SIZE_MAX).unwrap_or(16_384); let mut buf = vec![0; max_pw_size as usize]; let mut pwd = MaybeUninit::uninit(); let mut pwd_ptr = std::ptr::null_mut(); - let name_c = CString::new(name).expect("String contained null bytes"); + cerr(unsafe { libc::getpwnam_r( name_c.as_ptr(), @@ -367,7 +410,7 @@ Ok(None) } else { let pwd = unsafe { pwd.assume_init() }; - Ok(Some(unsafe { Self::from_libc(&pwd) })) + unsafe { Self::from_libc(&pwd).map(Some) } } } } @@ -430,12 +473,11 @@ } } - pub fn from_name(name: &str) -> std::io::Result> { + pub fn from_name(name_c: &CStr) -> std::io::Result> { let max_gr_size = sysconf(libc::_SC_GETGR_R_SIZE_MAX).unwrap_or(16_384); let mut buf = vec![0; max_gr_size as usize]; let mut grp = MaybeUninit::uninit(); let mut grp_ptr = std::ptr::null_mut(); - let name_c = CString::new(name).expect("String contained null bytes"); cerr(unsafe { libc::getgrnam_r( name_c.as_ptr(), diff -Nru rust-sudo-rs-0.2.1/src/system/time.rs rust-sudo-rs-0.2.2/src/system/time.rs --- rust-sudo-rs-0.2.1/src/system/time.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/system/time.rs 2006-07-24 01:21:28.000000000 +0000 @@ -23,6 +23,8 @@ crate::cutils::cerr(unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, spec.as_mut_ptr()) })?; + // SAFETY: The `libc::clock_gettime` will correctly initialize `spec`, + // otherwise it will return early with the `?` operator. let spec = unsafe { spec.assume_init() }; Ok(spec.into()) } diff -Nru rust-sudo-rs-0.2.1/src/system/timestamp.rs rust-sudo-rs-0.2.2/src/system/timestamp.rs --- rust-sudo-rs-0.2.1/src/system/timestamp.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/system/timestamp.rs 2006-07-24 01:21:28.000000000 +0000 @@ -4,7 +4,10 @@ path::PathBuf, }; -use crate::log::{auth_info, auth_warn}; +use crate::{ + common::resolve::CurrentUser, + log::{auth_info, auth_warn}, +}; use super::{ audit::secure_open_cookie_file, @@ -44,10 +47,11 @@ impl SessionRecordFile { const BASE_PATH: &'static str = "/var/run/sudo-rs/ts"; - pub fn open_for_user(user: UserId, timeout: Duration) -> io::Result { + pub fn open_for_user(user: &CurrentUser, timeout: Duration) -> io::Result { + let uid = user.uid; let mut path = PathBuf::from(Self::BASE_PATH); - path.push(user.to_string()); - SessionRecordFile::new(user, secure_open_cookie_file(&path)?, timeout) + path.push(uid.to_string()); + SessionRecordFile::new(uid, secure_open_cookie_file(&path)?, timeout) } const FILE_VERSION: u16 = 1; diff -Nru rust-sudo-rs-0.2.1/src/visudo/mod.rs rust-sudo-rs-0.2.2/src/visudo/mod.rs --- rust-sudo-rs-0.2.1/src/visudo/mod.rs 2006-07-24 01:21:28.000000000 +0000 +++ rust-sudo-rs-0.2.2/src/visudo/mod.rs 2006-07-24 01:21:28.000000000 +0000 @@ -2,7 +2,7 @@ mod help; use std::{ - ffi::{CStr, CString, OsString}, + ffi::{CString, OsString}, fs::{File, Permissions}, io::{self, Read, Seek, Write}, os::unix::prelude::{MetadataExt, OsStringExt, PermissionsExt}, @@ -107,7 +107,7 @@ } let mut stderr = io::stderr(); - for crate::sudoers::Error(_position, message) in errors { + for crate::sudoers::Error { message, .. } in errors { writeln!(stderr, "syntax error: {message}")?; } @@ -245,7 +245,7 @@ writeln!(stderr, "The provided sudoers file format is not recognized or contains syntax errors. Please review:\n")?; - for crate::sudoers::Error(_position, message) in errors { + for crate::sudoers::Error { message, .. } in errors { writeln!(stderr, "syntax error: {message}")?; } @@ -302,12 +302,7 @@ } fn create_temporary_dir() -> io::Result { - // SAFETY: the required safety checks (last byte is NULL; no inner NULLs) are performed at - // compile time by the const-constructor - const TEMPLATE: &CStr = - unsafe { CStr::from_bytes_with_nul_unchecked(b"/tmp/sudoers-XXXXXX\0") }; - - let template = TEMPLATE.to_owned(); + let template = cstr!("/tmp/sudoers-XXXXXX").to_owned(); let ptr = unsafe { libc::mkdtemp(template.into_raw()) };