Skip to content

Commit 6a36b16

Browse files
authored
Detect "system editor" at runtime on Linux (#1392)
2 parents 29946bf + 7ac0718 commit 6a36b16

File tree

6 files changed

+44
-51
lines changed

6 files changed

+44
-51
lines changed

docs/man/sudoers.5.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ sudo's behavior can be modified by Default_Entry lines, as explained earlier. A
430430

431431
* editor
432432

433-
A colon (‘:’) separated list of editor path names used by **sudoedit** and **visudo**. For **sudoedit**, this list is used to find an editor when none of the SUDO_EDITOR, VISUAL or EDITOR environment variables are set to an editor that exists and is executable. For **visudo**, it is used as a white list of allowed editors; **visudo** will choose the editor that matches the user's SUDO_EDITOR, VISUAL or EDITOR environment variable if possible, or the first editor in the list that exists and is executable if not. Unless invoked as **sudoedit**, sudo does not preserve the SUDO_EDITOR, VISUAL or EDITOR environment variables unless they are present in the **env_keep** list. The default on Linux is _/usr/bin/editor_, on FreeBSD _/usr/vim/vi_.
433+
A colon (‘:’) separated list of editor path names used by **sudoedit** and **visudo**. For **sudoedit**, this list is used to find an editor when none of the SUDO_EDITOR, VISUAL or EDITOR environment variables are set to an editor that exists and is executable. For **visudo**, it is used as a white list of allowed editors; **visudo** will choose the editor that matches the user's SUDO_EDITOR, VISUAL or EDITOR environment variable if possible, or the first editor in the list that exists and is executable if not. Unless invoked as **sudoedit**, sudo does not preserve the SUDO_EDITOR, VISUAL or EDITOR environment variables unless they are present in the **env_keep** list. The default on Linux is _/usr/bin/editor:/usr/bin/nano:/usr/bin/vi_. On FreeBSD the default is _/usr/bin/vi_.
434434

435435
## Strings that can be used in a boolean context:
436436

src/defaults/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use settings_dsl::{
2222
};
2323

2424
pub const SYSTEM_EDITOR: &str = if cfg!(target_os = "linux") {
25-
"/usr/bin/editor"
25+
"/usr/bin/editor:/usr/bin/nano:/usr/bin/vi"
2626
} else {
2727
"/usr/bin/vi"
2828
};

src/sudoers/mod.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ impl Sudoers {
295295
on_host: &system::Hostname,
296296
am_user: &User,
297297
target_user: &User,
298-
) -> PathBuf {
298+
) -> Option<PathBuf> {
299299
self.specify_host_user_runas(on_host, am_user, Some(target_user));
300300

301301
select_editor(&self.settings, self.settings.env_editor())
@@ -304,7 +304,7 @@ impl Sudoers {
304304

305305
/// Retrieve the chosen editor from a settings object, filtering based on whether the
306306
/// environment is trusted (sudoedit) or maybe less so (visudo)
307-
fn select_editor(settings: &Settings, trusted_env: bool) -> PathBuf {
307+
fn select_editor(settings: &Settings, trusted_env: bool) -> Option<PathBuf> {
308308
let blessed_editors = settings.editor();
309309

310310
let is_whitelisted = |path: &Path| -> bool {
@@ -329,7 +329,7 @@ fn select_editor(settings: &Settings, trusted_env: bool) -> PathBuf {
329329
};
330330

331331
if is_whitelisted(&editor) {
332-
return editor;
332+
return Some(editor);
333333
}
334334
}
335335
}
@@ -339,13 +339,11 @@ fn select_editor(settings: &Settings, trusted_env: bool) -> PathBuf {
339339
for editor in blessed_editors.split(':') {
340340
let editor = PathBuf::from(editor);
341341
if is_valid_executable(&editor) {
342-
return editor;
342+
return Some(editor);
343343
}
344344
}
345345

346-
// fallback on hardcoded path -- always provide something to the caller
347-
348-
PathBuf::from(defaults::SYSTEM_EDITOR)
346+
None
349347
}
350348

351349
// a `take_while` variant that does not consume the first non-matching item

src/sudoers/policy.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ impl Judgement {
147147
}
148148

149149
pub(crate) fn preferred_editor(&self) -> std::path::PathBuf {
150+
//if no editor could be selected, fall back to /bin/vi;
151+
//note that /bin/vi is also likely to have been tried as part of
152+
//the "editor" setting, so this is a last-resort
150153
super::select_editor(&self.settings, true)
154+
.unwrap_or_else(|| std::path::PathBuf::from("/usr/bin/vi"))
151155
}
152156
}
153157

src/visudo/mod.rs

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -146,32 +146,27 @@ fn run(file_arg: Option<&str>, perms: bool, owner: bool) -> io::Result<()> {
146146
.read(true)
147147
.write(true)
148148
.open(sudoers_path)
149-
.map_err(|e| {
150-
io::Error::new(
151-
e.kind(),
152-
format!("Failed to open existing sudoers file at {sudoers_path:?}: {e}"),
149+
.map_err(|err| {
150+
io_msg!(
151+
err,
152+
"Failed to open existing sudoers file at {sudoers_path:?}"
153153
)
154154
})?;
155155

156156
(file, true)
157157
} else {
158158
// Create a sudoers file if it doesn't exist.
159-
let file = File::create(sudoers_path).map_err(|e| {
160-
io::Error::new(
161-
e.kind(),
162-
format!("Failed to create sudoers file at {sudoers_path:?}: {e}"),
163-
)
164-
})?;
159+
let file = File::create(sudoers_path)
160+
.map_err(|err| io_msg!(err, "Failed to create sudoers file at {sudoers_path:?}"))?;
161+
165162
// ogvisudo sets the permissions of the file so it can be read and written by the user and
166163
// read by the group if the `-f` argument was passed.
167164
if file_arg.is_some() {
168165
file.set_permissions(Permissions::from_mode(0o640))
169-
.map_err(|e| {
170-
io::Error::new(
171-
e.kind(),
172-
format!(
173-
"Failed to set permissions on new sudoers file at {sudoers_path:?}: {e}"
174-
),
166+
.map_err(|err| {
167+
io_msg!(
168+
err,
169+
"Failed to set permissions on new sudoers file at {sudoers_path:?}"
175170
)
176171
})?;
177172
}
@@ -260,21 +255,21 @@ fn edit_sudoers_file(
260255

261256
let host_name = Hostname::resolve();
262257

263-
let editor_path = if existed {
258+
if existed {
264259
// If the sudoers file existed, read its contents and write them into the temporary file.
265260
sudoers_file.read_to_end(&mut sudoers_contents)?;
266261
// Rewind the sudoers file so it can be written later.
267262
sudoers_file.rewind()?;
268263
// Write to the temporary file.
269264
tmp_file.write_all(&sudoers_contents)?;
265+
}
270266

271-
let (sudoers, _errors) = Sudoers::read(sudoers_contents.as_slice(), sudoers_path)?;
272-
273-
sudoers.visudo_editor_path(&host_name, &current_user, &current_user)
274-
} else {
275-
// there is no /etc/sudoers config yet, so use a system default
276-
PathBuf::from(crate::defaults::SYSTEM_EDITOR)
277-
};
267+
let editor_path = Sudoers::read(sudoers_contents.as_slice(), sudoers_path)?
268+
.0
269+
.visudo_editor_path(&host_name, &current_user, &current_user)
270+
.ok_or_else(|| {
271+
io::Error::new(io::ErrorKind::NotFound, "no usable editor could be found")
272+
})?;
278273

279274
loop {
280275
Command::new(&editor_path)

test-framework/sudo-compliance-tests/src/visudo/sudoers/editor.rs

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,7 @@ fn no_valid_editor_in_list() {
7171
);
7272
} else {
7373
// this output shows that visudo has rejected /dev/null as an editor
74-
assert_eq!(
75-
"visudo: specified editor (/usr/bin/editor) could not be used",
76-
output.stderr()
77-
);
74+
assert_eq!("visudo: no usable editor could be found", output.stderr());
7875
}
7976
}
8077

@@ -92,22 +89,23 @@ fn editors_must_be_specified_by_absolute_path() {
9289
);
9390
} else {
9491
// this output shows that visudo has rejected /dev/null as an editor
95-
assert_eq!(
96-
"visudo: specified editor (/usr/bin/editor) could not be used",
97-
output.stderr()
98-
);
92+
assert_eq!("visudo: no usable editor could be found", output.stderr());
9993
}
10094
}
10195

10296
#[test]
10397
fn on_invalid_editor_does_not_falls_back_to_configured_default_value() {
104-
let env = Env("Defaults editor=true")
98+
let expected = "configured editor was called";
99+
//NOTE: ogsudo DOES fallback to the default editor if a syntax error is
100+
//made here, e.g. "Defaults editor=true" will simply not set the "editor" value.
101+
//This is surprising.
102+
let env = Env("Defaults editor=/bin/no-editor")
105103
.file(
106104
DEFAULT_EDITOR,
107-
TextFile(
105+
TextFile(format!(
108106
"#!/bin/sh
109-
rm -f {LOGS_PATH}",
110-
)
107+
echo '{expected}' >> {LOGS_PATH}"
108+
))
111109
.chmod(CHMOD_EXEC),
112110
)
113111
.build();
@@ -117,11 +115,9 @@ rm -f {LOGS_PATH}",
117115
.output(&env)
118116
.assert_success();
119117

120-
Command::new("visudo").output(&env).assert_success();
118+
Command::new("visudo").output(&env).assert_exit_code(1);
121119

122-
Command::new("sh")
123-
.arg("-c")
124-
.arg(format!("test -f {LOGS_PATH}"))
125-
.output(&env)
126-
.assert_success();
120+
let actual = Command::new("cat").arg(LOGS_PATH).output(&env).stdout();
121+
122+
assert_ne!(expected, actual);
127123
}

0 commit comments

Comments
 (0)