Isolate interactive terminal session ownership
This commit is contained in:
parent
b240bc1d12
commit
690738d846
8 changed files with 280 additions and 24 deletions
|
|
@ -422,7 +422,13 @@ impl ClientBackend for CoordinatorClientBackend {
|
||||||
} else {
|
} else {
|
||||||
execute_over_control_face_as(&endpoint, argv, stdout, stderr, &self.identity)?
|
execute_over_control_face_as(&endpoint, argv, stdout, stderr, &self.identity)?
|
||||||
};
|
};
|
||||||
if let Some(coordinator) = self.coordinator.take() {
|
self.bootstrap_owner.take();
|
||||||
|
if let Some(coordinator) = self.coordinator.take()
|
||||||
|
&& coordinator
|
||||||
|
.has_exited()
|
||||||
|
.map_err(failure)
|
||||||
|
.map_err(ClientError::Backend)?
|
||||||
|
{
|
||||||
let status = coordinator
|
let status = coordinator
|
||||||
.reap()
|
.reap()
|
||||||
.map_err(failure)
|
.map_err(failure)
|
||||||
|
|
@ -433,7 +439,6 @@ impl ClientBackend for CoordinatorClientBackend {
|
||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.bootstrap_owner.take();
|
|
||||||
Ok(exit)
|
Ok(exit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,6 @@ impl SessionTerminalEngine {
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
) -> Result<Vec<SessionTerminalMessage>, TerminalError> {
|
) -> Result<Vec<SessionTerminalMessage>, TerminalError> {
|
||||||
let authoritative = self.refresh_route(session_id, generation_id)?;
|
|
||||||
let terminal = self.active_mut(session_id, generation_id)?;
|
let terminal = self.active_mut(session_id, generation_id)?;
|
||||||
if terminal.process.exec_exited()? {
|
if terminal.process.exec_exited()? {
|
||||||
return self.close(
|
return self.close(
|
||||||
|
|
@ -158,6 +157,8 @@ impl SessionTerminalEngine {
|
||||||
GuestReleaseAuthority::Validate,
|
GuestReleaseAuthority::Validate,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let authoritative = self.refresh_route(session_id, generation_id)?;
|
||||||
|
let terminal = self.active_mut(session_id, generation_id)?;
|
||||||
if authoritative != TerminalRoute::Sandbox || terminal.releasing {
|
if authoritative != TerminalRoute::Sandbox || terminal.releasing {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
@ -446,6 +447,13 @@ pub(super) fn release_physical(
|
||||||
} else {
|
} else {
|
||||||
guest_authorized = false;
|
guest_authorized = false;
|
||||||
}
|
}
|
||||||
|
if matches!(
|
||||||
|
failure.as_ref(),
|
||||||
|
Some(TerminalError::GuestIdentityMismatch | TerminalError::GuestGroupStillPresent)
|
||||||
|
) {
|
||||||
|
failure = None;
|
||||||
|
guest_authorized = false;
|
||||||
|
}
|
||||||
if let Some(error) = failure {
|
if let Some(error) = failure {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ impl SessionTerminalFactory for RunscSessionTerminalFactory {
|
||||||
target = match slave
|
target = match slave
|
||||||
.try_clone()
|
.try_clone()
|
||||||
.and_then(|stdin| target.with_target_stdin(stdin))
|
.and_then(|stdin| target.with_target_stdin(stdin))
|
||||||
|
.map(lazaret_linux::process::TargetCommand::with_controlling_terminal)
|
||||||
{
|
{
|
||||||
Ok(target) => target,
|
Ok(target) => target,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
|
||||||
|
|
@ -321,15 +321,20 @@ fn changed_guest_identity_refuses_numeric_group_action() {
|
||||||
Err(TerminalError::GuestIdentityMismatch)
|
Err(TerminalError::GuestIdentityMismatch)
|
||||||
));
|
));
|
||||||
assert!(factory.log.lock().unwrap().is_empty());
|
assert!(factory.log.lock().unwrap().is_empty());
|
||||||
assert!(matches!(
|
let messages = engine
|
||||||
engine.handle(
|
.handle(
|
||||||
GenerationId::new(7),
|
GenerationId::new(7),
|
||||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||||
session_id: session,
|
session_id: session,
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
Err(TerminalError::GuestIdentityMismatch)
|
.unwrap();
|
||||||
));
|
assert!(
|
||||||
|
messages
|
||||||
|
.iter()
|
||||||
|
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
|
||||||
|
);
|
||||||
|
assert!(!engine.contains(session));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||||
SessionTerminalState::ReleaseIntent
|
SessionTerminalState::ReleaseIntent
|
||||||
|
|
@ -337,7 +342,7 @@ fn changed_guest_identity_refuses_numeric_group_action() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn absent_leader_with_live_group_retains_release_until_group_is_empty() {
|
fn absent_leader_with_live_group_closes_with_release_intent() {
|
||||||
let session = id(6);
|
let session = id(6);
|
||||||
let (fixture, factory, mut engine) = setup(&[session]);
|
let (fixture, factory, mut engine) = setup(&[session]);
|
||||||
engine.acquire(&acquisition(session)).unwrap();
|
engine.acquire(&acquisition(session)).unwrap();
|
||||||
|
|
@ -347,25 +352,7 @@ fn absent_leader_with_live_group_retains_release_until_group_is_empty() {
|
||||||
state.guest.leader_present = false;
|
state.guest.leader_present = false;
|
||||||
state.guest.group_present = true;
|
state.guest.group_present = true;
|
||||||
}
|
}
|
||||||
assert!(matches!(
|
let messages = engine
|
||||||
engine.handle(
|
|
||||||
GenerationId::new(7),
|
|
||||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
|
||||||
session_id: session,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Err(TerminalError::GuestIdentityMismatch)
|
|
||||||
));
|
|
||||||
let state = factory.state(0);
|
|
||||||
assert!(state.lock().unwrap().resources.exec_reaped);
|
|
||||||
assert!(!state.lock().unwrap().resources.pty_open);
|
|
||||||
assert_eq!(
|
|
||||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
|
||||||
SessionTerminalState::ReleaseIntent
|
|
||||||
);
|
|
||||||
|
|
||||||
state.lock().unwrap().guest.group_present = false;
|
|
||||||
engine
|
|
||||||
.handle(
|
.handle(
|
||||||
GenerationId::new(7),
|
GenerationId::new(7),
|
||||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||||
|
|
@ -373,9 +360,73 @@ fn absent_leader_with_live_group_retains_release_until_group_is_empty() {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let state = factory.state(0);
|
||||||
|
assert!(state.lock().unwrap().resources.exec_reaped);
|
||||||
|
assert!(!state.lock().unwrap().resources.pty_open);
|
||||||
|
assert!(
|
||||||
|
messages
|
||||||
|
.iter()
|
||||||
|
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
|
||||||
|
);
|
||||||
|
assert!(!engine.contains(session));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||||
SessionTerminalState::Clean
|
SessionTerminalState::ReleaseIntent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exited_exec_with_absent_leader_emits_exit_and_retains_group_release() {
|
||||||
|
let session = id(6);
|
||||||
|
let (fixture, factory, mut engine) = setup(&[session]);
|
||||||
|
engine.acquire(&acquisition(session)).unwrap();
|
||||||
|
{
|
||||||
|
let state = factory.state(0);
|
||||||
|
let mut state = state.lock().unwrap();
|
||||||
|
state.guest.leader_present = false;
|
||||||
|
state.guest.group_present = true;
|
||||||
|
state.resources.exec_exited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let messages = engine.poll(session, GenerationId::new(7)).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
messages
|
||||||
|
.iter()
|
||||||
|
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
|
||||||
|
);
|
||||||
|
assert!(!engine.contains(session));
|
||||||
|
assert_eq!(
|
||||||
|
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||||
|
SessionTerminalState::ReleaseIntent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persistent_guest_group_closes_session_with_release_intent() {
|
||||||
|
let session = id(6);
|
||||||
|
let (fixture, factory, mut engine) = setup(&[session]);
|
||||||
|
engine.acquire(&acquisition(session)).unwrap();
|
||||||
|
factory.state(0).lock().unwrap().guest.clears_after_signal = "never";
|
||||||
|
|
||||||
|
let messages = engine
|
||||||
|
.handle(
|
||||||
|
GenerationId::new(7),
|
||||||
|
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||||
|
session_id: session,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
messages
|
||||||
|
.iter()
|
||||||
|
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
|
||||||
|
);
|
||||||
|
assert!(!engine.contains(session));
|
||||||
|
assert_eq!(
|
||||||
|
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||||
|
SessionTerminalState::ReleaseIntent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ pub struct TargetCommand {
|
||||||
parent_edge: ParentEdge,
|
parent_edge: ParentEdge,
|
||||||
executable: Option<OwnedFd>,
|
executable: Option<OwnedFd>,
|
||||||
stdin: Option<File>,
|
stdin: Option<File>,
|
||||||
|
terminal: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
|
@ -106,6 +107,7 @@ impl TargetCommand {
|
||||||
parent_edge: ParentEdge::ParentDeathKill,
|
parent_edge: ParentEdge::ParentDeathKill,
|
||||||
executable: None,
|
executable: None,
|
||||||
stdin: None,
|
stdin: None,
|
||||||
|
terminal: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,6 +126,7 @@ impl TargetCommand {
|
||||||
parent_edge: ParentEdge::ParentDeathKill,
|
parent_edge: ParentEdge::ParentDeathKill,
|
||||||
executable: Some(executable),
|
executable: Some(executable),
|
||||||
stdin: None,
|
stdin: None,
|
||||||
|
terminal: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,6 +159,12 @@ impl TargetCommand {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_controlling_terminal(mut self) -> Self {
|
||||||
|
self.terminal = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn parent_edge(&self) -> ParentEdge {
|
pub const fn parent_edge(&self) -> ParentEdge {
|
||||||
self.parent_edge
|
self.parent_edge
|
||||||
|
|
@ -180,6 +189,7 @@ impl TargetCommand {
|
||||||
ParentEdge::ParentDeathKill => "kill",
|
ParentEdge::ParentDeathKill => "kill",
|
||||||
ParentEdge::LifetimePipe => "lifetime-pipe",
|
ParentEdge::LifetimePipe => "lifetime-pipe",
|
||||||
})
|
})
|
||||||
|
.args(self.terminal.then_some("--target-terminal"))
|
||||||
.args(
|
.args(
|
||||||
self.stdin
|
self.stdin
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use super::{
|
||||||
use crate::ipc::private_stream_pair;
|
use crate::ipc::private_stream_pair;
|
||||||
use crate::user_namespace::UserNamespaceMap;
|
use crate::user_namespace::UserNamespaceMap;
|
||||||
use std::io::{Read as _, Write as _};
|
use std::io::{Read as _, Write as _};
|
||||||
|
use std::os::unix::process::CommandExt as _;
|
||||||
|
|
||||||
impl ChildBroker {
|
impl ChildBroker {
|
||||||
/// Spawns the private Host Coordinator bootstrap with exact pipe ownership and pidfd reaping.
|
/// Spawns the private Host Coordinator bootstrap with exact pipe ownership and pidfd reaping.
|
||||||
|
|
@ -44,7 +45,8 @@ impl ChildBroker {
|
||||||
.args(&target.arguments)
|
.args(&target.arguments)
|
||||||
.stdin(Stdio::from(lifetime_reader))
|
.stdin(Stdio::from(lifetime_reader))
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::inherit());
|
.stderr(Stdio::inherit())
|
||||||
|
.process_group(0);
|
||||||
let mut child = command.spawn()?;
|
let mut child = command.spawn()?;
|
||||||
let status = child
|
let status = child
|
||||||
.stdout
|
.stdout
|
||||||
|
|
@ -743,6 +745,7 @@ fn run_mapped_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
|
||||||
rustix::process::Uid::ROOT,
|
rustix::process::Uid::ROOT,
|
||||||
rustix::process::Uid::ROOT,
|
rustix::process::Uid::ROOT,
|
||||||
)?;
|
)?;
|
||||||
|
establish_controlling_terminal(&target)?;
|
||||||
if let Some(stdin) = target.stdin.as_ref() {
|
if let Some(stdin) = target.stdin.as_ref() {
|
||||||
rustix::stdio::dup2_stdin(stdin)?;
|
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)?;
|
require_lifetime_pipe_and_recheck(expected_parent)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
establish_controlling_terminal(&target)?;
|
||||||
if let Some(stdin) = target.stdin.as_ref() {
|
if let Some(stdin) = target.stdin.as_ref() {
|
||||||
rustix::stdio::dup2_stdin(stdin)?;
|
rustix::stdio::dup2_stdin(stdin)?;
|
||||||
}
|
}
|
||||||
Err(exec_target(target))
|
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(
|
fn require_lifetime_pipe_and_recheck(
|
||||||
expected_parent: PidfsProcessIdentityV1,
|
expected_parent: PidfsProcessIdentityV1,
|
||||||
) -> io::Result<RetainedProcess> {
|
) -> io::Result<RetainedProcess> {
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,11 @@ pub fn parse_wrapper_arguments(
|
||||||
Some("lifetime-pipe") => ParentEdge::LifetimePipe,
|
Some("lifetime-pipe") => ParentEdge::LifetimePipe,
|
||||||
_ => return Err(io::Error::other("wrapper parent action is invalid")),
|
_ => 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 stdin = if next.as_deref() == Some(OsStr::new("--target-stdin-fd")) {
|
||||||
let descriptor = arguments
|
let descriptor = arguments
|
||||||
.next()
|
.next()
|
||||||
|
|
@ -117,6 +121,7 @@ pub fn parse_wrapper_arguments(
|
||||||
parent_edge,
|
parent_edge,
|
||||||
executable: None,
|
executable: None,
|
||||||
stdin,
|
stdin,
|
||||||
|
terminal,
|
||||||
};
|
};
|
||||||
let boot = parse_boot_hex(&boot)?;
|
let boot = parse_boot_hex(&boot)?;
|
||||||
Ok((
|
Ok((
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use std::os::fd::AsFd;
|
||||||
use std::process::{ExitCode, Stdio};
|
use std::process::{ExitCode, Stdio};
|
||||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Barrier};
|
use std::sync::{Arc, Barrier};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use lazaret_core::identity::{
|
use lazaret_core::identity::{
|
||||||
CoordinatorId, DomainId, GenerationId, HostPidLocator, PidfsProcessIdentityV1,
|
CoordinatorId, DomainId, GenerationId, HostPidLocator, PidfsProcessIdentityV1,
|
||||||
|
|
@ -13,14 +14,14 @@ use lazaret_core::records::{
|
||||||
HostProcessOwnershipSlot, ProcessOwnershipCommitAck, StagedProcessOwnership,
|
HostProcessOwnershipSlot, ProcessOwnershipCommitAck, StagedProcessOwnership,
|
||||||
};
|
};
|
||||||
use lazaret_linux::process::{
|
use lazaret_linux::process::{
|
||||||
AcknowledgedChildAcquisition, ChildStandardIo, ProcessOwnershipContext, TargetCommand,
|
AcknowledgedChildAcquisition, ChildStandardIo, ProcessOwnershipContext, RetainedProcess,
|
||||||
append_parent_identity, child_broker, current_process_identity, dispatch_private_process_role,
|
TargetCommand, append_parent_identity, child_broker, current_process_identity,
|
||||||
verify_direct_child_pidfd,
|
dispatch_private_process_role, verify_direct_child_pidfd,
|
||||||
};
|
};
|
||||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
||||||
use rustix::pipe::{PipeFlags, pipe_with};
|
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::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
||||||
use rustix::termios::{Winsize, tcgetwinsize, tcsetwinsize};
|
use rustix::termios::{Winsize, tcgetwinsize, tcsetwinsize};
|
||||||
|
|
||||||
|
|
@ -52,6 +53,8 @@ fn run_all() -> std::io::Result<()> {
|
||||||
mapped_transient_uses_configured_subordinates()?;
|
mapped_transient_uses_configured_subordinates()?;
|
||||||
transient_captured_child_preserves_streams_and_exit()?;
|
transient_captured_child_preserves_streams_and_exit()?;
|
||||||
terminal_child_restores_slave_stdin_and_merges_output()?;
|
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()?;
|
target_stdin_does_not_gain_write_authority()?;
|
||||||
acknowledged_transition_precedes_exact_wrapper_release()?;
|
acknowledged_transition_precedes_exact_wrapper_release()?;
|
||||||
failed_acknowledged_transition_refuses_exact_wrapper_release()?;
|
failed_acknowledged_transition_refuses_exact_wrapper_release()?;
|
||||||
|
|
@ -64,6 +67,33 @@ fn run_all() -> std::io::Result<()> {
|
||||||
concurrent_acquisitions_serialize()
|
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<()> {
|
fn mapped_transient_uses_configured_subordinates() -> std::io::Result<()> {
|
||||||
let map = UserNamespaceMap::resolve_current()?;
|
let map = UserNamespaceMap::resolve_current()?;
|
||||||
child_broker()
|
child_broker()
|
||||||
|
|
@ -149,7 +179,8 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
|
||||||
.argument(
|
.argument(
|
||||||
"read value; printf 'stdin-fd\\n' >&0; printf 'out:%s\\n' \"$value\"; printf 'err\\n' >&2",
|
"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 expected_parent = current_process_identity()?;
|
||||||
let ownership = ProcessOwnershipContext {
|
let ownership = ProcessOwnershipContext {
|
||||||
domain_id: DomainId::new([41; 32]),
|
domain_id: DomainId::new([41; 32]),
|
||||||
|
|
@ -193,6 +224,133 @@ fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()
|
||||||
Ok(())
|
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<()> {
|
fn transient_captured_child_preserves_streams_and_exit() -> std::io::Result<()> {
|
||||||
let target = TargetCommand::new("/bin/sh")
|
let target = TargetCommand::new("/bin/sh")
|
||||||
.argument("-c")
|
.argument("-c")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue