CVE-2025-64517
Description
sudo-rs is a memory safe implementation of sudo and su written in Rust. With Defaults targetpw (or Defaults rootpw) enabled, the password of the target account (or root account) instead of the invoking user is used for authentication. sudo-rs starting in version 0.2.5 and prior to version 0.2.10 incorrectly recorded the invoking user’s UID instead of the authenticated-as user's UID in the authentication timestamp. Any later sudo invocation on the same terminal while the timestamp was still valid would use that timestamp, potentially bypassing new authentication even if the policy would have required it. A highly-privileged user (able to run commands as other users, or as root, through sudo) who knows one password of an account they are allowed to run commands as, would be able to run commands as any other account the policy permits them to run commands for, even if they don't know the password for those accounts. A common instance of this would be that a user can still use their own password to run commands as root (the default behaviour of sudo), effectively negating the intended behaviour of the targetpw or rootpw options. Version 0.2.10 contains a patch for the issue. Versions prior to 0.2.5 are not affected, since they do not offer Defaults targetpw or Defaults rootpw.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
sudo-rscrates.io | >= 0.2.5, < 0.2.10 | 0.2.10 |
Affected products
1- Range: v0.2.5, v0.2.6, v0.2.7, …
Patches
18423fd986c3fUse auth user rather than current user in timestamp records
2 files changed · +45 −26
src/sudo/pipeline.rs+13 −11 modified@@ -158,15 +158,6 @@ fn auth_and_update_record_file( pwfeedback, }: Authentication, ) -> Result<PamContext, Error> { - 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()) @@ -177,6 +168,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, @@ -202,7 +203,7 @@ fn auth_and_update_record_file( 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}"); @@ -255,14 +256,15 @@ fn determine_auth_status( use_session_records: bool, record_for: Option<RecordScope>, current_user: &CurrentUser, + auth_user: &AuthUser, prior_validity: Duration, ) -> AuthStatus { if !must_policy_authenticate { AuthStatus::new(false, None) } 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 { .. }) => {
src/system/timestamp.rs+32 −15 modified@@ -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<TouchResult> { + pub fn touch(&mut self, scope: RecordScope, auth_user: &AuthUser) -> io::Result<TouchResult> { // 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 targeting that /// specific user will be disabled. - pub fn disable(&mut self, scope: RecordScope, auth_user: Option<UserId>) -> 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<CreateResult> { + pub fn create(&mut self, scope: RecordScope, auth_user: &AuthUser) -> io::Result<CreateResult> { // 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,22 +738,22 @@ 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"); }; assert_eq!(time, old_time); 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"); };
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-q428-6v73-fc4qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-64517ghsaADVISORY
- github.com/trifectatechfoundation/sudo-rs/commit/8423fd986c3fa58b357f238c0db5e54baca5255dghsaWEB
- github.com/trifectatechfoundation/sudo-rs/releases/tag/v0.2.10nvdWEB
- github.com/trifectatechfoundation/sudo-rs/security/advisories/GHSA-q428-6v73-fc4qnvdWEB
- github.com/trifectatechfoundation/sudo-rs/commit/8423fd986c3fa58b357f238c0db5e54baca5255d.nvd
News mentions
0No linked articles in our index yet.