Skip to content

Commit f052506

Browse files
authored
Canonicalize paths in the reference frame of the target user (#1234)
2 parents a64bf30 + a9859e7 commit f052506

File tree

2 files changed

+45
-29
lines changed

2 files changed

+45
-29
lines changed

src/common/context.rs

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::exec::{RunOptions, Umask};
55
#[cfg_attr(not(feature = "sudoedit"), allow(unused_imports))]
66
use crate::sudo::{SudoEditOptions, SudoListOptions, SudoRunOptions, SudoValidateOptions};
77
use crate::sudoers::Sudoers;
8-
use crate::system::{Group, Hostname, Process, User};
8+
use crate::system::{audit::sudo_call, Group, Hostname, Process, User};
99

1010
use super::{
1111
command::CommandAndArguments,
@@ -81,7 +81,9 @@ impl Context {
8181
system_path.as_ref()
8282
};
8383

84-
CommandAndArguments::build_from_args(shell, sudo_options.positional_args, path)
84+
sudo_call(&target_user, &target_group, || {
85+
CommandAndArguments::build_from_args(shell, sudo_options.positional_args, path)
86+
})?
8587
};
8688

8789
Ok(Context {
@@ -115,18 +117,20 @@ impl Context {
115117
resolve_target_user_and_group(&sudo_options.user, &sudo_options.group, &current_user)?;
116118

117119
// resolve file arguments; if something can't be resolved, don't add it to the "edit" list
118-
let resolved_args = sudo_options.positional_args.iter().map(|arg| {
119-
let path = Path::new(arg);
120-
let absolute_path;
121-
crate::common::resolve::canonicalize_newfile(if path.is_absolute() {
122-
path
123-
} else {
124-
absolute_path = Path::new(".").join(path);
125-
&absolute_path
120+
let resolved_args = sudo_call(&target_user, &target_group, || {
121+
sudo_options.positional_args.iter().map(|arg| {
122+
let path = Path::new(arg);
123+
let absolute_path;
124+
crate::common::resolve::canonicalize_newfile(if path.is_absolute() {
125+
path
126+
} else {
127+
absolute_path = Path::new(".").join(path);
128+
&absolute_path
129+
})
130+
.map_err(|_| arg)
131+
.and_then(|path| path.into_os_string().into_string().map_err(|_| arg))
126132
})
127-
.map_err(|_| arg)
128-
.and_then(|path| path.into_os_string().into_string().map_err(|_| arg))
129-
});
133+
})?;
130134

131135
let files_to_edit = resolved_args
132136
.clone()
@@ -223,7 +227,9 @@ impl Context {
223227
system_path.as_ref()
224228
};
225229

226-
CommandAndArguments::build_from_args(None, sudo_options.positional_args, path)
230+
sudo_call(&target_user, &target_group, || {
231+
CommandAndArguments::build_from_args(None, sudo_options.positional_args, path)
232+
})?
227233
};
228234

229235
Ok(Context {
@@ -270,21 +276,21 @@ impl Context {
270276

271277
#[cfg(test)]
272278
mod tests {
273-
use crate::{
274-
sudo::SudoAction,
275-
system::{interface::UserId, Hostname},
276-
};
279+
use crate::{common::resolve::CurrentUser, sudo::SudoAction, system::Hostname};
277280

278281
use super::Context;
279282

280283
#[test]
281284
fn test_build_run_context() {
282-
let options = SudoAction::try_parse_from(["sudo", "echo", "hello"])
285+
let mut options = SudoAction::try_parse_from(["sudo", "echo", "hello"])
283286
.unwrap()
284287
.try_into_run()
285288
.ok()
286289
.unwrap();
287290

291+
let current_user = CurrentUser::resolve().unwrap();
292+
options.user = Some(current_user.name.clone());
293+
288294
let context = Context::from_run_opts(options, &mut Default::default()).unwrap();
289295

290296
if cfg!(target_os = "linux") {
@@ -295,6 +301,6 @@ mod tests {
295301
}
296302
assert_eq!(context.command.arguments, ["hello"]);
297303
assert_eq!(context.hostname, Hostname::resolve());
298-
assert_eq!(context.target_user.uid, UserId::ROOT);
304+
assert_eq!(context.target_user.uid, current_user.uid);
299305
}
300306
}

src/system/audit.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ use crate::common::resolve::CurrentUser;
1717

1818
/// Temporary change privileges --- essentially a 'mini sudo'
1919
/// This is only used for sudoedit.
20-
fn sudo_call<T>(
20+
pub(crate) fn sudo_call<T>(
2121
target_user: &User,
2222
target_group: &Group,
2323
operation: impl FnOnce() -> T,
2424
) -> io::Result<T> {
2525
const KEEP_UID: libc::uid_t = -1i32 as libc::uid_t;
2626
const KEEP_GID: libc::gid_t = -1i32 as libc::gid_t;
2727

28+
// SAFETY: these libc functions are always safe to call
29+
let (cur_user_id, cur_group_id) =
30+
unsafe { (UserId::new(libc::geteuid()), GroupId::new(libc::getegid())) };
31+
2832
let cur_groups = {
2933
// SAFETY: calling with size 0 does not modify through the pointer, and is
3034
// a documented way of getting the length needed.
@@ -43,6 +47,19 @@ fn sudo_call<T>(
4347
let mut target_groups = target_user.groups.clone();
4448
inject_group(target_group.gid, &mut target_groups);
4549

50+
if cfg!(test)
51+
&& target_user.uid == cur_user_id
52+
&& target_group.gid == cur_group_id
53+
&& target_groups
54+
.iter()
55+
.filter(|x| **x != target_group.gid)
56+
.eq(cur_groups.iter().filter(|x| **x != cur_group_id))
57+
{
58+
// we are not actually switching users, simply run the closure
59+
// (this would also be safe in production mode, but it is a needless check)
60+
return Ok(operation());
61+
}
62+
4663
struct ResetUserGuard(UserId, GroupId, Vec<GroupId>);
4764

4865
impl Drop for ResetUserGuard {
@@ -59,14 +76,7 @@ fn sudo_call<T>(
5976
}
6077
}
6178

62-
// SAFETY: these libc functions are always safe to call
63-
let guard = unsafe {
64-
ResetUserGuard(
65-
UserId::new(libc::geteuid()),
66-
GroupId::new(libc::getegid()),
67-
cur_groups,
68-
)
69-
};
79+
let guard = ResetUserGuard(cur_user_id, cur_group_id, cur_groups);
7080

7181
set_supplementary_groups(&target_groups)?;
7282
// SAFETY: this function is always safe to call

0 commit comments

Comments
 (0)