Skip to content

Commit ba383eb

Browse files
authored
Implement support for the umask and umask_override defaults (#1151)
2 parents 77fa3b6 + bfbfbbd commit ba383eb

File tree

10 files changed

+130
-3
lines changed

10 files changed

+130
-3
lines changed

src/common/context.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::io;
22

33
use crate::common::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2};
4-
use crate::exec::RunOptions;
4+
use crate::exec::{RunOptions, Umask};
55
use crate::sudo::{SudoListOptions, SudoRunOptions, SudoValidateOptions};
66
use crate::sudoers::Sudoers;
77
use crate::system::{Group, Hostname, Process, User};
@@ -32,6 +32,7 @@ pub struct Context {
3232
// policy
3333
pub use_pty: bool,
3434
pub noexec: bool,
35+
pub umask: Umask,
3536
}
3637

3738
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -95,6 +96,7 @@ impl Context {
9596
process: Process::new(),
9697
use_pty: true,
9798
noexec: false,
99+
umask: Umask::Preserve,
98100
})
99101
}
100102

@@ -120,6 +122,7 @@ impl Context {
120122
process: Process::new(),
121123
use_pty: true,
122124
noexec: false,
125+
umask: Umask::Preserve,
123126
})
124127
}
125128

@@ -165,6 +168,7 @@ impl Context {
165168
process: Process::new(),
166169
use_pty: true,
167170
noexec: false,
171+
umask: Umask::Preserve,
168172
})
169173
}
170174

