Skip to content

Commit a8b1fc8

Browse files
authored
Editor spawning and file writing for sudoedit (#1199)
2 parents 06cd294 + 58036ef commit a8b1fc8

File tree

4 files changed

+312
-10
lines changed

4 files changed

+312
-10
lines changed

src/sudo/edit.rs

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
#![allow(unsafe_code)]
2+
3+
use std::fs::{File, OpenOptions};
4+
use std::io::{Read, Seek, Write};
5+
use std::net::Shutdown;
6+
use std::os::unix::{fs::OpenOptionsExt, net::UnixStream, process::ExitStatusExt};
7+
use std::path::{Path, PathBuf};
8+
use std::process::Command;
9+
use std::{io, process};
10+
11+
use crate::exec::ExitReason;
12+
use crate::system::file::{create_temporary_dir, FileLock};
13+
use crate::system::wait::{Wait, WaitError, WaitOptions};
14+
use crate::system::{fork, ForkResult};
15+
16+
struct ParentFileInfo<'a> {
17+
path: &'a Path,
18+
file: File,
19+
lock: FileLock,
20+
old_data: Vec<u8>,
21+
new_data_rx: UnixStream,
22+
new_data: Option<Vec<u8>>,
23+
}
24+
25+
struct ChildFileInfo<'a> {
26+
path: &'a Path,
27+
old_data: Vec<u8>,
28+
tempfile_path: Option<PathBuf>,
29+
new_data_tx: UnixStream,
30+
}
31+
32+
pub(super) fn edit_files(editor: &Path, paths: &[&Path]) -> io::Result<ExitReason> {
33+
let mut files = vec![];
34+
let mut child_files = vec![];
35+
for path in paths {
36+
// Open file
37+
let mut file: File = OpenOptions::new()
38+
.read(true)
39+
.write(true)
40+
.create(true)
41+
.custom_flags(libc::O_NOFOLLOW)
42+
.open(path)
43+
.map_err(|e| {
44+
io::Error::new(e.kind(), format!("Failed to open {}: {e}", path.display()))
45+
})?;
46+
47+
// Error for special files
48+
let metadata = file.metadata().map_err(|e| {
49+
io::Error::new(
50+
e.kind(),
51+
format!("Failed to read metadata for {}: {e}", path.display()),
52+
)
53+
})?;
54+
if !metadata.is_file() {
55+
return Err(io::Error::new(
56+
io::ErrorKind::Other,
57+
format!("File {} is not a regular file", path.display()),
58+
));
59+
}
60+
61+
// Take file lock
62+
let lock = FileLock::exclusive(&file, true).map_err(|e| {
63+
io::Error::new(e.kind(), format!("Failed to lock {}: {e}", path.display()))
64+
})?;
65+
66+
// Read file
67+
let mut old_data = Vec::new();
68+
file.read_to_end(&mut old_data).map_err(|e| {
69+
io::Error::new(e.kind(), format!("Failed to read {}: {e}", path.display()))
70+
})?;
71+
72+
// Create socket
73+
let (parent_socket, child_socket) = UnixStream::pair().unwrap();
74+
75+
files.push(ParentFileInfo {
76+
path,
77+
file,
78+
lock,
79+
old_data: old_data.clone(),
80+
new_data_rx: parent_socket,
81+
new_data: None,
82+
});
83+
84+
child_files.push(ChildFileInfo {
85+
path,
86+
old_data,
87+
tempfile_path: None,
88+
new_data_tx: child_socket,
89+
});
90+
}
91+
92+
// Spawn child
93+
// SAFETY: There should be no other threads at this point.
94+
let ForkResult::Parent(command_pid) = unsafe { fork() }.unwrap() else {
95+
drop(files);
96+
handle_child(editor, child_files)
97+
};
98+
drop(child_files);
99+
100+
for file in &mut files {
101+
// Read from socket
102+
file.new_data =
103+
Some(read_stream(&mut file.new_data_rx).map_err(|e| {
104+
io::Error::new(e.kind(), format!("Failed to read from socket: {e}"))
105+
})?);
106+
}
107+
108+
// If child has error, exit with non-zero exit code
109+
let status = loop {
110+
match command_pid.wait(WaitOptions::new()) {
111+
Ok((_, status)) => break status,
112+
Err(WaitError::Io(err)) if err.kind() == io::ErrorKind::Interrupted => {}
113+
Err(err) => panic!("{err:?}"),
114+
}
115+
};
116+
assert!(status.did_exit());
117+
118+
if let Some(signal) = status.term_signal() {
119+
return Ok(ExitReason::Signal(signal));
120+
} else if let Some(code) = status.exit_status() {
121+
if code != 0 {
122+
return Ok(ExitReason::Code(code));
123+
}
124+
} else {
125+
return Ok(ExitReason::Code(1));
126+
}
127+
128+
for mut file in files {
129+
let data = file.new_data.expect("filled in above");
130+
if data == file.old_data {
131+
// File unchanged. No need to write it again.
132+
continue;
133+
}
134+
135+
// FIXME check if modified since reading and if so ask user what to do
136+
137+
// Write file
138+
(move || {
139+
file.file.rewind()?;
140+
file.file.write_all(&data)?;
141+
file.file.set_len(
142+
data.len()
143+
.try_into()
144+
.expect("more than 18 exabyte of data???"),
145+
)
146+
})()
147+
.map_err(|e| {
148+
io::Error::new(
149+
e.kind(),
150+
format!("Failed to write {}: {e}", file.path.display()),
151+
)
152+
})?;
153+
154+
drop(file.lock);
155+
}
156+
157+
Ok(ExitReason::Code(0))
158+
}
159+
160+
struct TempDirDropGuard(PathBuf);
161+
162+
impl Drop for TempDirDropGuard {
163+
fn drop(&mut self) {
164+
if let Err(e) = std::fs::remove_dir(&self.0) {
165+
eprintln_ignore_io_error!(
166+
"Failed to remove temporary directory {}: {e}",
167+
self.0.display(),
168+
);
169+
};
170+
}
171+
}
172+
173+
fn handle_child(editor: &Path, file: Vec<ChildFileInfo<'_>>) -> ! {
174+
match handle_child_inner(editor, file) {
175+
Ok(()) => process::exit(0),
176+
Err(err) => {
177+
eprintln_ignore_io_error!("{err}");
178+
process::exit(1);
179+
}
180+
}
181+
}
182+
183+
// FIXME maybe use pipes once std::io::pipe has been stabilized long enough.
184+
fn handle_child_inner(editor: &Path, mut files: Vec<ChildFileInfo<'_>>) -> Result<(), String> {
185+
// Drop root privileges.
186+
// SAFETY: setuid does not change any memory and only affects OS state.
187+
unsafe {
188+
libc::setuid(libc::getuid());
189+
}
190+
191+
let tempdir = TempDirDropGuard(
192+
create_temporary_dir().map_err(|e| format!("Failed to create temporary directory: {e}"))?,
193+
);
194+
195+
for file in &mut files {
196+
// Create temp file
197+
let tempfile_path = tempdir
198+
.0
199+
.join(file.path.file_name().expect("file must have filename"));
200+
let mut tempfile = std::fs::OpenOptions::new()
201+
.read(true)
202+
.write(true)
203+
.create_new(true)
204+
.open(&tempfile_path)
205+
.map_err(|e| {
206+
format!(
207+
"Failed to create temporary file {}: {e}",
208+
tempfile_path.display(),
209+
)
210+
})?;
211+
212+
// Write to temp file
213+
tempfile.write_all(&file.old_data).map_err(|e| {
214+
format!(
215+
"Failed to write to temporary file {}: {e}",
216+
tempfile_path.display(),
217+
)
218+
})?;
219+
drop(tempfile);
220+
file.tempfile_path = Some(tempfile_path);
221+
}
222+
223+
// Spawn editor
224+
let status = Command::new(editor)
225+
.args(
226+
files
227+
.iter()
228+
.map(|file| file.tempfile_path.as_ref().expect("filled in above")),
229+
)
230+
.status()
231+
.map_err(|e| format!("Failed to run editor {}: {e}", editor.display()))?;
232+
233+
if !status.success() {
234+
drop(tempdir);
235+
236+
if let Some(signal) = status.signal() {
237+
process::exit(128 + signal);
238+
}
239+
process::exit(status.code().unwrap_or(1));
240+
}
241+
242+
for mut file in files {
243+
let tempfile_path = file.tempfile_path.as_ref().expect("filled in above");
244+
245+
// Read from temp file
246+
let new_data = std::fs::read(tempfile_path).map_err(|e| {
247+
format!(
248+
"Failed to read from temporary file {}: {e}",
249+
tempfile_path.display(),
250+
)
251+
})?;
252+
253+
// FIXME preserve temporary file if the original couldn't be written to
254+
std::fs::remove_file(tempfile_path).map_err(|e| {
255+
format!(
256+
"Failed to remove temporary file {}: {e}",
257+
tempfile_path.display(),
258+
)
259+
})?;
260+
261+
// If the file has been changed to be empty, ask the user what to do.
262+
if new_data.is_empty() && new_data != file.old_data {
263+
match crate::visudo::ask_response(
264+
format!(
265+
"sudoedit: truncate {} to zero? (y/n) [n] ",
266+
file.path.display()
267+
)
268+
.as_bytes(),
269+
b"yn",
270+
) {
271+
Ok(b'y') => {}
272+
_ => {
273+
eprintln_ignore_io_error!("Not overwriting {}", file.path.display());
274+
275+
// Parent ignores write when new data matches old data
276+
write_stream(&mut file.new_data_tx, &file.old_data)
277+
.map_err(|e| format!("Failed to write data to parent: {e}"))?;
278+
279+
continue;
280+
}
281+
}
282+
}
283+
284+
// Write to socket
285+
write_stream(&mut file.new_data_tx, &new_data)
286+
.map_err(|e| format!("Failed to write data to parent: {e}"))?;
287+
}
288+
289+
process::exit(0);
290+
}
291+
292+
fn write_stream(socket: &mut UnixStream, data: &[u8]) -> io::Result<()> {
293+
socket.write_all(data)?;
294+
socket.shutdown(Shutdown::Both)?;
295+
Ok(())
296+
}
297+
298+
fn read_stream(socket: &mut UnixStream) -> io::Result<Vec<u8>> {
299+
let mut new_data = Vec::new();
300+
socket.read_to_end(&mut new_data)?;
301+
Ok(new_data)
302+
}

