Preserve Session Terminal TTY through causal launch

This commit is contained in:
ms2k 2026-07-17 16:54:42 +09:00
commit 39307f4281
4 changed files with 87 additions and 8 deletions

View file

@ -48,7 +48,7 @@ fn run_session_exec(arguments: &[OsString]) -> io::Result<()> {
}) })
.ok_or_else(|| io::Error::other("session exec requires -- and a nonempty argv"))?; .ok_or_else(|| io::Error::other("session exec requires -- and a nonempty argv"))?;
let _ = separator; let _ = separator;
rustix::process::setsid()?; establish_process_group_leader()?;
let target = argv[1..] let target = argv[1..]
.iter() .iter()
.cloned() .cloned()
@ -56,6 +56,26 @@ fn run_session_exec(arguments: &[OsString]) -> io::Result<()> {
Err(exec_target(target)) Err(exec_target(target))
} }
fn establish_process_group_leader() -> io::Result<()> {
let pid = rustix::process::getpid();
if rustix::process::getpgrp() != pid {
rustix::process::setsid()?;
}
require_process_group_leader(pid, rustix::process::getpgrp())
}
fn require_process_group_leader(
pid: rustix::process::Pid,
process_group: rustix::process::Pid,
) -> io::Result<()> {
if pid != process_group {
return Err(io::Error::other(
"session exec is not its process-group leader",
));
}
Ok(())
}
fn report_session_identity(arguments: &[OsString]) -> io::Result<()> { fn report_session_identity(arguments: &[OsString]) -> io::Result<()> {
match arguments { match arguments {
[pid] => report_process_identity(parse_positive_pid(pid)?), [pid] => report_process_identity(parse_positive_pid(pid)?),
@ -179,6 +199,14 @@ mod tests {
); );
} }
#[test]
fn process_group_leader_validation_is_fail_closed() {
let leader = rustix::process::Pid::from_raw(41).unwrap();
let other_group = rustix::process::Pid::from_raw(42).unwrap();
require_process_group_leader(leader, leader).unwrap();
assert!(require_process_group_leader(leader, other_group).is_err());
}
#[test] #[test]
fn group_presence_is_independent_of_a_designated_leader() { fn group_presence_is_independent_of_a_designated_leader() {
let (_, _, group) = read_process_identity(std::process::id()).unwrap().unwrap(); let (_, _, group) = read_process_identity(std::process::id()).unwrap().unwrap();

View file

@ -305,7 +305,7 @@ fn run_session_terminal_matrix() -> Result<(), MatrixError> {
session.session_phase = SessionPhase::Attached; session.session_phase = SessionPhase::Attached;
fixture fixture
.store .store
.apply(&[DomainStoreMutation::PutSession(session)]) .apply(&[DomainStoreMutation::PutSession(session.clone())])
.map_err(failure)?; .map_err(failure)?;
let command = fixture.bundle.command(); let command = fixture.bundle.command();
let run = command let run = command
@ -383,7 +383,35 @@ fn run_session_terminal_matrix() -> Result<(), MatrixError> {
) )
.map_err(failure)?; .map_err(failure)?;
} }
wait_for_signal_lines(&signals, 4)?; wait_for_signal_lines(&mut engine, session_id, &signals, 4)?;
let mut forwarded_before_prompt = 0_usize;
for _ in 0..2_000 {
let messages = engine
.poll(session_id, GenerationId::new(7))
.map_err(failure)?;
if messages.is_empty() {
if forwarded_before_prompt > 0 {
break;
}
thread::sleep(Duration::from_millis(5));
continue;
}
for message in messages {
if let SessionTerminalMessage::Output(chunk) = message {
forwarded_before_prompt += chunk.bytes.len();
}
}
}
if forwarded_before_prompt == 0 {
return Err(failure(
"sandbox route did not forward buffered output before Prompt",
));
}
session.session_phase = SessionPhase::Prompting;
fixture
.store
.apply(&[DomainStoreMutation::PutSession(session.clone())])
.map_err(failure)?;
engine engine
.set_route(session_id, GenerationId::new(7), TerminalRoute::Prompt) .set_route(session_id, GenerationId::new(7), TerminalRoute::Prompt)
.map_err(failure)?; .map_err(failure)?;
@ -394,6 +422,11 @@ fn run_session_terminal_matrix() -> Result<(), MatrixError> {
if before != after { if before != after {
return Err(failure("prompt route did not apply PTY backpressure")); return Err(failure("prompt route did not apply PTY backpressure"));
} }
session.session_phase = SessionPhase::Attached;
fixture
.store
.apply(&[DomainStoreMutation::PutSession(session)])
.map_err(failure)?;
engine engine
.set_route(session_id, GenerationId::new(7), TerminalRoute::Sandbox) .set_route(session_id, GenerationId::new(7), TerminalRoute::Sandbox)
.map_err(failure)?; .map_err(failure)?;
@ -470,8 +503,16 @@ fn wait_for_path(path: &Path) -> Result<(), MatrixError> {
Err(failure("Session Terminal guest did not become ready")) Err(failure("Session Terminal guest did not become ready"))
} }
fn wait_for_signal_lines(path: &Path, expected: usize) -> Result<(), MatrixError> { fn wait_for_signal_lines(
engine: &mut SessionTerminalEngine,
session_id: SessionId,
path: &Path,
expected: usize,
) -> Result<(), MatrixError> {
for _ in 0..2_000 { for _ in 0..2_000 {
engine
.poll(session_id, GenerationId::new(7))
.map_err(failure)?;
if fs::read_to_string(path) if fs::read_to_string(path)
.ok() .ok()
.is_some_and(|value| value.lines().count() >= expected) .is_some_and(|value| value.lines().count() >= expected)

View file

@ -1376,7 +1376,12 @@ pub fn parse_wrapper_arguments(
if arguments.next().as_deref() != Some(OsStr::new("--")) { if arguments.next().as_deref() != Some(OsStr::new("--")) {
return Err(io::Error::other("wrapper target separator is missing")); return Err(io::Error::other("wrapper target separator is missing"));
} }
Some(File::open(format!("/proc/self/fd/{descriptor}"))?) Some(
File::options()
.read(true)
.write(true)
.open(format!("/proc/self/fd/{descriptor}"))?,
)
} else if next.as_deref() == Some(OsStr::new("--")) { } else if next.as_deref() == Some(OsStr::new("--")) {
None None
} else { } else {

View file

@ -88,7 +88,9 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
let slave = File::from(slave); let slave = File::from(slave);
let target = TargetCommand::new("/bin/sh") let target = TargetCommand::new("/bin/sh")
.argument("-c") .argument("-c")
.argument("read value; printf 'out:%s\\n' \"$value\"; printf 'err\\n' >&2") .argument(
"read value; printf 'stdin-fd\\n' >&0; printf 'out:%s\\n' \"$value\"; printf 'err\\n' >&2",
)
.with_target_stdin(slave.try_clone()?)?; .with_target_stdin(slave.try_clone()?)?;
let expected_parent = current_process_identity()?; let expected_parent = current_process_identity()?;
let ownership = ProcessOwnershipContext { let ownership = ProcessOwnershipContext {
@ -117,10 +119,13 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
let _ = read_available(&mut master, &mut merged); let _ = read_available(&mut master, &mut merged);
let exit = child_broker().reap_status(process)?; let exit = child_broker().reap_status(process)?;
let text = String::from_utf8_lossy(&merged); let text = String::from_utf8_lossy(&merged);
let stdin = text.find("stdin-fd");
let out = text.find("out:hello"); let out = text.find("out:hello");
let err = text.find("err"); let err = text.find("err");
if !matches!((out, err), (Some(out), Some(err)) if out < err) if !matches!(
|| exit.exit_code != Some(0) (stdin, out, err),
(Some(stdin), Some(out), Some(err)) if stdin < out && out < err
) || exit.exit_code != Some(0)
|| exit.terminating_signal.is_some() || exit.terminating_signal.is_some()
{ {
return Err(std::io::Error::other(format!( return Err(std::io::Error::other(format!(