@@ -181,6 +185,7 @@ impl Context {
181185
is_login: self.launch == LaunchType::Login,
182186
user: &self.target_user,
183187
group: &self.target_group,
188+
umask: self.umask,
184189

185190
use_pty: self.use_pty,
186191
noexec: self.noexec,

src/defaults/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ defaults! {
4343

4444
setenv = false
4545
apparmor_profile = None (!= None)
46+
umask = 0o022 (!= 0o777) {octal_mode}
47+
umask_override = false
4648

4749
passwd_tries = 3 [0..=1000]
4850

@@ -66,6 +68,12 @@ defaults! {
6668
"PYTHONINSPECT", "PYTHONUSERBASE", "RUBYLIB", "RUBYOPT", "*=()*"] #ignored
6769
}
6870

71+
fn octal_mode(input: &str) -> Option<i64> {
72+
<libc::mode_t>::from_str_radix(input.strip_prefix('0')?, 8)
73+
.ok()
74+
.map(Into::into)
75+
}
76+
6977
/// A custom parser to parse seconds as fractional "minutes", the format used by
7078
/// passwd_timeout and timestamp_timeout.
7179
fn fractional_minutes(input: &str) -> Option<i64> {

src/exec/mod.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use std::{
1818
};
1919

2020
use crate::{
21-
common::bin_serde::BinPipe,
21+
common::{
22+
bin_serde::BinPipe, HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2,
23+
},
2224
exec::no_pty::exec_no_pty,
2325
log::{dev_info, dev_warn, user_error},
2426
system::{
@@ -47,6 +49,18 @@ impl SpawnNoexecHandler {
4749
fn spawn(self) {}
4850
}
4951

52+
#[derive(Debug, Copy, Clone)]
53+
#[cfg_attr(test, derive(PartialEq))]
54+
#[repr(u32)]
55+
pub enum Umask {
56+
/// Keep the umask of the parent process.
57+
Preserve = HARDENED_ENUM_VALUE_0,
58+
/// Mask out more of the permission bits in the new umask.
59+
Extend(libc::mode_t) = HARDENED_ENUM_VALUE_1,
60+
/// Override the umask of the parent process entirely with the given umask.
61+
Override(libc::mode_t) = HARDENED_ENUM_VALUE_2,
62+
}
63+
5064
pub struct RunOptions<'a> {
5165
pub command: &'a Path,
5266
pub arguments: &'a [String],
@@ -55,6 +69,7 @@ pub struct RunOptions<'a> {
5569
pub is_login: bool,
5670
pub user: &'a User,
5771
pub group: &'a Group,
72+
pub umask: Umask,
5873

5974
pub use_pty: bool,
6075
pub noexec: bool,
@@ -131,6 +146,29 @@ pub fn run_command(
131146
}
132147
}
133148

149+
// SAFETY: Umask is async-signal-safe.
150+
unsafe {
151+
let umask = options.umask;
152+
153+
command.pre_exec(move || {
154+
match umask {
155+
Umask::Preserve => {}
156+
Umask::Extend(umask) => {
157+
// The only options to get the existing umask are overwriting it or
158+
// parsing a /proc file. Given that this is a single-threaded context,
159+
// overwrite it with a safe value is fine and the simpler option.
160+
let existing_umask = libc::umask(0o777);
161+
libc::umask(existing_umask | umask);
162+
}
163+
Umask::Override(umask) => {
164+
libc::umask(umask);
165+
}
166+
}
167+
168+
Ok(())
169+
});
170+
}
171+
134172
let sudo_pid = ProcessId::new(std::process::id() as i32);
135173

136174
if options.use_pty {

src/su/context.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{
77
};
88

99
use crate::common::{error::Error, resolve::CurrentUser};
10-
use crate::exec::RunOptions;
10+
use crate::exec::{RunOptions, Umask};
1111
use crate::log::user_warn;
1212
use crate::system::{Group, Process, User};
1313
use crate::{common::resolve::is_valid_executable, system::interface::UserId};
@@ -216,6 +216,7 @@ impl SuContext {
216216
is_login: self.options.login,
217217
user: &self.user,
218218
group: &self.group,
219+
umask: Umask::Preserve,
219220

220221
use_pty: true,
221222
noexec: false,

src/sudo/env/environment.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ mod tests {
280280
chdir: crate::sudoers::DirChange::Strict(None),
281281
trust_environment: false,
282282
use_pty: true,
283+
umask: crate::exec::Umask::Preserve,
283284
#[cfg(feature = "apparmor")]
284285
apparmor_profile: None,
285286
noexec: false,

src/sudo/env/tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::common::resolve::CurrentUser;
22
use crate::common::{CommandAndArguments, Context};
3+
use crate::exec::Umask;
34
use crate::sudo::{
45
cli::{SudoAction, SudoRunOptions},
56
env::environment::{get_target_environment, Environment},
@@ -134,6 +135,7 @@ fn create_test_context(sudo_options: SudoRunOptions) -> Context {
134135
use_pty: true,
135136
noexec: false,
136137
bell: false,
138+
umask: Umask::Preserve,
137139
}
138140
}
139141

@@ -170,6 +172,7 @@ fn test_environment_variable_filtering() {
170172
use_pty: true,
171173
chdir: crate::sudoers::DirChange::Strict(None),
172174
trust_environment: false,
175+
umask: crate::exec::Umask::Preserve,
173176
#[cfg(feature = "apparmor")]
174177
apparmor_profile: None,
175178
noexec: false,

src/sudo/pipeline.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ fn apply_policy_to_context(
238238
// could be needed, but here we copy these -- perhaps we should split up the Context type
239239
context.use_pty = controls.use_pty;
240240
context.noexec = controls.noexec;
241+
context.umask = controls.umask;
241242

242243
Ok(())
243244
}

src/sudoers/policy.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::Judgement;
44
use crate::common::{
55
SudoPath, HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2,
66
};
7+
use crate::exec::Umask;
78
use crate::sudoers::ast::{ExecControl, Tag};
89
use crate::system::{time::Duration, Hostname, User};
910
/// Data types and traits that represent what the "terms and conditions" are after a successful
@@ -59,6 +60,7 @@ pub struct Restrictions<'a> {
5960
pub env_check: &'a HashSet<String>,
6061
pub chdir: DirChange<'a>,
6162
pub path: Option<&'a str>,
63+
pub umask: Umask,
6264
#[cfg(feature = "apparmor")]
6365
pub apparmor_profile: Option<String>,
6466
}
@@ -109,6 +111,20 @@ impl Judgement {
109111
Some(super::ChDir::Path(path)) => DirChange::Strict(Some(path)),
110112
},
111113
path: self.settings.secure_path(),
114+
umask: {
115+
let mask = self
116+
.settings
117+
.umask()
118+
.try_into()
119+
.expect("the umask parser should have prevented overflow");
120+
if mask == 0o777 {
121+
Umask::Preserve
122+
} else if self.settings.umask_override() {
123+
Umask::Override(mask)
124+
} else {
125+
Umask::Extend(mask)
126+
}
127+
},
112128
#[cfg(feature = "apparmor")]
113129
apparmor_profile: tag
114130
.apparmor_profile

test-framework/sudo-compliance-tests/src/sudo.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ mod sudo_ps1;
2828
mod sudoers;
2929
mod syslog;
3030
mod timestamp;
31+
mod umask;
3132
mod use_pty;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use sudo_test::{Command, Env};
2+
3+
use crate::SUDOERS_ALL_ALL_NOPASSWD;
4+
5+
fn test_umask(config: &str, user_umask: &str, target_umask: &str) {
6+
let env = Env([SUDOERS_ALL_ALL_NOPASSWD, config]).build();
7+
8+
let output = Command::new("sh")
9+
.args(["-c", &format!("umask {user_umask}; sudo sh -c umask")])
10+
.output(&env);
11+
output.assert_success();
12+
13+
assert_eq!(output.stdout(), target_umask);
14+
}
15+
16+
#[test]
17+
fn umask_unchanged() {
18+
test_umask("Defaults umask=0777", "0123", "0123");
19+
test_umask("Defaults !umask", "0123", "0123");
20+
}
21+
22+
#[test]
23+
fn stricter_umask_respected() {
24+
test_umask("Defaults umask=0776", "0022", "0776");
25+
}
26+
27+
#[test]
28+
fn overlapping_umask_unioned() {
29+
test_umask("Defaults umask=0770", "0022", "0772");
30+
}
31+
32+
#[test]
33+
fn looser_umask_unchanged() {
34+
test_umask("Defaults umask=0000", "0022", "0022");
35+
}
36+
37+
#[test]
38+
fn umask_override() {
39+
test_umask(
40+
"Defaults umask=0700\nDefaults umask_override",
41+
"0022",
42+
"0700",
43+
);
44+
}
45+
46+
#[test]
47+
fn umask_override_0777() {
48+
test_umask(
49+
"Defaults umask=0777\nDefaults umask_override",
50+
"0022",
51+
"0022",
52+
);
53+
}

0 commit comments

Comments
 (0)