Version in base suite: 0.2.5-5 Base version: rust-sudo-rs_0.2.5-5 Target version: rust-sudo-rs_0.2.5-5+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/rust-sudo-rs/rust-sudo-rs_0.2.5-5.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/rust-sudo-rs/rust-sudo-rs_0.2.5-5+deb13u1.dsc changelog | 10 patches/GHSA-c978-wq47-pvvw-backport-targetpw_timestamp_fixes.patch | 226 ++++++++ patches/GHSA-q428-6v73-fc4q-backport-rpassword-fixes.patch | 276 ++++++++++ patches/series | 2 4 files changed, 514 insertions(+) diff -Nru rust-sudo-rs-0.2.5/debian/changelog rust-sudo-rs-0.2.5/debian/changelog --- rust-sudo-rs-0.2.5/debian/changelog 2025-05-11 07:10:37.000000000 +0000 +++ rust-sudo-rs-0.2.5/debian/changelog 2025-11-07 16:55:31.000000000 +0000 @@ -1,3 +1,13 @@ +rust-sudo-rs (0.2.5-5+deb13u1) trixie-security; urgency=high + + * Team upload + * Ensure (partially) input passwords are not printed if killed during + password prompt (Fixes:: GHSA-q428-6v73-fc4q) + * Ensure `Defaults targetpw` and `Defaults rootpw` are taken into account for + timestamp files (Fixes: GHSA-c978-wq47-pvvw) + + -- Fabian Grünbichler Fri, 07 Nov 2025 17:55:31 +0100 + rust-sudo-rs (0.2.5-5) unstable; urgency=medium * Package sudo-rs 0.2.5 from crates.io using debcargo 2.7.8 diff -Nru rust-sudo-rs-0.2.5/debian/patches/GHSA-c978-wq47-pvvw-backport-targetpw_timestamp_fixes.patch rust-sudo-rs-0.2.5/debian/patches/GHSA-c978-wq47-pvvw-backport-targetpw_timestamp_fixes.patch --- rust-sudo-rs-0.2.5/debian/patches/GHSA-c978-wq47-pvvw-backport-targetpw_timestamp_fixes.patch 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.5/debian/patches/GHSA-c978-wq47-pvvw-backport-targetpw_timestamp_fixes.patch 2025-11-07 16:55:06.000000000 +0000 @@ -0,0 +1,226 @@ +From a5fb8000bd83747c7ca56044bfadd8abbfaf2404 Mon Sep 17 00:00:00 2001 +From: bjorn3 <17426603+bjorn3@users.noreply.github.com> +Date: Wed, 29 Oct 2025 16:35:16 +0100 +Subject: [PATCH 2/2] backport targetpw_timestamp_fixes + +--- + src/sudo/pipeline.rs | 24 +++++++++++---------- + src/system/timestamp.rs | 47 ++++++++++++++++++++++++++++------------- + 2 files changed, 45 insertions(+), 26 deletions(-) + +diff --git a/src/sudo/pipeline.rs b/src/sudo/pipeline.rs +index 724d8853..d07b2142 100644 +--- a/src/sudo/pipeline.rs ++++ b/src/sudo/pipeline.rs +@@ -152,15 +152,6 @@ fn auth_and_update_record_file( + .. + }: &Authentication, + ) -> Result { +- let scope = RecordScope::for_process(&Process::new()); +- let mut auth_status = determine_auth_status( +- must_authenticate, +- context.use_session_records, +- scope, +- &context.current_user, +- prior_validity, +- ); +- + let auth_user = match credential { + AuthenticatingUser::InvokingUser => { + AuthUser::from_current_user(context.current_user.clone()) +@@ -171,6 +162,16 @@ fn auth_and_update_record_file( + } + }; + ++ let scope = RecordScope::for_process(&Process::new()); ++ let mut auth_status = determine_auth_status( ++ must_authenticate, ++ context.use_session_records, ++ scope, ++ &context.current_user, ++ &auth_user, ++ prior_validity, ++ ); ++ + let mut pam_context = init_pam(InitPamArgs { + launch: context.launch, + use_stdin: context.stdin, +@@ -186,7 +187,7 @@ fn auth_and_update_record_file( + if auth_status.must_authenticate { + attempt_authenticate(&mut pam_context, context.non_interactive, allowed_attempts)?; + if let (Some(record_file), Some(scope)) = (&mut auth_status.record_file, scope) { +- match record_file.create(scope, context.current_user.uid) { ++ match record_file.create(scope, &auth_user) { + Ok(_) => (), + Err(e) => { + auth_warn!("Could not update session record file with new record: {e}"); +@@ -238,6 +239,7 @@ fn determine_auth_status( + use_session_records: bool, + record_for: Option, + current_user: &CurrentUser, ++ auth_user: &AuthUser, + prior_validity: Duration, + ) -> AuthStatus { + if !must_policy_authenticate { +@@ -245,7 +247,7 @@ fn determine_auth_status( + } else if let (true, Some(record_for)) = (use_session_records, record_for) { + match SessionRecordFile::open_for_user(current_user, prior_validity) { + Ok(mut sr) => { +- match sr.touch(record_for, current_user.uid) { ++ match sr.touch(record_for, auth_user) { + // if a record was found and updated within the timeout, we do not need to authenticate + Ok(TouchResult::Updated { .. }) => AuthStatus::new(false, Some(sr)), + Ok(TouchResult::NotFound | TouchResult::Outdated { .. }) => { +diff --git a/src/system/timestamp.rs b/src/system/timestamp.rs +index 55e763f3..86abbde0 100644 +--- a/src/system/timestamp.rs ++++ b/src/system/timestamp.rs +@@ -4,6 +4,7 @@ use std::{ + path::PathBuf, + }; + ++use crate::common::resolve::AuthUser; + use crate::{ + common::resolve::CurrentUser, + log::{auth_info, auth_warn}, +@@ -179,7 +180,7 @@ impl SessionRecordFile { + /// that record time to the current time. This will not create a new record + /// when one is not found. A record will only be updated if it is still + /// valid at this time. +- pub fn touch(&mut self, scope: RecordScope, auth_user: UserId) -> io::Result { ++ pub fn touch(&mut self, scope: RecordScope, auth_user: &AuthUser) -> io::Result { + // lock the file to indicate that we are currently in a writing operation + let lock = FileLock::exclusive(&self.file, false)?; + self.seek_to_first_record()?; +@@ -218,7 +219,7 @@ impl SessionRecordFile { + /// Disable all records that match the given scope. If an auth user id is + /// given then only records with the given scope that are targetting that + /// specific user will be disabled. +- pub fn disable(&mut self, scope: RecordScope, auth_user: Option) -> io::Result<()> { ++ pub fn disable(&mut self, scope: RecordScope, auth_user: Option<&AuthUser>) -> io::Result<()> { + let lock = FileLock::exclusive(&self.file, false)?; + self.seek_to_first_record()?; + while let Some(record) = self.next_record()? { +@@ -237,7 +238,7 @@ impl SessionRecordFile { + /// Create a new record for the given scope and auth user id. + /// If there is an existing record that matches the scope and auth user, + /// then that record will be updated. +- pub fn create(&mut self, scope: RecordScope, auth_user: UserId) -> io::Result { ++ pub fn create(&mut self, scope: RecordScope, auth_user: &AuthUser) -> io::Result { + // lock the file to indicate that we are currently writing to it + let lock = FileLock::exclusive(&self.file, false)?; + self.seek_to_first_record()?; +@@ -256,7 +257,7 @@ impl SessionRecordFile { + } + + // record was not found in the list so far, create a new one +- let record = SessionRecord::new(scope, auth_user)?; ++ let record = SessionRecord::new(scope, auth_user.uid)?; + + // make sure we really are at the end of the file + self.file.seek(io::SeekFrom::End(0))?; +@@ -552,8 +553,8 @@ impl SessionRecord { + + /// Returns true if this record matches the specified scope and is for the + /// specified target auth user. +- pub fn matches(&self, scope: &RecordScope, auth_user: UserId) -> bool { +- self.scope == *scope && self.auth_user == auth_user ++ pub fn matches(&self, scope: &RecordScope, auth_user: &AuthUser) -> bool { ++ self.scope == *scope && self.auth_user == auth_user.uid + } + + /// Returns true if this record was written somewhere in the time range +@@ -566,11 +567,27 @@ impl SessionRecord { + + #[cfg(test)] + mod tests { ++ use std::path::Path; ++ + use super::*; ++ use crate::common::{SudoPath, SudoString}; ++ use crate::system::interface::GroupId; + use crate::system::tests::tempfile; ++ use crate::system::User; + + static TEST_USER_ID: UserId = UserId::ROOT; + ++ fn auth_user_from_uid(uid: libc::uid_t) -> AuthUser { ++ AuthUser::from_user_for_targetpw(User { ++ uid: UserId::new(uid), ++ gid: GroupId::new(0), ++ name: SudoString::new("dummy".to_owned()).unwrap(), ++ home: SudoPath::new(Path::new("/nonexistent").to_owned()).unwrap(), ++ shell: Path::new("/bin/sh").to_owned(), ++ groups: vec![], ++ }) ++ } ++ + #[test] + fn can_encode_and_decode() { + let tty_sample = SessionRecord::new( +@@ -618,22 +635,22 @@ mod tests { + + let tty_sample = SessionRecord::new(scope, UserId::new(675)).unwrap(); + +- assert!(tty_sample.matches(&scope, UserId::new(675))); +- assert!(!tty_sample.matches(&scope, UserId::new(789))); ++ assert!(tty_sample.matches(&scope, &auth_user_from_uid(675))); ++ assert!(!tty_sample.matches(&scope, &auth_user_from_uid(789))); + assert!(!tty_sample.matches( + &RecordScope::Tty { + tty_device: DeviceId::new(20), + session_pid: ProcessId::new(1234), + init_time + }, +- UserId::new(675), ++ &auth_user_from_uid(675), + )); + assert!(!tty_sample.matches( + &RecordScope::Ppid { + group_pid: ProcessId::new(42), + init_time + }, +- UserId::new(675), ++ &auth_user_from_uid(675), + )); + + // make sure time is different +@@ -644,7 +661,7 @@ mod tests { + session_pid: ProcessId::new(1234), + init_time: ProcessCreateTime::new(1, 1) + }, +- UserId::new(675), ++ &auth_user_from_uid(675), + )); + } + +@@ -721,14 +738,14 @@ mod tests { + session_pid: ProcessId::new(0), + init_time: ProcessCreateTime::new(0, 0), + }; +- let auth_user = UserId::new(2424); +- let res = srf.create(tty_scope, auth_user).unwrap(); ++ let auth_user = auth_user_from_uid(2424); ++ let res = srf.create(tty_scope, &auth_user).unwrap(); + let CreateResult::Created { time } = res else { + panic!("Expected record to be created"); + }; + + std::thread::sleep(std::time::Duration::from_millis(1)); +- let second = srf.touch(tty_scope, auth_user).unwrap(); ++ let second = srf.touch(tty_scope, &auth_user).unwrap(); + let TouchResult::Updated { old_time, new_time } = second else { + panic!("Expected record to be updated"); + }; +@@ -736,7 +753,7 @@ mod tests { + assert_ne!(old_time, new_time); + + std::thread::sleep(std::time::Duration::from_millis(1)); +- let res = srf.create(tty_scope, auth_user).unwrap(); ++ let res = srf.create(tty_scope, &auth_user).unwrap(); + let CreateResult::Updated { old_time, new_time } = res else { + panic!("Expected record to be updated"); + }; +-- +2.51.0 + diff -Nru rust-sudo-rs-0.2.5/debian/patches/GHSA-q428-6v73-fc4q-backport-rpassword-fixes.patch rust-sudo-rs-0.2.5/debian/patches/GHSA-q428-6v73-fc4q-backport-rpassword-fixes.patch --- rust-sudo-rs-0.2.5/debian/patches/GHSA-q428-6v73-fc4q-backport-rpassword-fixes.patch 1970-01-01 00:00:00.000000000 +0000 +++ rust-sudo-rs-0.2.5/debian/patches/GHSA-q428-6v73-fc4q-backport-rpassword-fixes.patch 2025-11-07 16:55:06.000000000 +0000 @@ -0,0 +1,276 @@ +From f6fb621f283da091aff421adfa176389c0f1973d Mon Sep 17 00:00:00 2001 +From: Marc Schoolderman +Date: Tue, 4 Nov 2025 20:43:12 +0100 +Subject: [PATCH 1/2] backport rpassword_fixes + +--- + src/pam/rpassword.rs | 174 +++++++++++++++++++++++-------------------- + 1 file changed, 92 insertions(+), 82 deletions(-) + +diff --git a/src/pam/rpassword.rs b/src/pam/rpassword.rs +index a433b3bf..3ad415b7 100644 +--- a/src/pam/rpassword.rs ++++ b/src/pam/rpassword.rs +@@ -29,7 +29,7 @@ struct HiddenInput { + } + + impl HiddenInput { +- fn new(feedback: bool) -> io::Result> { ++ fn new() -> io::Result> { + // control ourselves that we are really talking to a TTY + // mitigates: https://marc.info/?l=oss-security&m=168164424404224 + let Ok(tty) = fs::File::open("/dev/tty") else { +@@ -49,10 +49,8 @@ impl HiddenInput { + // But don't hide the NL character when the user hits ENTER. + term.c_lflag |= ECHONL; + +- if feedback { +- // Disable canonical mode to read character by character when pwfeedback is enabled. +- term.c_lflag &= !ICANON; +- } ++ // Disable canonical mode to read character by character when pwfeedback is enabled. ++ term.c_lflag &= !ICANON; + + // Save the settings for now. + // SAFETY: we are passing tcsetattr a valid file descriptor and pointer-to-struct +@@ -80,28 +78,6 @@ fn safe_tcgetattr(tty: impl AsFd) -> io::Result { + Ok(unsafe { term.assume_init() }) + } + +-/// Reads a password from the given file descriptor +-fn read_unbuffered(source: &mut dyn io::Read) -> io::Result { +- let mut password = PamBuffer::default(); +- let mut pwd_iter = password.iter_mut(); +- +- const EOL: u8 = 0x0A; +- let input = source.bytes().take_while(|x| x.as_ref().ok() != Some(&EOL)); +- +- for read_byte in input { +- if let Some(dest) = pwd_iter.next() { +- *dest = read_byte? +- } else { +- return Err(Error::new( +- ErrorKind::OutOfMemory, +- "incorrect password attempt", +- )); +- } +- } +- +- Ok(password) +-} +- + fn erase_feedback(sink: &mut dyn io::Write, i: usize) { + const BACKSPACE: u8 = 0x08; + for _ in 0..i { +@@ -111,56 +87,90 @@ fn erase_feedback(sink: &mut dyn io::Write, i: usize) { + } + } + +-/// Reads a password from the given file descriptor while showing feedback to the user. +-fn read_unbuffered_with_feedback( ++enum Hidden<'a> { ++ No, ++ Yes(&'a HiddenInput), ++ WithFeedback(&'a HiddenInput), ++} ++ ++/// Reads a password from the given file descriptor while optionally showing feedback to the user. ++fn read_unbuffered( + source: &mut dyn io::Read, + sink: &mut dyn io::Write, +- hide_input: &HiddenInput, ++ hide_input: Hidden<'_>, + ) -> io::Result { ++ struct ExitGuard<'a> { ++ pw_len: usize, ++ feedback: bool, ++ sink: &'a mut dyn io::Write, ++ } ++ ++ // Ensure we erase the password feedback no matter how we exit read_unbuffered ++ impl Drop for ExitGuard<'_> { ++ fn drop(&mut self) { ++ if self.feedback { ++ erase_feedback(self.sink, self.pw_len); ++ } ++ let _ = self.sink.write(b"\n"); ++ } ++ } ++ + let mut password = PamBuffer::default(); +- let mut pw_len = 0; ++ let mut state = ExitGuard { ++ pw_len: 0, ++ feedback: matches!(hide_input, Hidden::WithFeedback(_)), ++ sink, ++ }; + + // invariant: the amount of nonzero-bytes in the buffer correspond + // with the amount of asterisks on the terminal (both tracked in `pw_len`) ++ //TODO: we actually only want to allow clippy::unbuffered_bytes ++ #[allow(clippy::perf)] + for read_byte in source.bytes() { + let read_byte = read_byte?; + + if read_byte == b'\n' || read_byte == b'\r' { +- erase_feedback(sink, pw_len); +- let _ = sink.write(b"\n"); + break; + } + +- if read_byte == hide_input.term_orig.c_cc[VEOF] { +- erase_feedback(sink, pw_len); +- password.fill(0); +- break; ++ if let Hidden::Yes(input) | Hidden::WithFeedback(input) = hide_input { ++ if read_byte == input.term_orig.c_cc[VEOF] { ++ password.fill(0); ++ break; ++ } ++ ++ if read_byte == input.term_orig.c_cc[VERASE] { ++ if state.pw_len > 0 { ++ if let Hidden::WithFeedback(_) = hide_input { ++ erase_feedback(state.sink, 1); ++ } ++ password[state.pw_len - 1] = 0; ++ state.pw_len -= 1; ++ } ++ continue; ++ } ++ ++ if read_byte == input.term_orig.c_cc[VKILL] { ++ if let Hidden::WithFeedback(_) = hide_input { ++ erase_feedback(state.sink, state.pw_len); ++ } ++ password.fill(0); ++ state.pw_len = 0; ++ continue; ++ } + } + +- if read_byte == hide_input.term_orig.c_cc[VERASE] { +- if pw_len > 0 { +- erase_feedback(sink, 1); +- password[pw_len - 1] = 0; +- pw_len -= 1; ++ if let Some(dest) = password.get_mut(state.pw_len) { ++ *dest = read_byte; ++ state.pw_len += 1; ++ if let Hidden::WithFeedback(_) = hide_input { ++ let _ = state.sink.write(b"*"); + } +- } else if read_byte == hide_input.term_orig.c_cc[VKILL] { +- erase_feedback(sink, pw_len); +- password.fill(0); +- pw_len = 0; + } else { +- #[allow(clippy::collapsible_else_if)] +- if let Some(dest) = password.get_mut(pw_len) { +- *dest = read_byte; +- pw_len += 1; +- let _ = sink.write(b"*"); +- } else { +- erase_feedback(sink, pw_len); +- +- return Err(Error::new( +- ErrorKind::OutOfMemory, +- "incorrect password attempt", +- )); +- } ++ return Err(Error::new( ++ ErrorKind::OutOfMemory, ++ "incorrect password attempt", ++ )); + } + } + +@@ -197,26 +207,31 @@ impl Terminal<'_> { + + /// Reads input with TTY echo disabled + pub fn read_password(&mut self) -> io::Result { +- let input = self.source(); +- let _hide_input = HiddenInput::new(false)?; +- read_unbuffered(input) ++ let hide_input = HiddenInput::new()?; ++ self.read_inner(hide_input.as_ref().map(Hidden::Yes).unwrap_or(Hidden::No)) + } + + /// Reads input with TTY echo disabled, but do provide visual feedback while typing. + pub fn read_password_with_feedback(&mut self) -> io::Result { +- if let Some(hide_input) = HiddenInput::new(true)? { +- match self { +- Terminal::StdIE(x, y) => read_unbuffered_with_feedback(x, y, &hide_input), +- Terminal::Tty(x) => read_unbuffered_with_feedback(&mut &*x, &mut &*x, &hide_input), +- } +- } else { +- read_unbuffered(self.source()) +- } ++ let hide_input = HiddenInput::new()?; ++ self.read_inner( ++ hide_input ++ .as_ref() ++ .map(Hidden::WithFeedback) ++ .unwrap_or(Hidden::No), ++ ) + } + + /// Reads input with TTY echo enabled + pub fn read_cleartext(&mut self) -> io::Result { +- read_unbuffered(self.source()) ++ self.read_inner(Hidden::No) ++ } ++ ++ fn read_inner(&mut self, hide_input: Hidden<'_>) -> io::Result { ++ match self { ++ Terminal::StdIE(stdin, stdout) => read_unbuffered(stdin, stdout, hide_input), ++ Terminal::Tty(file) => read_unbuffered(&mut &*file, &mut &*file, hide_input), ++ } + } + + /// Display information +@@ -231,13 +246,6 @@ impl Terminal<'_> { + } + + // boilerplate reduction functions +- fn source(&mut self) -> &mut dyn io::Read { +- match self { +- Terminal::StdIE(x, _) => x, +- Terminal::Tty(x) => x, +- } +- } +- + fn sink(&mut self) -> &mut dyn io::Write { + match self { + Terminal::StdIE(_, x) => x, +@@ -248,12 +256,13 @@ impl Terminal<'_> { + + #[cfg(test)] + mod test { +- use super::{read_unbuffered, write_unbuffered}; ++ use super::*; + + #[test] + fn miri_test_read() { + let mut data = "password123\nhello world".as_bytes(); +- let buf = read_unbuffered(&mut data).unwrap(); ++ let mut stdout = Vec::new(); ++ let buf = read_unbuffered(&mut data, &mut stdout, Hidden::No).unwrap(); + // check that the \n is not part of input + assert_eq!( + buf.iter() +@@ -268,8 +277,9 @@ mod test { + + #[test] + fn miri_test_longpwd() { +- assert!(read_unbuffered(&mut "a".repeat(511).as_bytes()).is_ok()); +- assert!(read_unbuffered(&mut "a".repeat(512).as_bytes()).is_err()); ++ let mut stdout = Vec::new(); ++ assert!(read_unbuffered(&mut "a".repeat(511).as_bytes(), &mut stdout, Hidden::No).is_ok()); ++ assert!(read_unbuffered(&mut "a".repeat(512).as_bytes(), &mut stdout, Hidden::No).is_err()); + } + + #[test] +-- +2.51.0 + diff -Nru rust-sudo-rs-0.2.5/debian/patches/series rust-sudo-rs-0.2.5/debian/patches/series --- rust-sudo-rs-0.2.5/debian/patches/series 2025-05-11 07:10:37.000000000 +0000 +++ rust-sudo-rs-0.2.5/debian/patches/series 2025-11-07 16:55:31.000000000 +0000 @@ -1,3 +1,5 @@ disable-test-timeout.diff fix-unit-test-on-32-bit.patch CVE-2025-46717-CVE-2025-46718.diff +GHSA-q428-6v73-fc4q-backport-rpassword-fixes.patch +GHSA-c978-wq47-pvvw-backport-targetpw_timestamp_fixes.patch