Skip to content

Commit fafa9a4

Browse files
committed
feat(sandbox): pluggable handler for bootstrap-subsystem failures
The supervisor performs three optional hardening steps that the host kernel may refuse: unshare(CLONE_NEWNET), the supervisor seccomp prelude, and the workload seccomp filter. On bare-metal Linux a refusal is fatal. Under an outer sandbox (gVisor, Firecracker, Kata) the host runtime is itself the enforcing boundary and routinely intercepts those syscalls, leaving the supervisor unable to boot. Introduce a pluggable SandboxFailureHandler trait. The default StrictHandler preserves the historical contract — every refusal aborts. Outer-sandbox integrations link this crate and call set_failure_handler once at process start to register a handler that returns Ok(()) for the kinds the host runtime is expected to manage. The three internal call sites in run_sandbox and sandbox::linux::enforce route through failure_handler().handle(kind, err)?. Also make drop_privileges idempotent: when euid/egid already match the resolved target, skip initgroups(3), which would otherwise fail without CAP_SETGID. The stock openshell-sandbox binary never calls set_failure_handler, so its behaviour against this commit is byte-identical to upstream main. All 777 sandbox library tests pass unchanged. Signed-off-by: Davanum Srinivas <dsrinivas@nvidia.com>
1 parent f0f17bf commit fafa9a4

3 files changed

Lines changed: 71 additions & 7 deletions

File tree

crates/openshell-sandbox/src/lib.rs

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,55 @@ pub(crate) fn agent_proposals_enabled() -> bool {
111111
.is_some_and(|flag| flag.load(Ordering::Relaxed))
112112
}
113113

114+
// Pluggable policy for bootstrap subsystems the host kernel may refuse
115+
// (netns create, supervisor seccomp, workload seccomp). Default
116+
// `StrictHandler` aborts; outer-sandbox integrations register their own
117+
// via `set_failure_handler`.
118+
119+
/// Which bootstrap subsystem failed.
120+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121+
pub enum SandboxFailureKind {
122+
/// `unshare(CLONE_NEWNET)` or a follow-up netns op refused by the kernel.
123+
NetworkNamespaceCreate,
124+
/// Supervisor seccomp prelude install failed.
125+
SupervisorSeccompInstall,
126+
/// Workload per-policy seccomp filter failed in `sandbox::linux::enforce`.
127+
WorkloadSeccompInstall,
128+
}
129+
130+
/// Policy for handling bootstrap refusals. `Ok(())` continues in degraded
131+
/// mode; `Err` aborts. Invoked synchronously — do not block.
132+
pub trait SandboxFailureHandler: Send + Sync + 'static {
133+
fn handle(&self, kind: SandboxFailureKind, err: miette::Report) -> Result<()>;
134+
}
135+
136+
/// Default handler — every refusal aborts.
137+
pub struct StrictHandler;
138+
139+
impl SandboxFailureHandler for StrictHandler {
140+
fn handle(&self, _kind: SandboxFailureKind, err: miette::Report) -> Result<()> {
141+
Err(err)
142+
}
143+
}
144+
145+
/// Set-once handler slot; lazy default is [`StrictHandler`].
146+
static FAILURE_HANDLER: OnceLock<Box<dyn SandboxFailureHandler>> = OnceLock::new();
147+
148+
/// Register the process-wide handler. Call once at process start, before
149+
/// [`run_sandbox`]. Returns the handler back on `Err` if the slot is
150+
/// already set.
151+
pub fn set_failure_handler(
152+
handler: Box<dyn SandboxFailureHandler>,
153+
) -> Result<(), Box<dyn SandboxFailureHandler>> {
154+
FAILURE_HANDLER.set(handler)
155+
}
156+
157+
pub(crate) fn failure_handler() -> &'static dyn SandboxFailureHandler {
158+
FAILURE_HANDLER
159+
.get_or_init(|| Box::new(StrictHandler))
160+
.as_ref()
161+
}
162+
114163
/// Test-only helpers shared across sibling test modules.
115164
#[cfg(test)]
116165
pub(crate) mod test_helpers {
@@ -547,11 +596,15 @@ pub async fn run_sandbox(
547596
Some(ns)
548597
}
549598
Err(e) => {
550-
return Err(miette::miette!(
551-
"Network namespace creation failed and proxy mode requires isolation. \
552-
Ensure CAP_NET_ADMIN and CAP_SYS_ADMIN are available and iproute2 is installed. \
553-
Error: {e}"
554-
));
599+
failure_handler().handle(
600+
SandboxFailureKind::NetworkNamespaceCreate,
601+
miette::miette!(
602+
"Network namespace creation failed and proxy mode requires isolation. \
603+
Ensure CAP_NET_ADMIN and CAP_SYS_ADMIN are available and iproute2 is installed. \
604+
Error: {e}"
605+
),
606+
)?;
607+
None
555608
}
556609
}
557610
} else {
@@ -566,7 +619,9 @@ pub async fn run_sandbox(
566619
// Install the supervisor seccomp prelude after privileged startup helpers
567620
// (network namespace setup, nftables probes) complete, but before the SSH
568621
// listener and workload process are exposed.
569-
apply_supervisor_startup_hardening()?;
622+
if let Err(e) = apply_supervisor_startup_hardening() {
623+
failure_handler().handle(SandboxFailureKind::SupervisorSeccompInstall, e)?;
624+
}
570625

571626
// Shared PID: set after process spawn so the proxy can look up
572627
// the entrypoint process's /proc/net/tcp for identity binding.

crates/openshell-sandbox/src/process.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,13 @@ pub fn drop_privileges(policy: &SandboxPolicy) -> Result<()> {
477477
.ok_or_else(|| miette::miette!("Failed to resolve user primary group"))?
478478
};
479479

480+
// Idempotent fast-path: if euid/egid already match the target (e.g. a
481+
// container entrypoint pre-dropped before exec'ing the sandbox), skip
482+
// initgroups(3), which would otherwise fail without CAP_SETGID.
483+
if nix::unistd::geteuid() == user.uid && nix::unistd::getegid() == group.gid {
484+
return Ok(());
485+
}
486+
480487
if user_name.is_some() {
481488
let user_cstr =
482489
CString::new(user.name.clone()).map_err(|_| miette::miette!("Invalid user name"))?;

crates/openshell-sandbox/src/sandbox/linux/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ pub fn enforce(prepared: PreparedSandbox) -> Result<()> {
4040
if let Some(ruleset) = prepared.landlock {
4141
landlock::enforce(ruleset)?;
4242
}
43-
seccomp::apply(&prepared.policy)?;
43+
if let Err(e) = seccomp::apply(&prepared.policy) {
44+
crate::failure_handler().handle(crate::SandboxFailureKind::WorkloadSeccompInstall, e)?;
45+
}
4446
Ok(())
4547
}
4648

0 commit comments

Comments
 (0)