Isolate interactive terminal session ownership

This commit is contained in:
ms2k 2026-08-01 00:00:47 +09:00
commit 690738d846
3 changed files with 280 additions and 24 deletions

View file

@ -33,6 +33,7 @@ pub struct TargetCommand {
parent_edge: ParentEdge,
executable: Option<OwnedFd>,
stdin: Option<File>,
terminal: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -106,6 +107,7 @@ impl TargetCommand {
parent_edge: ParentEdge::ParentDeathKill,
executable: None,
stdin: None,
terminal: false,
}
}
@ -124,6 +126,7 @@ impl TargetCommand {
parent_edge: ParentEdge::ParentDeathKill,
executable: Some(executable),
stdin: None,
terminal: false,
})
}
@ -156,6 +159,12 @@ impl TargetCommand {
Ok(self)
}
#[must_use]
pub fn with_controlling_terminal(mut self) -> Self {
self.terminal = true;
self
}
#[must_use]
pub const fn parent_edge(&self) -> ParentEdge {
self.parent_edge
@ -180,6 +189,7 @@ impl TargetCommand {
ParentEdge::ParentDeathKill => "kill",
ParentEdge::LifetimePipe => "lifetime-pipe",
})
.args(self.terminal.then_some("--target-terminal"))
.args(
self.stdin
.as_ref()

View file

@ -16,6 +16,7 @@ use super::{
use crate::ipc::private_stream_pair;
use crate::user_namespace::UserNamespaceMap;
use std::io::{Read as _, Write as _};
use std::os::unix::process::CommandExt as _;
impl ChildBroker {
/// Spawns the private Host Coordinator bootstrap with exact pipe ownership and pidfd reaping.
@ -44,7 +45,8 @@ impl ChildBroker {
.args(&target.arguments)
.stdin(Stdio::from(lifetime_reader))
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
.stderr(Stdio::inherit())
.process_group(0);
let mut child = command.spawn()?;
let status = child
.stdout
@ -743,6 +745,7 @@ fn run_mapped_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
rustix::process::Uid::ROOT,
rustix::process::Uid::ROOT,
)?;
establish_controlling_terminal(&target)?;
if let Some(stdin) = target.stdin.as_ref() {
rustix::stdio::dup2_stdin(stdin)?;
}
@ -761,12 +764,27 @@ fn run_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
require_lifetime_pipe_and_recheck(expected_parent)?;
}
}
establish_controlling_terminal(&target)?;
if let Some(stdin) = target.stdin.as_ref() {
rustix::stdio::dup2_stdin(stdin)?;
}
Err(exec_target(target))
}
fn establish_controlling_terminal(target: &TargetCommand) -> io::Result<()> {
if !target.terminal {
return Ok(());
}
let stdin = target
.stdin
.as_ref()
.ok_or_else(|| io::Error::other("terminal target stdin is missing"))?;
rustix::process::setsid()?;
rustix::process::ioctl_tiocsctty(stdin)?;
rustix::termios::tcsetpgrp(stdin, rustix::process::getpgrp())?;
Ok(())
}
fn require_lifetime_pipe_and_recheck(
expected_parent: PidfsProcessIdentityV1,
) -> io::Result<RetainedProcess> {

View file

@ -91,7 +91,11 @@ pub fn parse_wrapper_arguments(
Some("lifetime-pipe") => ParentEdge::LifetimePipe,
_ => return Err(io::Error::other("wrapper parent action is invalid")),
};
let next = arguments.next();
let mut next = arguments.next();
let terminal = next.as_deref() == Some(OsStr::new("--target-terminal"));
if terminal {
next = arguments.next();
}
let stdin = if next.as_deref() == Some(OsStr::new("--target-stdin-fd")) {
let descriptor = arguments
.next()
@ -117,6 +121,7 @@ pub fn parse_wrapper_arguments(
parent_edge,
executable: None,
stdin,
terminal,
};
let boot = parse_boot_hex(&boot)?;
Ok((