src/sudo/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![forbid(unsafe_code)]
1+
#![deny(unsafe_code)]
22

33
use crate::common::resolve::CurrentUser;
44
use crate::common::Error;
@@ -15,6 +15,8 @@ use std::path::PathBuf;
1515

1616
mod cli;
1717
pub(crate) use cli::{SudoEditOptions, SudoListOptions, SudoRunOptions, SudoValidateOptions};
18+
#[cfg(feature = "sudoedit")]
19+
mod edit;
1820

1921
pub(crate) mod diagnostic;
2022
mod env;

src/sudo/pipeline/edit.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,15 @@ pub fn run_edit(edit_opts: SudoEditOptions) -> Result<(), Error> {
3535

3636
let editor = policy.preferred_editor();
3737

38-
eprintln_ignore_io_error!(
39-
"this would launch sudoedit as requested, to edit the files: {:?} using editor {}",
40-
context
38+
crate::sudo::edit::edit_files(
39+
&editor,
40+
&context
4141
.files_to_edit
42-
.into_iter()
42+
.iter()
4343
.flatten()
44+
.map(|path| &**path)
4445
.collect::<Vec<_>>(),
45-
editor.display(),
46-
);
47-
48-
Ok::<_, std::io::Error>(ExitReason::Code(42))
46+
)
4947
};
5048

5149
pam_context.close_session();

src/visudo/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ fn sudo_visudo_is_allowed(mut sudoers: Sudoers, host_name: &Hostname) -> Option<
385385
}
386386

387387
// Make sure that the first valid response is the "safest" choice
388-
fn ask_response(prompt: &[u8], valid_responses: &[u8]) -> io::Result<u8> {
388+
pub(crate) fn ask_response(prompt: &[u8], valid_responses: &[u8]) -> io::Result<u8> {
389389
let stdin = io::stdin();
390390
let stdout = io::stdout();
391391
let mut stderr = io::stderr();

0 commit comments

Comments
 (0)