use super::profile::maybe_profile_finish;
use crate::cli::args::SandboxArgs;
use crate::env_filter::EnvFilterResult;
use crate::exit_codes;
use crate::metrics;
use crate::profile::{events::ProfileEvent, ProfileCollector};
use std::path::Path;
use std::process::Stdio;
use tokio::time::Duration;
pub(super) async fn run_child(
args: &SandboxArgs,
policy: &crate::policy::Policy,
env_result: &EnvFilterResult,
tmp_dir: &Path,
cwd: &Path,
profiler: Option<ProfileCollector>,
actual_enforcement: bool,
) -> anyhow::Result<i32> {
let cmd_name = &args.command[0];
let cmd_args = &args.command[1..];
if let Some(p) = &profiler {
let home = std::env::var("HOME").ok().map(std::path::PathBuf::from);
let resolved_cmd = resolve_command_path(cmd_name);
let g = crate::profile::generalize::generalize_path(
&resolved_cmd,
cwd,
home.as_deref(),
Some(tmp_dir),
);
p.record(ProfileEvent::ExecObserved { argv0: g.rendered });
}
#[cfg(not(target_os = "linux"))]
let _ = actual_enforcement;
let mut cmd = tokio::process::Command::new(cmd_name);
cmd.args(cmd_args)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(cwd);
cmd.env_clear();
for (key, value) in &env_result.filtered_env {
cmd.env(key, value);
if let Some(p) = &profiler {
p.record(ProfileEvent::EnvProvidedKeys {
key: key.clone(),
scrubbed: false,
});
}
}
cmd.env("TMPDIR", tmp_dir);
cmd.env("TMP", tmp_dir);
cmd.env("TEMP", tmp_dir);
#[cfg(target_os = "linux")]
let net_allow_ports: Option<Vec<u16>> = if actual_enforcement && args.enforce_net {
let abi = crate::backend::detect_backend().1.abi_version;
match crate::landlock_net::plan_landlock_net_ports(&policy.net) {
Ok(ports) => Some(ports),
Err(rejects) => {
let reason = net_reject_to_reason_code(&rejects);
let detail = rejects
.iter()
.map(|r| format!("{}: {}", r.reason.as_str(), r.entry))
.collect::<Vec<_>>()
.join("; ");
let health = crate::enforcement_health_v1::EnforcementHealthV1::landlock_failed(
abi,
reason,
detail,
abi >= 4,
false,
);
write_enforcement_health_v1(args, &health)?;
if !args.quiet {
eprintln!(
"ERROR: network policy is not Landlock-net enforceable (fail-closed)"
);
}
return Ok(exit_codes::WOULD_BLOCK);
}
}
} else {
None
};
#[cfg(target_os = "linux")]
let enforcer_opt = if actual_enforcement {
Some(crate::backend::prepare_landlock(
policy,
tmp_dir,
net_allow_ports.as_deref(),
)?)
} else {
None
};
#[cfg(all(target_os = "linux", target_family = "unix"))]
{
if let Some(mut enforcer) = enforcer_opt {
unsafe {
cmd.pre_exec(move || {
enforcer.enforce()?;
Ok(())
});
}
}
}
let spawn_result = cmd.spawn();
#[cfg(target_os = "linux")]
if actual_enforcement && args.enforce_net {
let abi = crate::backend::detect_backend().1.abi_version;
match &spawn_result {
Ok(_) => {
let ports = net_allow_ports.clone().unwrap_or_default();
let probe = if args.probe_enforcement {
run_enforcement_self_probe(&ports, args.quiet)
} else {
None
};
let shedding = if args.aee_seal.is_some() {
match crate::backend::landlock_shedding_probe() {
Ok(outcome) => {
if !args.quiet
&& outcome != crate::backend::SheddingProbeOutcome::RestrictionsHeld
{
eprintln!(
"WARN: aee-seal: restriction-shedding probe returned {:?}; this run cannot claim still-armed",
outcome
);
}
Some(outcome.label().to_string())
}
Err(e) => {
if !args.quiet {
eprintln!(
"WARN: aee-seal: restriction-shedding probe could not run: {e}"
);
}
Some(
crate::backend::SheddingProbeOutcome::Inconclusive("probe_error")
.label()
.to_string(),
)
}
}
} else {
None
};
let health = crate::enforcement_health_v1::EnforcementHealthV1::landlock_active(
abi,
ports.clone(),
probe,
shedding,
);
write_enforcement_health_v1(args, &health)?;
maybe_emit_aee_seal(args, &health, &ports);
}
Err(_) => {
let health = crate::enforcement_health_v1::EnforcementHealthV1::landlock_failed(
abi,
crate::enforcement_health_v1::ReasonCode::RestrictSelfFailed,
"landlock restrict_self failed in the enforcing child",
abi >= 4,
true,
);
write_enforcement_health_v1(args, &health)?;
}
}
}
let mut child = spawn_result.map_err(|e| anyhow::anyhow!("failed to spawn child: {}", e))?;
let status_res = if let Some(sec) = args.timeout {
match tokio::time::timeout(Duration::from_secs(sec), child.wait()).await {
Ok(res) => res,
Err(_) => {
let _ = child.start_kill();
let _ = child.wait().await;
eprintln!("\nTIMEOUT: Process exceeded {}s limit", sec);
metrics::increment("sandbox_timeout");
return Ok(exit_codes::COMMAND_FAILED);
}
}
} else {
child.wait().await
};
let status = status_res?;
#[cfg(any(test, feature = "profile-test-hook"))]
if let Some(events) = crate::profile::events::try_load_test_events() {
if let Some(p) = &profiler {
p.note("injected_test_events: true");
for ev in events {
p.record(ev);
}
}
}
if let Some(p) = profiler {
let report = p.finish();
let suggestions = report.to_suggestion(crate::profile::suggest::SuggestConfig {
widen_dirs_to_glob: false,
});
if args.dry_run {
let mut violations = 0;
for path in &suggestions.fs.allow {
if !policy.fs.allow.iter().any(|p| p == path) {
violations += 1;
if !args.quiet {
eprintln!(
"DRY-RUN VIOLATION: Would have blocked FS access to: {}",
path
);
}
}
}
if violations > 0 {
if !args.quiet {
eprintln!("──────────────────");
eprintln!("DRY-RUN: Found {} potential violations.", violations);
}
maybe_profile_finish(report, args)?;
return Ok(exit_codes::WOULD_BLOCK);
}
}
maybe_profile_finish(report, args)?;
}
match status.code() {
Some(code) => Ok(code),
None => {
eprintln!("sandbox error: child terminated by signal");
Ok(exit_codes::INTERNAL_ERROR)
}
}
}
#[cfg(target_os = "linux")]
fn build_network_posture(allowed_ports: &[u16]) -> serde_json::Value {
let mut posture = serde_json::json!({
"mode": "deny-default",
"mechanism": "landlock",
"scope": crate::enforcement_health_v1::SCOPE_TCP_CONNECT_LANDLOCK_PORT,
"allowedConnectTcpPorts": allowed_ports,
});
let declared = crate::aee_seal::digest_json_public(&posture);
posture["digest"] = serde_json::json!({ "sha256": declared });
posture
}
#[cfg(target_os = "linux")]
fn maybe_emit_aee_seal(
args: &SandboxArgs,
health: &crate::enforcement_health_v1::EnforcementHealthV1,
allowed_ports: &[u16],
) {
let (Some(ctx_path), Some(key_path), Some(out_path)) = (
args.aee_run_context.as_ref(),
args.aee_seal_key.as_ref(),
args.aee_seal.as_ref(),
) else {
return;
};
let warn = |what: &str| {
if !args.quiet {
eprintln!("WARN: aee-seal: {what}; no seal was written");
}
};
let ctx_raw = match std::fs::read_to_string(ctx_path) {
Ok(r) => r,
Err(e) => return warn(&format!("run context unreadable: {e}")),
};
let context = match crate::aee_run_context::AeeRunContext::parse(&ctx_raw) {
Ok(c) => c,
Err(e) => return warn(&e.to_string()),
};
let key_raw = match std::fs::read_to_string(key_path) {
Ok(r) => r,
Err(e) => return warn(&format!("key descriptor unreadable: {e}")),
};
let key = match crate::aee_seal_key::load(key_path, &key_raw) {
Ok(k) => k,
Err(e) => return warn(&e.to_string()),
};
let env = context.into_environment(build_network_posture(allowed_ports));
let sealed_at = crate::aee_seal::now_rfc3339_utc();
let run = match crate::aee_seal::build_sealed_run(
crate::aee_seal::Vantage::Landlock(health),
&env,
&[],
&sealed_at,
&crate::aee_seal::DropAccounting::SynchronousProbe,
crate::aee_seal::COLLECTION_PATH_LANDLOCK_TCP_CONNECT,
) {
Ok(r) => r,
Err(e) => return warn(&format!("not seal-eligible: {e}")),
};
let envelope = match crate::aee_seal_envelope::sign_seal(
&run.seal,
key.signing_key(),
key.keyid(),
key.role(),
) {
Ok(e) => e,
Err(e) => return warn(&format!("signing failed: {e}")),
};
if let Some(records_path) = args.aee_records.as_ref() {
let lines = match crate::aee_seal::records_ndjson(&run.records) {
Ok(l) => l,
Err(e) => return warn(&format!("observation record not serializable: {e}")),
};
if let Some(parent) = records_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = std::fs::write(records_path, lines) {
return warn(&format!("could not write {}: {e}", records_path.display()));
}
}
let doc = match serde_json::to_string_pretty(&envelope) {
Ok(d) => d,
Err(e) => return warn(&format!("envelope not serializable: {e}")),
};
if let Some(parent) = out_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
match std::fs::write(out_path, doc + "\n") {
Ok(()) => {
if !args.quiet {
eprintln!(
"aee-seal: wrote {} (keyid {})",
out_path.display(),
key.keyid()
);
}
}
Err(e) => warn(&format!("could not write {}: {e}", out_path.display())),
}
}
#[cfg(target_os = "linux")]
fn write_enforcement_health_v1(
args: &SandboxArgs,
health: &crate::enforcement_health_v1::EnforcementHealthV1,
) -> anyhow::Result<()> {
use anyhow::Context;
if let Some(path) = args.enforcement_health.as_ref() {
health.write_to(path).with_context(|| {
format!(
"failed to write enforcement_health.v1 to {}",
path.display()
)
})?;
}
Ok(())
}
#[cfg(target_os = "linux")]
fn net_reject_to_reason_code(
_rejects: &[crate::landlock_net::NetReject],
) -> crate::enforcement_health_v1::ReasonCode {
crate::enforcement_health_v1::ReasonCode::PolicyNotExpressible
}
#[cfg(target_os = "linux")]
fn run_enforcement_self_probe(
allowed_ports: &[u16],
quiet: bool,
) -> Option<crate::enforcement_health_v1::Probe> {
use crate::backend::SelfProbeOutcome;
let listener = match bind_denied_listener(allowed_ports) {
Some(l) => l,
None => {
if !quiet {
eprintln!("WARN: probe-enforcement: could not bind a denied probe port; no probe");
}
return None;
}
};
let deny_port = listener.local_addr().ok().map(|a| a.port()).unwrap_or(0);
let outcome = match crate::backend::self_probe_denied_connect(allowed_ports, deny_port) {
Ok(o) => o,
Err(e) => {
if !quiet {
eprintln!("WARN: probe-enforcement: self-probe could not run: {e}");
}
return None;
}
};
let _ = listener.set_nonblocking(true);
let listener_reached = listener.accept().is_ok();
if outcome == SelfProbeOutcome::BlockedEacces && !listener_reached {
return Some(crate::enforcement_health_v1::Probe {
kind: "real_block".to_string(),
transport: "ipv4".to_string(),
blocked_action: "tcp_connect".to_string(),
blocked_port: deny_port,
blocked_errno: "EACCES".to_string(),
listener_reached: false,
});
}
if !quiet {
eprintln!(
"WARN: probe-enforcement: self-probe did not prove a block ({}); the enforcement \
ruleset was still applied",
describe_probe_nonblock(outcome, listener_reached)
);
}
None
}
#[cfg(target_os = "linux")]
fn bind_denied_listener(allowed_ports: &[u16]) -> Option<std::net::TcpListener> {
for _ in 0..8 {
let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).ok()?;
let port = listener.local_addr().ok()?.port();
if !allowed_ports.contains(&port) {
return Some(listener);
}
}
None
}
#[cfg(target_os = "linux")]
fn describe_probe_nonblock(
outcome: crate::backend::SelfProbeOutcome,
listener_reached: bool,
) -> String {
use crate::backend::SelfProbeOutcome;
if listener_reached {
return "the denied connect reached the listener".to_string();
}
match outcome {
SelfProbeOutcome::Connected => "the denied connect succeeded".to_string(),
SelfProbeOutcome::OtherErrno(e) => {
format!("the denied connect failed with errno {e}, not EACCES")
}
SelfProbeOutcome::ProbeInfraError(r) => format!("the probe could not run: {r}"),
SelfProbeOutcome::BlockedEacces => "blocked, but the listener was reached".to_string(),
}
}
fn resolve_command_path(cmd_name: &str) -> std::path::PathBuf {
if std::path::Path::new(cmd_name).is_absolute() {
return std::path::PathBuf::from(cmd_name);
}
std::env::var_os("PATH")
.and_then(|paths| {
std::env::split_paths(&paths).find_map(|dir| {
let full_path = dir.join(cmd_name);
if full_path.exists() {
Some(full_path)
} else {
None
}
})
})
.unwrap_or_else(|| std::path::PathBuf::from(cmd_name))
}