Skip to content

Commit 6a34ed9

Browse files
domenkozarclaude
andcommitted
feat(build/oci): make sandbox shell configurable via query parameter
Previously, the sandbox shell path was configured at compile time using the SNIX_BUILD_SANDBOX_SHELL environment variable. This change moves the configuration to runtime via a query parameter in the OCI build service URL. The sandbox_shell can now be specified as: oci:///path/to/bundle?sandbox_shell=/path/to/shell If not specified, it defaults to /bin/sh. Also adds an embedded-busybox cargo feature that embeds the busybox binary at compile time for environments where a suitable shell isn't available. This makes the build service more flexible and removes the need for compile-time shell configuration in most cases. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Change-Id: Idf14ad2d1ddf111fb3173d6aa13b4049a2a89070
1 parent abb2daa commit 6a34ed9

File tree

7 files changed

+123
-14
lines changed

7 files changed

+123
-14
lines changed

snix/build/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ tonic-build.workspace = true
3737
[features]
3838
default = []
3939
tonic-reflection = ["dep:tonic-reflection", "snix-castore/tonic-reflection"]
40+
embedded-busybox = []
4041

4142
[dev-dependencies]
4243
rstest.workspace = true

snix/build/build.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
use std::io::Result;
22

33
fn main() -> Result<()> {
4+
// Handle embedded busybox feature
5+
#[cfg(feature = "embedded-busybox")]
6+
{
7+
if let Ok(busybox_path) = std::env::var("SNIX_BUILD_SANDBOX_SHELL") {
8+
// Tell cargo to rerun if the busybox binary changes
9+
println!("cargo:rerun-if-changed={}", busybox_path);
10+
} else {
11+
panic!(
12+
"SNIX_BUILD_SANDBOX_SHELL environment variable must be set when using embedded-busybox feature"
13+
);
14+
}
15+
}
416
#[allow(unused_mut)]
517
let mut builder = tonic_build::configure();
618

snix/build/src/buildservice/from_addr.rs

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,105 @@
11
use super::{BuildService, DummyBuildService, grpc::GRPCBuildService};
2+
use serde::Deserialize;
23
use snix_castore::{blobservice::BlobService, directoryservice::DirectoryService};
4+
use std::path::PathBuf;
5+
use std::sync::OnceLock;
36
use url::Url;
47

58
#[cfg(target_os = "linux")]
69
use super::oci::OCIBuildService;
710

11+
/// Extract the embedded busybox binary to a temporary location and return its path
12+
#[cfg(all(target_os = "linux", feature = "embedded-busybox"))]
13+
fn get_embedded_busybox_path() -> Result<PathBuf, std::io::Error> {
14+
use std::fs;
15+
use std::os::unix::fs::PermissionsExt;
16+
17+
static EXTRACTED_BUSYBOX_PATH: OnceLock<PathBuf> = OnceLock::new();
18+
19+
// The embedded busybox binary (included at compile time)
20+
static EMBEDDED_BUSYBOX_BINARY: &[u8] = include_bytes!(env!("SNIX_BUILD_SANDBOX_SHELL"));
21+
22+
EXTRACTED_BUSYBOX_PATH
23+
.get_or_try_init(|| {
24+
let temp_dir = std::env::temp_dir();
25+
let busybox_path = temp_dir.join(format!("snix-busybox-{}", std::process::id()));
26+
27+
// Write the binary
28+
fs::write(&busybox_path, EMBEDDED_BUSYBOX_BINARY)?;
29+
30+
// Make it executable
31+
let mut perms = fs::metadata(&busybox_path)?.permissions();
32+
perms.set_mode(0o755);
33+
fs::set_permissions(&busybox_path, perms)?;
34+
35+
tracing::debug!(?busybox_path, "extracted embedded busybox binary");
36+
37+
Ok(busybox_path)
38+
})
39+
.cloned()
40+
}
41+
42+
/// Configuration for OCIBuildService
43+
#[cfg(target_os = "linux")]
44+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
45+
#[serde(deny_unknown_fields)]
46+
pub struct OCIBuildServiceConfig {
47+
/// Root path in which all bundles are created
48+
pub bundle_root: PathBuf,
49+
50+
/// Path to the sandbox shell to use.
51+
/// This needs to be a statically linked binary, or you must ensure all
52+
/// dependencies are part of the build (which they usually are not).
53+
#[serde(default = "default_sandbox_shell")]
54+
pub sandbox_shell: PathBuf,
55+
// TODO: make rootless_uid_gid configurable
56+
}
57+
58+
#[cfg(target_os = "linux")]
59+
fn default_sandbox_shell() -> PathBuf {
60+
// Priority: embedded busybox -> SNIX_BUILD_SANDBOX_SHELL env var -> /bin/sh
61+
#[cfg(feature = "embedded-busybox")]
62+
{
63+
if let Ok(path) = get_embedded_busybox_path() {
64+
return path;
65+
}
66+
tracing::warn!(
67+
"failed to extract embedded busybox, falling back to environment variable or /bin/sh"
68+
);
69+
}
70+
71+
// Check environment variable (for backward compatibility when feature is disabled)
72+
if let Ok(shell_path) = std::env::var("SNIX_BUILD_SANDBOX_SHELL") {
73+
return PathBuf::from(shell_path);
74+
}
75+
76+
PathBuf::from("/bin/sh")
77+
}
78+
79+
#[cfg(target_os = "linux")]
80+
impl TryFrom<Url> for OCIBuildServiceConfig {
81+
type Error = Box<dyn std::error::Error + Send + Sync>;
82+
83+
fn try_from(url: Url) -> Result<Self, Self::Error> {
84+
// oci wants a path in which it creates bundles
85+
if url.path().is_empty() {
86+
return Err("oci needs a bundle dir as path".into());
87+
}
88+
89+
// Parse sandbox_shell from query parameters
90+
let query_pairs: std::collections::HashMap<_, _> = url.query_pairs().collect();
91+
let sandbox_shell = query_pairs
92+
.get("sandbox_shell")
93+
.map(|s| PathBuf::from(s.as_ref()))
94+
.unwrap_or_else(default_sandbox_shell);
95+
96+
Ok(OCIBuildServiceConfig {
97+
bundle_root: url.path().into(),
98+
sandbox_shell,
99+
})
100+
}
101+
}
102+
8103
/// Constructs a new instance of a [BuildService] from an URI.
9104
///
10105
/// The following schemes are supported by the following services:
@@ -32,17 +127,14 @@ where
32127
"dummy" => Box::<DummyBuildService>::default(),
33128
#[cfg(target_os = "linux")]
34129
"oci" => {
35-
// oci wants a path in which it creates bundles.
36-
if url.path().is_empty() {
37-
Err(std::io::Error::other("oci needs a bundle dir as path"))?
38-
}
39-
40-
// TODO: make sandbox shell and rootless_uid_gid
130+
let config = OCIBuildServiceConfig::try_from(url)
131+
.map_err(|e| std::io::Error::other(format!("invalid oci config: {}", e)))?;
41132

42133
Box::new(OCIBuildService::new(
43-
url.path().into(),
134+
config.bundle_root,
44135
blob_service,
45136
directory_service,
137+
config.sandbox_shell,
46138
))
47139
}
48140
scheme => {

snix/build/src/buildservice/oci.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ use std::{ffi::OsStr, path::PathBuf, process::Stdio};
1818

1919
use super::BuildService;
2020

21-
const SANDBOX_SHELL: &str = env!("SNIX_BUILD_SANDBOX_SHELL");
2221
const MAX_CONCURRENT_BUILDS: usize = 2; // TODO: make configurable
2322

2423
pub struct OCIBuildService<BS, DS> {
@@ -30,13 +29,21 @@ pub struct OCIBuildService<BS, DS> {
3029
/// Handle to a [DirectoryService], used by filesystems spawned during builds.
3130
directory_service: DS,
3231

32+
/// Path to the sandbox shell to use
33+
sandbox_shell: PathBuf,
34+
3335
// semaphore to track number of concurrently running builds.
3436
// this is necessary, as otherwise we very quickly run out of open file handles.
3537
concurrent_builds: tokio::sync::Semaphore,
3638
}
3739

3840
impl<BS, DS> OCIBuildService<BS, DS> {
39-
pub fn new(bundle_root: PathBuf, blob_service: BS, directory_service: DS) -> Self {
41+
pub fn new(
42+
bundle_root: PathBuf,
43+
blob_service: BS,
44+
directory_service: DS,
45+
sandbox_shell: PathBuf,
46+
) -> Self {
4047
// We map root inside the container to the uid/gid this is running at,
4148
// and allocate one for uid 1000 into the container from the range we
4249
// got in /etc/sub{u,g}id.
@@ -45,6 +52,7 @@ impl<BS, DS> OCIBuildService<BS, DS> {
4552
bundle_root,
4653
blob_service,
4754
directory_service,
55+
sandbox_shell,
4856
concurrent_builds: tokio::sync::Semaphore::new(MAX_CONCURRENT_BUILDS),
4957
}
5058
}
@@ -66,7 +74,7 @@ where
6674
let span = Span::current();
6775
span.record("bundle_name", bundle_name.to_string());
6876

69-
let mut runtime_spec = make_spec(&request, true, SANDBOX_SHELL)
77+
let mut runtime_spec = make_spec(&request, true, self.sandbox_shell.to_str().unwrap())
7078
.context("failed to create spec")
7179
.map_err(std::io::Error::other)?;
7280

snix/default.nix

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ in
6767
inherit cargoDeps src;
6868
name = "snix-rust-docs";
6969
PROTO_ROOT = protos;
70-
SNIX_BUILD_SANDBOX_SHELL = "/homeless-shelter";
7170

7271
nativeBuildInputs = with pkgs; [
7372
cargo
@@ -93,7 +92,6 @@ in
9392
inherit cargoDeps src;
9493
name = "snix-clippy";
9594
PROTO_ROOT = protos;
96-
SNIX_BUILD_SANDBOX_SHELL = "/homeless-shelter";
9795

9896
buildInputs = [
9997
pkgs.fuse

snix/shell.nix

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ pkgs.mkShell {
5151
# should also benchmark with a more static nixpkgs checkout, so nixpkgs
5252
# refactorings are not observed as eval perf changes.
5353
shellHook = ''
54-
export SNIX_BUILD_SANDBOX_SHELL=${if pkgs.stdenv.isLinux then pkgs.busybox-sandbox-shell + "/bin/busybox" else "/bin/sh"}
5554
export SNIX_BENCH_NIX_PATH=nixpkgs=${pkgs.path}
5655
'';
5756
}

snix/utils.nix

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ in
8080
};
8181
PROTO_ROOT = depot.snix.build.protos.protos;
8282
nativeBuildInputs = [ pkgs.protobuf ];
83-
SNIX_BUILD_SANDBOX_SHELL = if pkgs.stdenv.isLinux then pkgs.pkgsStatic.busybox + "/bin/sh" else "/bin/sh";
8483
};
8584

8685
snix-castore = prev: {

0 commit comments

Comments
 (0)