Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/lxc-support/lxc-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ The `process.cwd` and `process.env` fields from the standard schema are honored
| `process.cwd` | `cd -- "$1" && exec /bin/sh -c "$2"` wrapper prelude, with the cwd passed as a positional argument | Empty string preserves the container default cwd. A nonexistent or non-permitted path surfaces as a generic non-zero exit (typically `1`, from `cd`'s own status); callers needing strong cwd validation should pre-check the path. The positional-arg trick means cwd values with spaces, quotes, `$vars`, or backticks pass through verbatim with no shell escaping. |
| `process.env` | Each `KEY=VAL` entry becomes a repeated `--set-var=KEY=VAL` flag to `lxc-attach` | Malformed entries β€” those without `=` (e.g. `"BADENTRY"`) or with an empty key (e.g. `"=foo"`) β€” are silently skipped. Embedded `=` in the value (e.g. `"X=a=b=c"`) is preserved. |

**Replace semantics.** When `process.env` is non-empty, `lxc-exec` also passes `--clear-env` to `lxc-attach` so the host environment does **not** leak into the sandbox, regardless of how many entries survive the malformed-skip. This matches the Seatbelt backend's `env_clear()`-on-non-empty contract and is the posture `lxc-attach(1)` recommends for sandbox-spawn callers. If a variable is set in both the host and `process.env`, the `process.env` value wins.
**Replace semantics.** When `process.env` is non-empty, `lxc-exec` also passes `--clear-env` to `lxc-attach` so the host environment does **not** leak into the sandbox, regardless of how many entries survive the malformed-skip. This is the posture `lxc-attach(1)` recommends for sandbox-spawn callers. If a variable is set in both the host and `process.env`, the `process.env` value wins.

When `process.env` is empty (or absent), the legacy keep-env behavior is preserved and the host environment is inherited.

Expand Down
15 changes: 15 additions & 0 deletions docs/macos-support/seatbelt-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,21 @@ SDK rejects it with a clear error, mirroring the Linux behavior.
| `ui.clipboard: "none"` (default) | `(deny mach-lookup (global-name "com.apple.pasteboard.1"))` |
| `ui.injection: false` (default) | `(deny iokit-open (iokit-user-client-class "IOHIDLibUserClient"))` |

### Process environment

The host environment is **never** inherited β€” the sandboxed child always starts
from a cleared environment, so host secrets (cloud credentials, API tokens) can
never leak into untrusted code. `PATH` defaults to `/usr/bin:/bin:/usr/sbin:/sbin`,
and each `process.env` entry adds to / overrides that baseline. (This is
unconditional; it applies whether or not `process.env` is provided.)

### Working directory

If `process.cwd` is omitted it resolves to `readwritePaths[0]`, else
`readonlyPaths[0]`, else `/`; a `~`/`~/…` default is tilde-expanded the same way
the sandbox profile expands policy paths. `PWD` is exported to the resolved
directory so the child's `getcwd()` takes its fast `$PWD` path.

## Usage

### Command line
Expand Down
1 change: 0 additions & 1 deletion src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

713 changes: 500 additions & 213 deletions src/backends/appcontainer/common/src/appcontainer_runner.rs

Large diffs are not rendered by default.

695 changes: 523 additions & 172 deletions src/backends/appcontainer/common/src/base_container_runner.rs

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions src/backends/appcontainer/common/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ use crate::fallback_detector::{self, FallbackError, IsolationTier};
use wxc_common::error::WxcError;
use wxc_common::filesystem_dacl::{DaclError, DaclManager, RO_MASK, RW_MASK};
use wxc_common::models::ExecutionRequest;
use wxc_common::sandbox_process::Runner;
use wxc_common::script_runner::ScriptRunner;

/// Result of a successful dispatch decision: a phased handle holding a
Expand Down Expand Up @@ -287,7 +288,7 @@ pub fn dispatch_with_fallback(request: &ExecutionRequest) -> Result<Dispatched,
// opaque principal `Experimental_CreateProcessInSandbox`
// actually runs the child under; a mismatch would render
// the ACEs inert and silently un-enforce `deniedPaths`.
let runner: Box<dyn ScriptRunner> = Box::new(BaseContainerRunner::new());
let runner: Box<dyn ScriptRunner> = Box::new(Runner::new(BaseContainerRunner::new()));
(runner, None)
}
IsolationTier::AppContainerBfs => {
Expand All @@ -297,9 +298,9 @@ pub fn dispatch_with_fallback(request: &ExecutionRequest) -> Result<Dispatched,
// common no-deny case skips both costs.
let denied = paths_to_pathbufs(&request.policy.denied_paths);
if denied.is_empty() {
let runner: Box<dyn ScriptRunner> = Box::new(
let runner: Box<dyn ScriptRunner> = Box::new(Runner::new(
AppContainerScriptRunner::with_filesystem_mode(FilesystemMode::Bfs),
);
));
(runner, None)
} else {
let sid =
Expand All @@ -308,12 +309,12 @@ pub fn dispatch_with_fallback(request: &ExecutionRequest) -> Result<Dispatched,
// Hand the derived SID string to the runner so it does
// not re-run `ConvertSidToStringSidW` for the firewall
// principal-id lookup.
let runner: Box<dyn ScriptRunner> = Box::new(
let runner: Box<dyn ScriptRunner> = Box::new(Runner::new(
AppContainerScriptRunner::with_filesystem_mode_and_sid_string(
FilesystemMode::Bfs,
sid,
),
);
));
(runner, mgr)
}
}
Expand Down Expand Up @@ -342,12 +343,12 @@ pub fn dispatch_with_fallback(request: &ExecutionRequest) -> Result<Dispatched,
let denied = paths_to_pathbufs(&request.policy.denied_paths);
let sid = derive_sid_string(&container_name(request)).map_err(DispatchError::Sid)?;
let mgr = build_t3_dacl(&sid, &readwrite, &readonly, &denied)?;
let runner: Box<dyn ScriptRunner> = Box::new(
let runner: Box<dyn ScriptRunner> = Box::new(Runner::new(
AppContainerScriptRunner::with_filesystem_mode_and_sid_string(
FilesystemMode::Dacl,
sid,
),
);
));
(runner, Some(mgr))
}
};
Expand Down
20 changes: 16 additions & 4 deletions src/backends/appcontainer/common/src/job_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ use std::sync::OnceLock;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectBasicUIRestrictions,
SetInformationJobObject, JOBOBJECT_BASIC_UI_RESTRICTIONS, JOB_OBJECT_UILIMIT,
JOB_OBJECT_UILIMIT_DESKTOP, JOB_OBJECT_UILIMIT_DISPLAYSETTINGS, JOB_OBJECT_UILIMIT_EXITWINDOWS,
JOB_OBJECT_UILIMIT_GLOBALATOMS, JOB_OBJECT_UILIMIT_HANDLES, JOB_OBJECT_UILIMIT_READCLIPBOARD,
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS, JOB_OBJECT_UILIMIT_WRITECLIPBOARD,
SetInformationJobObject, TerminateJobObject, JOBOBJECT_BASIC_UI_RESTRICTIONS,
JOB_OBJECT_UILIMIT, JOB_OBJECT_UILIMIT_DESKTOP, JOB_OBJECT_UILIMIT_DISPLAYSETTINGS,
JOB_OBJECT_UILIMIT_EXITWINDOWS, JOB_OBJECT_UILIMIT_GLOBALATOMS, JOB_OBJECT_UILIMIT_HANDLES,
JOB_OBJECT_UILIMIT_READCLIPBOARD, JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS,
JOB_OBJECT_UILIMIT_WRITECLIPBOARD,
};
use windows::Win32::System::SystemServices::JOB_OBJECT_UILIMIT_IME;
use windows_core::PCWSTR;
Expand Down Expand Up @@ -273,6 +274,17 @@ impl UiJobObject {
unsafe { AssignProcessToJobObject(self.handle, process_handle) }
.map_err(|e| WxcError::Process(format!("AssignProcessToJobObject: {e}")))
}

/// Terminate every process currently assigned to this job (the sandboxed
/// child and all of its descendants) with the given exit code. Used to
/// tree-kill a running sandbox. Best-effort: errors are ignored since the
/// processes may already have exited.
pub fn terminate(&self, exit_code: u32) {
// SAFETY: `self.handle` is a valid job handle owned by this struct.
unsafe {
let _ = TerminateJobObject(self.handle, exit_code);
}
}
}

impl Drop for UiJobObject {
Expand Down
Loading
Loading