Preserve inherited target stdin authority
This commit is contained in:
parent
39307f4281
commit
f5455d1d6c
2 changed files with 109 additions and 7 deletions
|
|
@ -34,6 +34,62 @@ pub struct TargetCommand {
|
||||||
stdin: Option<File>,
|
stdin: Option<File>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum TargetStdinAccess {
|
||||||
|
ReadOnly,
|
||||||
|
WriteOnly,
|
||||||
|
ReadWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TargetStdinAccess {
|
||||||
|
fn from_flags(flags: OFlags) -> io::Result<Self> {
|
||||||
|
if flags.contains(OFlags::PATH) {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"target stdin descriptor has no read or write authority",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
match flags & OFlags::ACCMODE {
|
||||||
|
flags if flags == OFlags::RDONLY => Ok(Self::ReadOnly),
|
||||||
|
flags if flags == OFlags::WRONLY => Ok(Self::WriteOnly),
|
||||||
|
flags if flags == OFlags::RDWR => Ok(Self::ReadWrite),
|
||||||
|
_ => Err(io::Error::other(
|
||||||
|
"target stdin descriptor has an unsupported access mode",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_descriptor(descriptor: i32) -> io::Result<Self> {
|
||||||
|
let metadata = std::fs::read_to_string(format!("/proc/self/fdinfo/{descriptor}"))?;
|
||||||
|
let flags = metadata
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("flags:"))
|
||||||
|
.map(str::trim)
|
||||||
|
.ok_or_else(|| io::Error::other("target stdin descriptor flags are missing"))
|
||||||
|
.and_then(|flags| {
|
||||||
|
u32::from_str_radix(flags, 8)
|
||||||
|
.map(OFlags::from_bits_retain)
|
||||||
|
.map_err(|_| io::Error::other("target stdin descriptor flags are invalid"))
|
||||||
|
})?;
|
||||||
|
Self::from_flags(flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reopen(self, descriptor: i32) -> io::Result<File> {
|
||||||
|
let mut options = File::options();
|
||||||
|
match self {
|
||||||
|
Self::ReadOnly => {
|
||||||
|
options.read(true);
|
||||||
|
}
|
||||||
|
Self::WriteOnly => {
|
||||||
|
options.write(true);
|
||||||
|
}
|
||||||
|
Self::ReadWrite => {
|
||||||
|
options.read(true).write(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.open(format!("/proc/self/fd/{descriptor}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ParentEdge {
|
pub enum ParentEdge {
|
||||||
ParentDeathKill,
|
ParentDeathKill,
|
||||||
|
|
@ -93,6 +149,7 @@ impl TargetCommand {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error when the descriptor cannot be inherited by the wrapper.
|
/// Returns an error when the descriptor cannot be inherited by the wrapper.
|
||||||
pub fn with_target_stdin(mut self, stdin: File) -> io::Result<Self> {
|
pub fn with_target_stdin(mut self, stdin: File) -> io::Result<Self> {
|
||||||
|
TargetStdinAccess::from_flags(fcntl_getfl(&stdin)?)?;
|
||||||
fcntl_setfd(stdin.as_fd(), FdFlags::empty())?;
|
fcntl_setfd(stdin.as_fd(), FdFlags::empty())?;
|
||||||
self.stdin = Some(stdin);
|
self.stdin = Some(stdin);
|
||||||
Ok(self)
|
Ok(self)
|
||||||
|
|
@ -1373,15 +1430,11 @@ pub fn parse_wrapper_arguments(
|
||||||
.and_then(|value| value.to_str().and_then(|value| value.parse::<i32>().ok()))
|
.and_then(|value| value.to_str().and_then(|value| value.parse::<i32>().ok()))
|
||||||
.filter(|descriptor| *descriptor > 2)
|
.filter(|descriptor| *descriptor > 2)
|
||||||
.ok_or_else(|| io::Error::other("wrapper target stdin descriptor is invalid"))?;
|
.ok_or_else(|| io::Error::other("wrapper target stdin descriptor is invalid"))?;
|
||||||
|
let access = TargetStdinAccess::from_descriptor(descriptor)?;
|
||||||
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(
|
Some(access.reopen(descriptor)?)
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs::File;
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::{Read, Seek, Write};
|
use std::io::{Read, Seek, Write};
|
||||||
use std::os::fd::AsFd;
|
use std::os::fd::AsFd;
|
||||||
use std::process::{ExitCode, Stdio};
|
use std::process::{ExitCode, Stdio};
|
||||||
|
|
@ -50,6 +50,7 @@ fn run_all() -> std::io::Result<()> {
|
||||||
successful_acquisition()?;
|
successful_acquisition()?;
|
||||||
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()?;
|
||||||
|
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()?;
|
||||||
lifetime_pipe_descriptor_contract()?;
|
lifetime_pipe_descriptor_contract()?;
|
||||||
|
|
@ -61,6 +62,54 @@ fn run_all() -> std::io::Result<()> {
|
||||||
concurrent_acquisitions_serialize()
|
concurrent_acquisitions_serialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn target_stdin_does_not_gain_write_authority() -> std::io::Result<()> {
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"lazaret-target-stdin-authority-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
SEQUENCE.fetch_add(1, Ordering::Relaxed)
|
||||||
|
));
|
||||||
|
let mut source = OpenOptions::new()
|
||||||
|
.create_new(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&path)?;
|
||||||
|
source.write_all(b"unchanged")?;
|
||||||
|
drop(source);
|
||||||
|
let stdin = OpenOptions::new().read(true).open(&path)?;
|
||||||
|
let target = TargetCommand::new("/bin/sh")
|
||||||
|
.argument("-c")
|
||||||
|
.argument("printf changed >&0")
|
||||||
|
.with_target_stdin(stdin)?;
|
||||||
|
let expected_parent = current_process_identity()?;
|
||||||
|
let ownership = ProcessOwnershipContext {
|
||||||
|
domain_id: DomainId::new([45; 32]),
|
||||||
|
generation_id: GenerationId::new(46),
|
||||||
|
coordinator_id: CoordinatorId::new([47; 32]),
|
||||||
|
slot: HostProcessOwnershipSlot::SessionTerminalExecClient(
|
||||||
|
lazaret_core::identity::SessionId::new([48; 32]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let process = child_broker().acquire_with_stdio(
|
||||||
|
expected_parent,
|
||||||
|
ownership,
|
||||||
|
&target,
|
||||||
|
ChildStandardIo::silenced(),
|
||||||
|
|_| Ok(()),
|
||||||
|
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
|
||||||
|
)?;
|
||||||
|
while !process.has_exited()? {
|
||||||
|
std::thread::yield_now();
|
||||||
|
}
|
||||||
|
let exit = child_broker().reap_status(process)?;
|
||||||
|
let contents = std::fs::read(&path);
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
if exit.exit_code == Some(0) || contents? != b"unchanged" {
|
||||||
|
return Err(std::io::Error::other(format!(
|
||||||
|
"read-only target stdin gained write authority: {exit:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()> {
|
fn terminal_child_restores_slave_stdin_and_merges_output() -> std::io::Result<()> {
|
||||||
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)?;
|
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)?;
|
||||||
grantpt(&master)?;
|
grantpt(&master)?;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue