use std::collections::VecDeque;
use std::ffi::c_int;
use std::io;
use std::os::fd::{FromRawFd, OwnedFd};
use std::process::{Command, Stdio};
use libc::{O_CLOEXEC, close};
use crate::cutils::cerr;
use crate::exec::event::{EventHandle, EventRegistry, PollEvent, Process, StopReason};
use crate::exec::use_pty::SIGCONT_FG;
use crate::exec::use_pty::monitor::exec_monitor;
use crate::exec::{
ExitReason, SpawnNoexecHandler,
io_util::retry_while_interrupted,
use_pty::backchannel::{BackchannelPair, MonitorMessage, ParentBackchannel, ParentMessage},
};
use crate::exec::{HandleSigchld, cond_fmt, handle_sigchld, signal_fmt, terminate_process};
use crate::log::{dev_error, dev_info, dev_warn};
use crate::system::signal::{
SignalHandler, SignalHandlerBehavior, SignalNumber, SignalSet, SignalStream, SignalsState,
consts::*, register_handlers,
};
use crate::system::term::{Pty, PtyFollower, PtyLeader, TermSize, Terminal, UserTerm};
use crate::system::wait::WaitOptions;
use crate::system::{_exit, ForkResult, Group, User, chown, fork, getpgrp, kill, killpg};
use crate::system::{getpgid, interface::ProcessId};
use super::pipe::Pipe;
use super::{CommandStatus, SIGCONT_BG};
pub(in crate::exec) fn exec_pty(
sudo_pid: ProcessId,
spawn_noexec_handler: Option<SpawnNoexecHandler>,
mut command: Command,
user_tty: UserTerm,
pty_owner: &User,
background: bool,
) -> io::Result<ExitReason> {
let pty = get_pty(pty_owner)?;
let mut original_signals = SignalsState::save()?;
let mut backchannels = BackchannelPair::new().map_err(|err| {
dev_error!("cannot create backchannel: {err}");
err
})?;
match SignalHandler::register(
SIGTTIN,
SignalHandlerBehavior::Ignore,
&mut original_signals,
) {
Ok(handler) => handler.forget(),
Err(err) => dev_warn!("cannot set handler for SIGTTIN: {err}"),
}
match SignalHandler::register(
SIGTTOU,
SignalHandlerBehavior::Ignore,
&mut original_signals,
) {
Ok(handler) => handler.forget(),
Err(err) => dev_warn!("cannot set handler for SIGTTOU: {err}"),
}
let parent_pgrp = getpgrp();
let clone_follower = || -> io::Result<PtyFollower> {
pty.follower.try_clone().map_err(|err| {
dev_error!("cannot clone pty follower: {err}");
err
})
};
command.stdin(clone_follower()?);
command.stdout(clone_follower()?);
command.stderr(clone_follower()?);
let mut registry = EventRegistry::<ParentClosure>::new();
let mut tty_pipe = Pipe::new(
user_tty,
pty.leader,
&mut registry,
ParentEvent::Tty,
ParentEvent::Pty,
);
if background {
tty_pipe.disable_input(&mut registry);
}
let user_tty = tty_pipe.left_mut();
let mut foreground = user_tty
.tcgetpgrp()
.is_ok_and(|tty_pgrp| tty_pgrp == parent_pgrp);
dev_info!(
"sudo is running in the {}",
cond_fmt(foreground, "foreground", "background")
);
let mut exec_bg = false;
let mut term_raw = false;
let mut preserve_oflag = false;
if !io::stdin().is_terminal_for_pgrp(parent_pgrp) {
if background {
dev_info!("stdin is not a terminal, creating a pipe");
exec_bg = true;
let mut pipes = [-1, -1];
unsafe {
cerr(libc::pipe2(pipes.as_mut_ptr(), O_CLOEXEC))?;
}
unsafe {
command.stdin(OwnedFd::from_raw_fd(pipes[0]));
close(pipes[1]);
}
} else {
dev_info!("stdin is not a terminal, command will inherit it");
if io::stdin().is_pipe_or_socket() {
exec_bg = true;
}
command.stdin(Stdio::inherit());
}
if foreground && parent_pgrp != sudo_pid {
exec_bg = true;
}
}
if !io::stdout().is_terminal_for_pgrp(parent_pgrp) {
dev_info!("stdout is not a terminal, command will inherit it");
if io::stdout().is_pipe_or_socket() {
exec_bg = true;
preserve_oflag = true;
}
command.stdout(Stdio::inherit());
}
if !io::stderr().is_terminal_for_pgrp(parent_pgrp) {
dev_info!("stderr is not a terminal, command will inherit it");
command.stderr(Stdio::inherit());
}
if io::stdout().is_pipe_or_socket() {
foreground = false;
}
if let Err(err) = user_tty.copy_to(&pty.follower) {
dev_error!("cannot copy terminal settings to pty: {err}");
foreground = false;
}
if foreground && !exec_bg {
if user_tty.set_raw_mode(false, preserve_oflag).is_ok() {
term_raw = true;
}
}
let tty_size = tty_pipe.left().get_size().map_err(|err| {
dev_error!("cannot get terminal size: {err}");
err
})?;
let original_set = match SignalSet::full().and_then(|set| set.block()) {
Ok(original_set) => Some(original_set),
Err(err) => {
dev_warn!("cannot block signals: {err}");
None
}
};
if !foreground {
tty_pipe.disable_input(&mut registry);
}
let ForkResult::Parent(monitor_pid) = (unsafe { fork() }).map_err(|err| {
dev_error!("cannot fork monitor process: {err}");
err
})?
else {
drop(tty_pipe);
drop(backchannels.parent);
match exec_monitor(
pty.follower,
command,
foreground && !exec_bg,
&mut backchannels.monitor,
original_set,
original_signals,
) {
Ok(exec_output) => match exec_output {},
Err(err) => {
backchannels.monitor.set_nonblocking_assertions(true);
match err.try_into() {
Ok(msg) => {
if let Err(err) = backchannels.monitor.send(&msg) {
dev_error!("cannot send status to parent: {err}");
}
}
Err(err) => {
dev_warn!("execution error {err:?} cannot be send over backchannel")
}
}
}
}
_exit(1);
};
if let Some(spawner) = spawn_noexec_handler {
spawner.spawn();
}
drop(pty.follower);
drop(backchannels.monitor);
retry_while_interrupted(|| backchannels.parent.send(&MonitorMessage::Edge)).map_err(|err| {
dev_error!("cannot send green light to monitor: {err}");
err
})?;
let mut closure = ParentClosure::new(
monitor_pid,
sudo_pid,
parent_pgrp,
backchannels.parent,
tty_pipe,
tty_size,
foreground,
term_raw,
preserve_oflag,
&mut registry,
original_signals,
)?;
if let Some(set) = original_set {
if let Err(err) = set.set_mask() {
dev_warn!("cannot restore signal mask: {err}");
}
}
let exit_reason = closure.run(registry);
closure.tty_pipe.right().set_nonblocking()?;
closure.tty_pipe.flush_left().ok();
if closure.term_raw {
if let Ok(pgrp) = closure.tty_pipe.left().tcgetpgrp() {
if pgrp == closure.parent_pgrp {
match closure.tty_pipe.left_mut().restore(false) {
Ok(()) => closure.term_raw = false,
Err(err) => dev_warn!("cannot restore terminal settings: {err}"),
}
}
}
}
drop(closure.signal_handlers);
exit_reason
}
fn get_pty(pty_owner: &User) -> io::Result<Pty> {
let tty_gid = Group::from_name(c"tty")
.unwrap_or(None)
.map(|group| group.gid);
let pty = Pty::open().map_err(|err| {
dev_error!("cannot allocate pty: {err}");
io::Error::new(io::ErrorKind::NotFound, "unable to open pty")
})?;
let gid = tty_gid.unwrap_or(User::effective_gid());
chown(&pty.path, pty_owner.uid, gid).map_err(|err| {
dev_error!("cannot change owner for pty: {err}");
err
})?;
Ok(pty)
}
struct ParentClosure {
monitor_pid: Option<ProcessId>,
sudo_pid: ProcessId,
parent_pgrp: ProcessId,
command_pid: Option<ProcessId>,
tty_pipe: Pipe<UserTerm, PtyLeader>,
tty_size: TermSize,
foreground: bool,
term_raw: bool,
preserve_oflag: bool,
backchannel: ParentBackchannel,
message_queue: VecDeque<MonitorMessage>,
backchannel_write_handle: EventHandle,
original_signals: SignalsState,
signal_stream: &'static SignalStream,
signal_handlers: [SignalHandler; ParentClosure::SIGNALS.len()],
}
impl ParentClosure {
const SIGNALS: [SignalNumber; 11] = [
SIGINT, SIGQUIT, SIGTSTP, SIGTERM, SIGHUP, SIGALRM, SIGUSR1, SIGUSR2, SIGCHLD, SIGCONT,
SIGWINCH,
];
#[allow(clippy::too_many_arguments)]
fn new(
monitor_pid: ProcessId,
sudo_pid: ProcessId,
parent_pgrp: ProcessId,
mut backchannel: ParentBackchannel,
tty_pipe: Pipe<UserTerm, PtyLeader>,
tty_size: TermSize,
foreground: bool,
term_raw: bool,
preserve_oflag: bool,
registry: &mut EventRegistry<Self>,
mut original_signals: SignalsState,
) -> io::Result<Self> {
backchannel.set_nonblocking_asserts(true);
registry.register_event(&backchannel, PollEvent::Readable, ParentEvent::Backchannel);
let mut backchannel_write_handle =
registry.register_event(&backchannel, PollEvent::Writable, ParentEvent::Backchannel);
backchannel_write_handle.ignore(registry);
let signal_stream = SignalStream::init()?;
registry.register_event(signal_stream, PollEvent::Readable, |_| ParentEvent::Signal);
let signal_handlers = register_handlers(Self::SIGNALS, &mut original_signals)?;
Ok(Self {
monitor_pid: Some(monitor_pid),
sudo_pid,
parent_pgrp,
command_pid: None,
tty_pipe,
tty_size,
foreground,
term_raw,
preserve_oflag,
backchannel,
message_queue: VecDeque::new(),
backchannel_write_handle,
original_signals,
signal_stream,
signal_handlers,
})
}
fn run(&mut self, registry: EventRegistry<Self>) -> io::Result<ExitReason> {
let result = match registry.event_loop(self) {
StopReason::Break(err) | StopReason::Exit(ParentExit::Backchannel(err)) => Err(err),
StopReason::Exit(ParentExit::Command(exit_reason)) => Ok(exit_reason),
};
retry_while_interrupted(|| self.backchannel.send(&MonitorMessage::Edge)).map_err(
|err| {
dev_error!("cannot send red light to monitor: {err}");
err
},
)?;
result
}
fn on_message_received(&mut self, registry: &mut EventRegistry<Self>) {
match self.backchannel.recv() {
Err(err) => {
match err.kind() {
io::ErrorKind::UnexpectedEof => {
dev_info!("received EOF from backchannel");
registry.set_exit(err.into());
}
io::ErrorKind::Interrupted => {}
_ => {
dev_error!("cannot receive message from backchannel: {err}");
if !registry.got_break() {
registry.set_break(err);
}
}
}
}
Ok(event) => {
match event {
ParentMessage::CommandPid(pid) => {
dev_info!("received command PID ({pid}) from monitor");
self.command_pid = pid.into();
}
ParentMessage::CommandStatus(status) => {
match status {
CommandStatus::Exit(exit_code) => {
dev_info!("command exited with status code {exit_code}");
registry.set_exit(ExitReason::Code(exit_code).into());
}
CommandStatus::Term(signal) => {
dev_info!("command was terminated by {}", signal_fmt(signal));
registry.set_exit(ExitReason::Signal(signal).into());
}
CommandStatus::Stop(signal) => {
dev_info!(
"command was stopped by {}, suspending parent",
signal_fmt(signal)
);
if let Some(signal) = self.suspend_pty(signal, registry) {
self.schedule_signal(signal, registry);
}
self.tty_pipe.resume_events(registry);
}
}
}
ParentMessage::IoError(code) => {
let err = io::Error::from_raw_os_error(code);
dev_info!("received error ({code}) for monitor: {err}");
registry.set_break(err);
}
ParentMessage::ShortRead => {
dev_info!("received short read error for monitor");
registry.set_break(io::ErrorKind::UnexpectedEof.into());
}
}
}
}
}
fn is_self_terminating(&self, signaler_pid: ProcessId) -> bool {
if signaler_pid.is_valid() {
if Some(signaler_pid) == self.command_pid {
return true;
}
if let Ok(signaler_pgrp) = getpgid(signaler_pid) {
if Some(signaler_pgrp) == self.command_pid || signaler_pgrp == self.sudo_pid {
return true;
}
}
}
false
}
fn schedule_signal(&mut self, signal: c_int, registry: &mut EventRegistry<Self>) {
dev_info!("scheduling message with {} for monitor", signal_fmt(signal));
self.message_queue.push_back(MonitorMessage::Signal(signal));
self.backchannel_write_handle.resume(registry);
}
fn check_message_queue(&mut self, registry: &mut EventRegistry<Self>) {
if let Some(msg) = self.message_queue.front() {
dev_info!("sending message {msg:?} to monitor over backchannel");
match self.backchannel.send(msg) {
Ok(()) => {
self.message_queue.pop_front().unwrap();
if self.message_queue.is_empty() {
self.backchannel_write_handle.ignore(registry);
}
}
Err(err) => {
if err.kind() != io::ErrorKind::Interrupted {
dev_error!("cannot send via backchannel {err}");
registry.set_break(err);
}
}
}
}
}
fn suspend_pty(
&mut self,
signal: SignalNumber,
registry: &mut EventRegistry<Self>,
) -> Option<SignalNumber> {
let sigcont_handler = SignalHandler::register(
SIGCONT,
SignalHandlerBehavior::Ignore,
&mut self.original_signals,
)
.map_err(|err| dev_warn!("cannot set handler for SIGCONT: {err}"))
.ok();
if let SIGTTOU | SIGTTIN = signal {
if !self.foreground && self.check_foreground().is_err() {
return None;
}
if self.foreground {
dev_info!(
"command received {}, parent running in the foreground",
signal_fmt(signal)
);
if !self.term_raw {
if self
.tty_pipe
.left_mut()
.set_raw_mode(false, self.preserve_oflag)
.is_ok()
{
self.term_raw = true;
}
self.tty_pipe.enable_input(registry);
return Some(SIGCONT_FG);
}
}
}
self.tty_pipe.ignore_events(registry);
if self.term_raw {
match self.tty_pipe.left_mut().restore(false) {
Ok(()) => self.term_raw = false,
Err(err) => dev_warn!("cannot restore terminal settings: {err}"),
}
}
let signal_handler = if signal != SIGSTOP {
SignalHandler::register(
signal,
SignalHandlerBehavior::Default,
&mut self.original_signals,
)
.map_err(|err| dev_warn!("cannot set handler for {}: {err}", signal_fmt(signal)))
.ok()
} else {
None
};
if self.parent_pgrp != self.sudo_pid && kill(self.parent_pgrp, 0).is_err()
|| killpg(self.parent_pgrp, signal).is_err()
{
dev_error!("no parent to suspend, terminating command");
if let Some(command_pid) = self.command_pid.take() {
terminate_process(command_pid, true);
}
}
drop(signal_handler);
if self.command_pid.is_none() || self.resume_terminal().is_err() {
return None;
}
let ret_signal = if self.term_raw {
SIGCONT_FG
} else {
SIGCONT_BG
};
drop(sigcont_handler);
Some(ret_signal)
}
fn check_foreground(&mut self) -> io::Result<()> {
let pgrp = self.tty_pipe.left().tcgetpgrp()?;
self.foreground = pgrp == self.parent_pgrp;
Ok(())
}
fn resume_terminal(&mut self) -> io::Result<()> {
self.check_foreground()?;
self.tty_pipe
.left()
.copy_to(self.tty_pipe.right())
.map_err(|err| {
dev_error!("cannot copy terminal settings to pty: {err}");
err
})?;
dev_info!(
"parent is in {} ({} -> {})",
cond_fmt(self.foreground, "foreground", "background"),
cond_fmt(self.term_raw, "raw", "cooked"),
cond_fmt(self.foreground, "raw", "cooked"),
);
if self.foreground {
if self
.tty_pipe
.left_mut()
.set_raw_mode(false, self.preserve_oflag)
.is_ok()
{
self.term_raw = true;
}
} else {
self.term_raw = false;
}
Ok(())
}
fn on_signal(&mut self, registry: &mut EventRegistry<Self>) {
let info = match self.signal_stream.recv() {
Ok(info) => info,
Err(err) => {
dev_error!("parent could not receive signal: {err}");
return;
}
};
dev_info!("parent received{}", info);
let Some(monitor_pid) = self.monitor_pid else {
dev_info!("monitor was terminated, ignoring signal");
return;
};
match info.signal() {
SIGCHLD => handle_sigchld(self, registry, "monitor", monitor_pid),
SIGCONT => {
self.resume_terminal().ok();
}
SIGWINCH => {
if let Err(err) = self.handle_sigwinch() {
dev_warn!("cannot resize terminal: {}", err);
}
}
signal => {
if let Some(pid) = info.signaler_pid() {
if self.is_self_terminating(pid) {
return;
}
}
self.schedule_signal(signal, registry)
}
}
}
fn handle_sigwinch(&mut self) -> io::Result<()> {
let new_size = self.tty_pipe.left().get_size()?;
if new_size != self.tty_size {
dev_info!("updating pty size from {} to {new_size}", self.tty_size);
self.tty_pipe.right().set_size(&new_size)?;
if let Some(command_pid) = self.command_pid {
killpg(command_pid, SIGWINCH).ok();
}
self.tty_size = new_size;
}
Ok(())
}
}
enum ParentExit {
Backchannel(io::Error),
Command(ExitReason),
}
impl From<io::Error> for ParentExit {
fn from(err: io::Error) -> Self {
Self::Backchannel(err)
}
}
impl From<ExitReason> for ParentExit {
fn from(reason: ExitReason) -> Self {
Self::Command(reason)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParentEvent {
Signal,
Tty(PollEvent),
Pty(PollEvent),
Backchannel(PollEvent),
}
impl Process for ParentClosure {
type Event = ParentEvent;
type Break = io::Error;
type Exit = ParentExit;
fn on_event(&mut self, event: Self::Event, registry: &mut EventRegistry<Self>) {
match event {
ParentEvent::Signal => self.on_signal(registry),
ParentEvent::Tty(poll_event) => {
if self.tty_pipe.left().tcgetsid().is_err() {
dev_warn!("tty gone (closed/detached), ignoring future events");
self.tty_pipe.ignore_events(registry);
} else {
self.tty_pipe.on_left_event(poll_event, registry).ok();
}
}
ParentEvent::Pty(poll_event) => {
self.tty_pipe.on_right_event(poll_event, registry).ok();
}
ParentEvent::Backchannel(poll_event) => match poll_event {
PollEvent::Readable => self.on_message_received(registry),
PollEvent::Writable => self.check_message_queue(registry),
},
}
}
}
impl HandleSigchld for ParentClosure {
const OPTIONS: WaitOptions = WaitOptions::new().all().untraced().no_hang();
fn on_exit(&mut self, _exit_code: c_int, _registry: &mut EventRegistry<Self>) {
self.monitor_pid = None;
}
fn on_term(&mut self, _signal: SignalNumber, _registry: &mut EventRegistry<Self>) {
self.monitor_pid = None;
}
fn on_stop(&mut self, signal: SignalNumber, registry: &mut EventRegistry<Self>) {
if let Some(signal) = self.suspend_pty(signal, registry) {
self.schedule_signal(signal, registry);
}
self.tty_pipe.resume_events(registry);
}
}