Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ production configs and the dev schema when working on experimental features:
"enforcementMode": "firewall", // "capabilities", "firewall", or "both"
"proxy": { "localhost": 8080 } // Loopback proxy port (processcontainer; bubblewrap)
// (use { "builtinTestServer": true } for the bundled
// testing-only proxy; requires --allow-testing-features)
// testing-only proxy; requires --allow-testing-features.
// lxc can't reach a loopback proxy — use { "url": "..." })
},

"processContainer": { // Process-based container-specific
Expand Down
44 changes: 40 additions & 4 deletions src/backends/lxc/common/src/lxc_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,26 +68,45 @@ pub fn resolve_default_lxcpath() -> String {
resolve_lxcpath_with_env(|k| std::env::var(k).ok(), current_euid)
}

/// Test-only convenience wrapper over [`build_attach_args_with_env_control`]
/// that hardcodes `force_clear_env = false` (the legacy behavior). Kept
/// `#[cfg(test)]`-only because production code always calls the
/// `_with_env_control` variant directly, so compiling this wrapper outside
/// tests would trip the dead-code lint.
#[cfg(test)]
fn build_attach_args(env: &[String], working_directory: &str, command: &str) -> Vec<String> {
build_attach_args_with_env_control(env, working_directory, command, false)
}

/// Build the post-binary argv for `lxc-attach` (the args that follow the
/// `-n NAME -P lxcpath` flags already appended by `lxc_command`).
///
/// Extracted so the env / cwd / command layering is unit-testable without
/// actually spawning `lxc-attach`. See [`LxcContainer::attach_run`] for
/// the full contract.
///
/// `force_clear_env` forces `--clear-env` even when `env` is empty, so a
/// fully-scrubbed proxy env can't silently fall back to inheriting the
/// host's variables.
///
/// Gated to Linux + test builds because `attach_run` is a Windows stub
/// that never calls this helper, and the workspace clippy lane on
/// `windows-latest` would otherwise flag it as dead code.
#[cfg(any(target_os = "linux", test))]
fn build_attach_args(env: &[String], working_directory: &str, command: &str) -> Vec<String> {
fn build_attach_args_with_env_control(
env: &[String],
working_directory: &str,
command: &str,
force_clear_env: bool,
) -> Vec<String> {
// Loose upper bound; realloc-avoidance hint only.
let mut args: Vec<String> = Vec::with_capacity(env.len() + 8);

// Replace semantics: any non-empty env opts the caller into a clean
// slate, even if every entry is malformed. Matches Seatbelt exactly
// and is the posture lxc-attach(1) recommends for sandbox callers.
// See `attach_run` doc for the full contract.
if !env.is_empty() {
if force_clear_env || !env.is_empty() {
args.push("--clear-env".to_string());
for kv in env {
// Well-formed = "KEY=VAL" with a non-empty KEY. `"=foo"` and
Expand Down Expand Up @@ -294,7 +313,11 @@ impl LxcContainer {
/// and are outside this function's control.
///
/// When `env` is empty, the legacy keep-env behavior is preserved so
/// existing call sites without explicit env are undisturbed.
/// existing call sites without explicit env are undisturbed unless
/// `force_clear_env` is true. The LXC runner uses `force_clear_env`
/// after proxy-env scrubbing removes every caller-supplied proxy entry;
/// that still must clear inherited proxy variables instead of falling
/// back to keep-env mode.
///
/// We pass `unblock_signals = [SIGHUP, SIGTERM, SIGINT]` because
/// [`crate::signal_cleanup::install`] blocks them in this process so
Expand All @@ -314,14 +337,20 @@ impl LxcContainer {
command: &str,
working_directory: &str,
env: &[String],
force_clear_env: bool,
timeout: Option<std::time::Duration>,
) -> Result<(i32, String, String), String> {
use mxc_pty::{run_with_pty, PtyOptions, PtyOutcome, Signal};

const UNBLOCK: &[Signal] = &[Signal::SIGHUP, Signal::SIGTERM, Signal::SIGINT];

let mut cmd = self.lxc_command("lxc-attach");
cmd.args(build_attach_args(env, working_directory, command));
cmd.args(build_attach_args_with_env_control(
env,
working_directory,
command,
force_clear_env,
));

let options = PtyOptions {
unblock_signals: UNBLOCK,
Expand All @@ -348,6 +377,7 @@ impl LxcContainer {
_command: &str,
_working_directory: &str,
_env: &[String],
_force_clear_env: bool,
_timeout: Option<std::time::Duration>,
) -> Result<(i32, String, String), String> {
Err("LxcContainer::attach_run is only supported on Linux".to_string())
Expand Down Expand Up @@ -746,6 +776,12 @@ mod tests {
);
}

#[test]
fn build_attach_args_can_force_clear_env_when_env_empty() {
let args = build_attach_args_with_env_control(&[], "", "cmd", true);
assert_eq!(args, vec!["--clear-env", "--", "/bin/sh", "-c", "cmd"]);
}

#[test]
fn build_attach_args_clears_env_even_when_all_entries_malformed() {
// Caller opted into env control by populating the field. Even if
Expand Down
13 changes: 9 additions & 4 deletions src/backends/lxc/common/src/lxc_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,14 @@ impl LxcScriptRunner {
let _ = writeln!(logger, "Container already running.");
}

// Wait for network only when the config uses network features (firewall rules
// or allowed/blocked hosts).
// Wait for network only when the config uses network features
// (firewall rules, allowed/blocked hosts, or proxy enforcement).
let needs_network = matches!(
request.policy.network_enforcement_mode,
NetworkEnforcementMode::Firewall | NetworkEnforcementMode::Both
) || !request.policy.allowed_hosts.is_empty()
|| !request.policy.blocked_hosts.is_empty();
|| !request.policy.blocked_hosts.is_empty()
|| request.policy.network_proxy.is_enabled();

if needs_network {
Self::wait_for_network(&container_name, Duration::from_secs(10), logger);
Expand Down Expand Up @@ -242,10 +243,14 @@ impl LxcScriptRunner {
Some(Duration::from_millis(u64::from(request.script_timeout)))
};
let _ = writeln!(logger, "Executing script inside container...");
let mut exec_env = request.env.clone();
let force_clear_env =
wxc_common::proxy_env::apply_proxy_env(&mut exec_env, &request.policy.network_proxy);
let result = container.attach_run(
&request.script_code,
&request.working_directory,
&request.env,
&exec_env,
force_clear_env,
timeout,
);

Expand Down
Loading
Loading