Enforce sandbox causal edge stdio policies
This commit is contained in:
parent
b653293840
commit
903ff837d6
2 changed files with 185 additions and 33 deletions
|
|
@ -29,12 +29,12 @@ static CHILD_BROKER: OnceLock<ChildBroker> = OnceLock::new();
|
|||
pub struct TargetCommand {
|
||||
program: OsString,
|
||||
arguments: Vec<OsString>,
|
||||
parent_action: ParentAction,
|
||||
parent_edge: ParentEdge,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ParentAction {
|
||||
Kill,
|
||||
pub enum ParentEdge {
|
||||
ParentDeathKill,
|
||||
LifetimePipe,
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ impl TargetCommand {
|
|||
Self {
|
||||
program: program.into(),
|
||||
arguments: Vec::new(),
|
||||
parent_action: ParentAction::Kill,
|
||||
parent_edge: ParentEdge::ParentDeathKill,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,23 +56,28 @@ impl TargetCommand {
|
|||
|
||||
#[must_use]
|
||||
pub fn with_lifetime_pipe_parent_edge(mut self) -> Self {
|
||||
self.parent_action = ParentAction::LifetimePipe;
|
||||
self.parent_edge = ParentEdge::LifetimePipe;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_parent_death_kill_edge(mut self) -> Self {
|
||||
self.parent_action = ParentAction::Kill;
|
||||
self.parent_edge = ParentEdge::ParentDeathKill;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn parent_edge(&self) -> ParentEdge {
|
||||
self.parent_edge
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_types)]
|
||||
fn append_to(&self, command: &mut std::process::Command) {
|
||||
command
|
||||
.arg("--parent-action")
|
||||
.arg(match self.parent_action {
|
||||
ParentAction::Kill => "kill",
|
||||
ParentAction::LifetimePipe => "lifetime-pipe",
|
||||
.arg(match self.parent_edge {
|
||||
ParentEdge::ParentDeathKill => "kill",
|
||||
ParentEdge::LifetimePipe => "lifetime-pipe",
|
||||
})
|
||||
.arg("--")
|
||||
.arg(&self.program)
|
||||
|
|
@ -83,6 +88,14 @@ impl TargetCommand {
|
|||
pub struct ChildStandardIo {
|
||||
stdout: Option<Stdio>,
|
||||
stderr: Option<Stdio>,
|
||||
policy: ChildStandardIoPolicy,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ChildStandardIoPolicy {
|
||||
Inherit,
|
||||
PrivateControlWithLifetimePipe,
|
||||
Silenced,
|
||||
}
|
||||
|
||||
impl ChildStandardIo {
|
||||
|
|
@ -91,16 +104,35 @@ impl ChildStandardIo {
|
|||
Self {
|
||||
stdout: None,
|
||||
stderr: None,
|
||||
policy: ChildStandardIoPolicy::Inherit,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn redirected(stdout: impl Into<Stdio>, stderr: impl Into<Stdio>) -> Self {
|
||||
pub fn private_control_with_lifetime_pipe(
|
||||
control: impl Into<Stdio>,
|
||||
lifetime_pipe: impl Into<Stdio>,
|
||||
) -> Self {
|
||||
Self {
|
||||
stdout: Some(stdout.into()),
|
||||
stderr: Some(stderr.into()),
|
||||
stdout: Some(control.into()),
|
||||
stderr: Some(lifetime_pipe.into()),
|
||||
policy: ChildStandardIoPolicy::PrivateControlWithLifetimePipe,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn silenced() -> Self {
|
||||
Self {
|
||||
stdout: Some(Stdio::null()),
|
||||
stderr: Some(Stdio::null()),
|
||||
policy: ChildStandardIoPolicy::Silenced,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn policy(&self) -> ChildStandardIoPolicy {
|
||||
self.policy
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
@ -159,6 +191,27 @@ pub fn child_broker() -> &'static ChildBroker {
|
|||
})
|
||||
}
|
||||
|
||||
fn require_parent_edge_standard_io(
|
||||
parent_edge: ParentEdge,
|
||||
standard_io: ChildStandardIoPolicy,
|
||||
) -> io::Result<()> {
|
||||
match (parent_edge, standard_io) {
|
||||
(ParentEdge::LifetimePipe, ChildStandardIoPolicy::PrivateControlWithLifetimePipe)
|
||||
| (
|
||||
ParentEdge::ParentDeathKill,
|
||||
ChildStandardIoPolicy::Inherit | ChildStandardIoPolicy::Silenced,
|
||||
) => Ok(()),
|
||||
(ParentEdge::LifetimePipe, _) => Err(io::Error::other(
|
||||
"lifetime-pipe parent edge requires private control stdout and lifetime-pipe stderr",
|
||||
)),
|
||||
(ParentEdge::ParentDeathKill, ChildStandardIoPolicy::PrivateControlWithLifetimePipe) => {
|
||||
Err(io::Error::other(
|
||||
"parent-death edge cannot substitute a lifetime-pipe stdio policy",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChildBroker {
|
||||
/// Spawns the repository wrapper, stages exact ownership, accepts its acknowledgment,
|
||||
/// and only then releases target execution.
|
||||
|
|
@ -208,6 +261,7 @@ impl ChildBroker {
|
|||
.exclusion
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
||||
require_parent_edge_standard_io(target.parent_edge(), standard_io.policy())?;
|
||||
require_default_sigchld()?;
|
||||
require_reapable_sigchld()?;
|
||||
let (release, wait) = launch_barrier()?;
|
||||
|
|
@ -306,11 +360,11 @@ fn run_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
|
|||
let (expected_parent, target) = parse_wrapper_arguments(arguments)?;
|
||||
BarrierWait::from_stdin()?.wait()?;
|
||||
rustix::io::fcntl_setfd(std::io::stdin(), rustix::io::FdFlags::CLOEXEC)?;
|
||||
match target.parent_action {
|
||||
ParentAction::Kill => {
|
||||
match target.parent_edge {
|
||||
ParentEdge::ParentDeathKill => {
|
||||
arm_parent_death_and_recheck(Signal::KILL, expected_parent)?;
|
||||
}
|
||||
ParentAction::LifetimePipe => {
|
||||
ParentEdge::LifetimePipe => {
|
||||
require_lifetime_pipe_and_recheck(expected_parent)?;
|
||||
}
|
||||
}
|
||||
|
|
@ -779,9 +833,9 @@ pub fn parse_wrapper_arguments(
|
|||
let inode = number_argument(&mut arguments, "--parent-inode")?;
|
||||
let pid = number_argument(&mut arguments, "--parent-pid")?;
|
||||
let start = number_argument(&mut arguments, "--parent-start")?;
|
||||
let parent_action = match named_argument(&mut arguments, "--parent-action")?.to_str() {
|
||||
Some("kill") => ParentAction::Kill,
|
||||
Some("lifetime-pipe") => ParentAction::LifetimePipe,
|
||||
let parent_edge = match named_argument(&mut arguments, "--parent-action")?.to_str() {
|
||||
Some("kill") => ParentEdge::ParentDeathKill,
|
||||
Some("lifetime-pipe") => ParentEdge::LifetimePipe,
|
||||
_ => return Err(io::Error::other("wrapper parent action is invalid")),
|
||||
};
|
||||
if arguments.next().as_deref() != Some(OsStr::new("--")) {
|
||||
|
|
@ -793,7 +847,7 @@ pub fn parse_wrapper_arguments(
|
|||
let target = TargetCommand {
|
||||
program,
|
||||
arguments: arguments.collect(),
|
||||
parent_action,
|
||||
parent_edge,
|
||||
};
|
||||
let boot = parse_boot_hex(&boot)?;
|
||||
Ok((
|
||||
|
|
@ -880,6 +934,38 @@ mod tests {
|
|||
assert!(std::ptr::eq(child_broker(), child_broker()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_edges_require_their_exact_stdio_policy() {
|
||||
assert!(
|
||||
require_parent_edge_standard_io(
|
||||
ParentEdge::LifetimePipe,
|
||||
ChildStandardIoPolicy::PrivateControlWithLifetimePipe,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
require_parent_edge_standard_io(
|
||||
ParentEdge::LifetimePipe,
|
||||
ChildStandardIoPolicy::Silenced,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
require_parent_edge_standard_io(
|
||||
ParentEdge::ParentDeathKill,
|
||||
ChildStandardIoPolicy::Silenced,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
require_parent_edge_standard_io(
|
||||
ParentEdge::ParentDeathKill,
|
||||
ChildStandardIoPolicy::PrivateControlWithLifetimePipe,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn barrier_reader_is_inheritable_and_writer_is_close_on_exec() {
|
||||
let (release, wait) = launch_barrier().unwrap();
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ fn lifetime_pipe_descriptor_contract() -> std::io::Result<()> {
|
|||
parent,
|
||||
context(),
|
||||
&target,
|
||||
ChildStandardIo::redirected(Stdio::null(), Stdio::from(lifetime_reader)),
|
||||
ChildStandardIo::private_control_with_lifetime_pipe(
|
||||
Stdio::null(),
|
||||
Stdio::from(lifetime_reader),
|
||||
),
|
||||
|_| Ok(()),
|
||||
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
|
||||
)?;
|
||||
|
|
@ -108,7 +111,7 @@ fn run_lifetime_pipe_target(marker: &std::path::Path, stderr: Stdio) -> std::io:
|
|||
parent,
|
||||
context(),
|
||||
&target,
|
||||
ChildStandardIo::redirected(Stdio::null(), stderr),
|
||||
ChildStandardIo::private_control_with_lifetime_pipe(Stdio::null(), stderr),
|
||||
|_| Ok(()),
|
||||
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
|
||||
)?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue