Skip to content

Commit 372315f

Browse files
committed
fork and exec the command
1 parent 95093c4 commit 372315f

File tree

1 file changed

+123
-25
lines changed

1 file changed

+123
-25
lines changed

sudo/lib/exec/monitor.rs

Lines changed: 123 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
use std::{
22
ffi::c_int,
3-
io,
4-
os::fd::OwnedFd,
5-
process::{exit, Child, Command},
3+
io::{self, Read, Write},
4+
os::{
5+
fd::OwnedFd,
6+
unix::{net::UnixStream, process::CommandExt},
7+
},
8+
process::{exit, Command},
69
time::Duration,
710
};
811

912
use crate::log::{dev_info, dev_warn};
1013
use crate::system::{
11-
getpgid, interface::ProcessId, kill, setpgid, setsid, signal::SignalInfo,
12-
term::set_controlling_terminal,
14+
fork, getpgid,
15+
interface::ProcessId,
16+
kill, setpgid, setsid,
17+
signal::SignalInfo,
18+
term::{set_controlling_terminal, tcgetpgrp, tcsetpgrp},
19+
wait::{waitpid, WaitError, WaitOptions},
1320
};
1421

1522
use signal_hook::consts::*;
@@ -24,7 +31,7 @@ use super::{cond_fmt, signal_fmt};
2431
// FIXME: This should return `io::Result<!>` but `!` is not stable yet.
2532
pub(super) fn exec_monitor(
2633
pty_follower: OwnedFd,
27-
mut command: Command,
34+
command: Command,
2835
backchannel: &mut MonitorBackchannel,
2936
) -> io::Result<()> {
3037
let mut dispatcher = EventDispatcher::<MonitorClosure>::new()?;
@@ -46,6 +53,9 @@ pub(super) fn exec_monitor(
4653
err
4754
})?;
4855

56+
// Use a pipe to get the IO error if `exec_command` fails.
57+
let (mut errpipe_tx, errpipe_rx) = UnixStream::pair()?;
58+
4959
// Wait for the parent to give us green light before spawning the command. This avoids race
5060
// conditions when the command exits quickly.
5161
let event = retry_while_interrupted(|| backchannel.recv()).map_err(|err| {
@@ -58,22 +68,35 @@ pub(super) fn exec_monitor(
5868

5969
// FIXME (ogsudo): Some extra config happens here if selinux is available.
6070

61-
// FIXME (ogsudo): Do any additional configuration that needs to be run after `fork` but before `exec`.
62-
63-
// spawn the command.
64-
let command = command.spawn().map_err(|err| {
65-
dev_warn!("cannot spawn command: {err}");
71+
let command_pid = fork().map_err(|err| {
72+
dev_warn!("unable to fork command process: {err}");
6673
err
6774
})?;
6875

69-
let command_pid = command.id() as ProcessId;
76+
if command_pid == 0 {
77+
drop(errpipe_rx);
78+
79+
let err = exec_command(command, pty_follower);
80+
dev_warn!("failed to execute command: {err}");
81+
// If `exec_command` returns, it means that executing the command failed. Send the error to
82+
// the monitor using the pipe.
83+
if let Some(error_code) = err.raw_os_error() {
84+
errpipe_tx.write_all(&error_code.to_ne_bytes()).ok();
85+
}
86+
drop(errpipe_tx);
87+
// FIXME: Calling `exit` doesn't run any destructors, clean everything up.
88+
exit(1)
89+
}
7090

7191
// Send the command's PID to the parent.
7292
if let Err(err) = backchannel.send(&ParentMessage::CommandPid(command_pid)) {
7393
dev_warn!("cannot send command PID to parent: {err}");
7494
}
7595

76-
let mut closure = MonitorClosure::new(command, command_pid, backchannel, &mut dispatcher);
96+
let mut closure = MonitorClosure::new(command_pid, errpipe_rx, backchannel, &mut dispatcher);
97+
98+
// Set the foreground group for the pty follower.
99+
tcsetpgrp(&pty_follower, closure.command_pgrp).ok();
77100

78101
// FIXME (ogsudo): Here's where the signal mask is removed because the handlers for the signals
79102
// have been setup after initializing the closure.
@@ -91,25 +114,45 @@ pub(super) fn exec_monitor(
91114
exit(1)
92115
}
93116

117+
// FIXME: This should return `io::Result<!>` but `!` is not stable yet.
118+
fn exec_command(mut command: Command, pty_follower: OwnedFd) -> io::Error {
119+
// FIXME (ogsudo): Do any additional configuration that needs to be run after `fork` but before `exec`
120+
let command_pid = std::process::id() as ProcessId;
121+
122+
setpgid(0, command_pid).ok();
123+
124+
// Wait for the monitor to set us as the foreground group for the pty.
125+
while !tcgetpgrp(&pty_follower).is_ok_and(|pid| pid == command_pid) {
126+
std::thread::sleep(std::time::Duration::from_micros(1));
127+
}
128+
129+
command.exec()
130+
}
131+
94132
struct MonitorClosure<'a> {
95-
command: Child,
96133
/// The command PID.
97134
///
98135
/// This is `Some` iff the process is still running.
99136
command_pid: Option<ProcessId>,
100137
command_pgrp: ProcessId,
138+
errpipe_rx: UnixStream,
101139
backchannel: &'a mut MonitorBackchannel,
102140
}
103141

104142
impl<'a> MonitorClosure<'a> {
105143
fn new(
106-
command: Child,
107144
command_pid: ProcessId,
145+
errpipe_rx: UnixStream,
108146
backchannel: &'a mut MonitorBackchannel,
109147
dispatcher: &mut EventDispatcher<Self>,
110148
) -> Self {
111149
// FIXME (ogsudo): Store the pgid of the monitor.
112150

151+
// Register the callback to receive the IO error if the command fails to execute.
152+
dispatcher.set_read_callback(&errpipe_rx, |monitor, dispatcher| {
153+
monitor.read_errpipe(dispatcher)
154+
});
155+
113156
// Register the callback to receive events from the backchannel
114157
dispatcher.set_read_callback(backchannel, |monitor, dispatcher| {
115158
monitor.read_backchannel(dispatcher)
@@ -122,9 +165,9 @@ impl<'a> MonitorClosure<'a> {
122165
};
123166

124167
Self {
125-
command,
126168
command_pid: Some(command_pid),
127169
command_pgrp,
170+
errpipe_rx,
128171
backchannel,
129172
}
130173
}
@@ -154,7 +197,67 @@ impl<'a> MonitorClosure<'a> {
154197
}
155198
}
156199
}
200+
201+
fn handle_sigchld(&mut self, command_pid: ProcessId) {
202+
let status = loop {
203+
match waitpid(command_pid, WaitOptions::new().untraced().no_hang()) {
204+
Ok((_pid, status)) => break status,
205+
Err(WaitError::Io(err)) if was_interrupted(&err) => {}
206+
Err(_) => return,
207+
}
208+
};
209+
210+
if let Some(exit_code) = status.exit_status() {
211+
dev_info!("command ({command_pid}) exited with status code {exit_code}");
212+
// The command did exit, set it's PID to `None`.
213+
self.command_pid = None;
214+
self.backchannel
215+
.send(&ParentMessage::CommandExit(exit_code))
216+
.ok();
217+
} else if let Some(signal) = status.term_signal() {
218+
dev_info!(
219+
"command ({command_pid}) was terminated by {}",
220+
signal_fmt(signal),
221+
);
222+
// The command was terminated, set it's PID to `None`.
223+
self.command_pid = None;
224+
self.backchannel
225+
.send(&ParentMessage::CommandSignal(signal))
226+
.ok();
227+
} else if let Some(_signal) = status.stop_signal() {
228+
// FIXME: we should save the foreground process group ID so we can restore it later.
229+
dev_info!(
230+
"command ({command_pid}) was stopped by {}",
231+
signal_fmt(_signal),
232+
);
233+
} else if status.did_continue() {
234+
dev_info!("command ({command_pid}) continued execution");
235+
} else {
236+
dev_warn!("unexpected wait status for command ({command_pid})")
237+
}
238+
}
239+
240+
fn read_errpipe(&mut self, dispatcher: &mut EventDispatcher<Self>) {
241+
let mut buf = 0i32.to_ne_bytes();
242+
match self.errpipe_rx.read_exact(&mut buf) {
243+
Err(err) if was_interrupted(&err) => { /* Retry later */ }
244+
Err(err) => {
245+
// Could not read from the pipe, report error to the parent.
246+
// FIXME: Maybe we should have a different variant for this.
247+
self.backchannel.send(&err.into()).ok();
248+
dispatcher.set_break(());
249+
}
250+
Ok(_) => {
251+
// Received error code from the command, forward it to the parent.
252+
let error_code = i32::from_ne_bytes(buf);
253+
self.backchannel
254+
.send(&ParentMessage::IoError(error_code))
255+
.ok();
256+
}
257+
}
258+
}
157259
}
260+
158261
/// Send a signal to the command.
159262
fn send_signal(signal: c_int, command_pid: ProcessId, from_parent: bool) {
160263
dev_info!(
@@ -221,17 +324,12 @@ impl<'a> EventClosure for MonitorClosure<'a> {
221324
};
222325

223326
match info.signal() {
224-
// FIXME: check `mon_handle_sigchld`
225327
SIGCHLD => {
226-
if let Ok(Some(exit_status)) = self.command.try_wait() {
227-
dev_info!(
228-
"command ({command_pid}) exited with status: {}",
229-
exit_status
230-
);
231-
// The command has terminated, set it's PID to `None`.
232-
self.command_pid = None;
328+
self.handle_sigchld(command_pid);
329+
if self.command_pid.is_none() {
330+
// FIXME: This is what ogsudo calls a `loopexit`. Meaning that we should still
331+
// dispatch the remaining events to their callbacks and exit nicely.
233332
dispatcher.set_break(());
234-
self.backchannel.send(&exit_status.into()).unwrap();
235333
}
236334
}
237335
// Skip the signal if it was sent by the user and it is self-terminating.

0 commit comments

Comments
 (0)