Isolate interactive terminal session ownership
This commit is contained in:
parent
b240bc1d12
commit
690738d846
1 changed files with 280 additions and 24 deletions
|
|
@ -5,6 +5,7 @@ use std::os::fd::AsFd;
|
|||
use std::process::{ExitCode, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Barrier};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use lazaret_core::identity::{
|
||||
CoordinatorId, DomainId, GenerationId, HostPidLocator, PidfsProcessIdentityV1,
|
||||
|
|
@ -13,14 +14,14 @@ use lazaret_core::records::{
|
|||
HostProcessOwnershipSlot, ProcessOwnershipCommitAck, StagedProcessOwnership,
|
||||
};
|
||||
use lazaret_linux::process::{
|
||||
AcknowledgedChildAcquisition, ChildStandardIo, ProcessOwnershipContext, TargetCommand,
|
||||
append_parent_identity, child_broker, current_process_identity, dispatch_private_process_role,
|
||||
verify_direct_child_pidfd,
|
||||
AcknowledgedChildAcquisition, ChildStandardIo, ProcessOwnershipContext, RetainedProcess,
|
||||
TargetCommand, append_parent_identity, child_broker, current_process_identity,
|
||||
dispatch_private_process_role, verify_direct_child_pidfd,
|
||||
};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
||||
use rustix::pipe::{PipeFlags, pipe_with};
|
||||
use rustix::process::{PidfdFlags, pidfd_open};
|
||||
use rustix::process::{PidfdFlags, Signal, pidfd_open};
|
||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
||||
use rustix::termios::{Winsize, tcgetwinsize, tcsetwinsize};
|
||||
|
||||
|
|
@ -52,6 +53,8 @@ fn run_all() -> std::io::Result<()> {
|
|||
mapped_transient_uses_configured_subordinates()?;
|
||||
transient_captured_child_preserves_streams_and_exit()?;
|
||||
terminal_child_restores_slave_stdin_and_merges_output()?;
|
||||
terminal_children_isolate_window_signals()?;
|
||||
bootstrap_child_uses_a_distinct_process_group()?;
|
||||
target_stdin_does_not_gain_write_authority()?;
|
||||
acknowledged_transition_precedes_exact_wrapper_release()?;
|
||||
failed_acknowledged_transition_refuses_exact_wrapper_release()?;
|
||||
|
|
@ -64,6 +67,33 @@ fn run_all() -> std::io::Result<()> {
|
|||
concurrent_acquisitions_serialize()
|
||||
}
|
||||
|
||||
fn bootstrap_child_uses_a_distinct_process_group() -> std::io::Result<()> {
|
||||
let (reader, writer) = pipe_with(PipeFlags::CLOEXEC)?;
|
||||
let target = TargetCommand::new("/bin/sh")
|
||||
.argument("-c")
|
||||
.argument(
|
||||
"if [ \"$(ps -o pgid= -p $$)\" = \"$(ps -o pgid= -p $PPID)\" ]; then printf E; else printf R; fi; read value || exit 0",
|
||||
)
|
||||
.with_lifetime_pipe_parent_edge();
|
||||
let mut child = child_broker().spawn_bootstrap(&target, File::from(reader))?;
|
||||
if child.bootstrap_status()? != b'R' {
|
||||
return Err(std::io::Error::other(
|
||||
"bootstrap child shared the client process group",
|
||||
));
|
||||
}
|
||||
drop(writer);
|
||||
while !child.has_exited()? {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
let exit = child.reap()?;
|
||||
if exit.exit_code != Some(0) || exit.terminating_signal.is_some() {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"bootstrap process-group probe failed: {exit:?}"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mapped_transient_uses_configured_subordinates() -> std::io::Result<()> {
|
||||
let map = UserNamespaceMap::resolve_current()?;
|
||||
child_broker()
|
||||
|
|
@ -149,7 +179,8 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
|
|||
.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()?)?
|
||||
.with_controlling_terminal();
|
||||
let expected_parent = current_process_identity()?;
|
||||
let ownership = ProcessOwnershipContext {
|
||||
domain_id: DomainId::new([41; 32]),
|
||||
|
|
@ -193,6 +224,133 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn terminal_children_isolate_window_signals() -> std::io::Result<()> {
|
||||
let (mut first_master, first) = terminal_child("S1", 51)?;
|
||||
let (mut second_master, second) = terminal_child("S2", 52)?;
|
||||
let mut first_output = Vec::new();
|
||||
let mut second_output = Vec::new();
|
||||
wait_for_terminal_marker(&mut first_master, &mut first_output, b"S1_READY")?;
|
||||
wait_for_terminal_marker(&mut second_master, &mut second_output, b"S2_READY")?;
|
||||
let first_identity = terminal_identity(&first_output, "S1_READY")?;
|
||||
let second_identity = terminal_identity(&second_output, "S2_READY")?;
|
||||
if first_identity
|
||||
.iter()
|
||||
.any(|value| *value != first_identity[0])
|
||||
|| second_identity
|
||||
.iter()
|
||||
.any(|value| *value != second_identity[0])
|
||||
|| first_identity[0] == second_identity[0]
|
||||
{
|
||||
return Err(std::io::Error::other(
|
||||
"terminal targets did not receive distinct controlling sessions",
|
||||
));
|
||||
}
|
||||
first.send_signal(Signal::WINCH)?;
|
||||
wait_for_terminal_marker(&mut first_master, &mut first_output, b"S1_WINCH")?;
|
||||
second_master.write_all(b"after\n")?;
|
||||
wait_for_terminal_marker(&mut second_master, &mut second_output, b"S2:after")?;
|
||||
if second_output
|
||||
.windows(b"S2_WINCH".len())
|
||||
.any(|part| part == b"S2_WINCH")
|
||||
{
|
||||
return Err(std::io::Error::other(
|
||||
"window signal crossed terminal process groups",
|
||||
));
|
||||
}
|
||||
while !first.has_exited()? {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
let first_exit = child_broker().reap_status(first)?;
|
||||
second.send_signal(Signal::KILL)?;
|
||||
let second_exit = child_broker().reap_status(second)?;
|
||||
if first_exit.exit_code != Some(0)
|
||||
|| first_exit.terminating_signal.is_some()
|
||||
|| second_exit.terminating_signal.is_none()
|
||||
{
|
||||
return Err(std::io::Error::other(
|
||||
"terminal signal isolation produced unexpected exits",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn terminal_child(marker: &str, seed: u8) -> std::io::Result<(File, RetainedProcess)> {
|
||||
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)?;
|
||||
grantpt(&master)?;
|
||||
unlockpt(&master)?;
|
||||
let slave = File::from(ioctl_tiocgptpeer(
|
||||
&master,
|
||||
OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC,
|
||||
)?);
|
||||
let target = TargetCommand::new("/usr/bin/python3")
|
||||
.argument("-c")
|
||||
.argument(format!(
|
||||
"import os, signal, sys\nmarker = {marker:?}\ndef winch(*_):\n print(marker + '_WINCH', flush=True)\n sys.exit(0)\nsignal.signal(signal.SIGWINCH, winch)\nprint(f'{{marker}}_READY pid={{os.getpid()}} sid={{os.getsid(0)}} pgrp={{os.getpgrp()}} tty={{os.tcgetpgrp(0)}}', flush=True)\nfor line in sys.stdin:\n print(marker + ':' + line.strip(), flush=True)"
|
||||
))
|
||||
.with_target_stdin(slave.try_clone()?)?
|
||||
.with_controlling_terminal();
|
||||
let process = child_broker().acquire_with_stdio(
|
||||
current_process_identity()?,
|
||||
ProcessOwnershipContext {
|
||||
domain_id: DomainId::new([seed; 32]),
|
||||
generation_id: GenerationId::new(u64::from(seed)),
|
||||
coordinator_id: CoordinatorId::new([seed.wrapping_add(1); 32]),
|
||||
slot: HostProcessOwnershipSlot::SessionTerminalExecClient(
|
||||
lazaret_core::identity::SessionId::new([seed.wrapping_add(2); 32]),
|
||||
),
|
||||
},
|
||||
&target,
|
||||
ChildStandardIo::terminal(slave)?,
|
||||
|_| Ok(()),
|
||||
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
|
||||
)?;
|
||||
let master = File::from(master);
|
||||
let flags = fcntl_getfl(&master)?;
|
||||
fcntl_setfl(&master, flags | OFlags::NONBLOCK)?;
|
||||
Ok((master, process))
|
||||
}
|
||||
|
||||
fn terminal_identity(output: &[u8], marker: &str) -> std::io::Result<[u32; 4]> {
|
||||
let text = String::from_utf8_lossy(output);
|
||||
let line = text
|
||||
.lines()
|
||||
.find(|line| line.starts_with(marker))
|
||||
.ok_or_else(|| std::io::Error::other("terminal identity marker is missing"))?;
|
||||
let values = line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.map(|field| {
|
||||
field
|
||||
.split_once('=')
|
||||
.and_then(|(_, value)| value.parse::<u32>().ok())
|
||||
.ok_or_else(|| std::io::Error::other("terminal identity field is invalid"))
|
||||
})
|
||||
.collect::<std::io::Result<Vec<_>>>()?;
|
||||
values
|
||||
.try_into()
|
||||
.map_err(|_| std::io::Error::other("terminal identity field count is invalid"))
|
||||
}
|
||||
|
||||
fn wait_for_terminal_marker(
|
||||
master: &mut File,
|
||||
output: &mut Vec<u8>,
|
||||
marker: &[u8],
|
||||
) -> std::io::Result<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while !output.windows(marker.len()).any(|part| part == marker) {
|
||||
read_available(master, output)?;
|
||||
if Instant::now() >= deadline {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"terminal marker {:?} deadline expired with {:?}",
|
||||
String::from_utf8_lossy(marker),
|
||||
String::from_utf8_lossy(output)
|
||||
)));
|
||||
}
|
||||
std::thread::yield_now();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transient_captured_child_preserves_streams_and_exit() -> std::io::Result<()> {
|
||||
let target = TargetCommand::new("/bin/sh")
|
||||
.argument("-c")
|
||||
|
|
|
|||
Loading…
Reference in a new issue