Skip to content

Commit e78ad76

Browse files
domenkozarclaude
andcommitted
feat(build): add embedded busybox support for sandbox shell
This adds an optional "embedded-busybox" feature that embeds the busybox binary directly into the build at compile time. This simplifies deployment by removing the runtime dependency on SNIX_BUILD_SANDBOX_SHELL. The implementation: - Adds embedded-busybox feature flag in Cargo.toml - Embeds busybox binary at compile time when feature is enabled - Extracts embedded binary to temp directory at runtime - Falls back to SNIX_BUILD_SANDBOX_SHELL env var when feature disabled - Updates OCIBuildService to accept sandbox_shell via URL parameters 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Change-Id: I46e3856b7d5d8a65abd4938e713ecaece61f24cc
1 parent abb2daa commit e78ad76

File tree

7 files changed

+161
-13
lines changed

7 files changed

+161
-13
lines changed

snix/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

snix/Cargo.nix

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13553,6 +13553,11 @@ rec {
1355313553
name = "prost";
1355413554
packageId = "prost";
1355513555
}
13556+
{
13557+
name = "serde";
13558+
packageId = "serde";
13559+
features = [ "derive" ];
13560+
}
1355613561
{
1355713562
name = "serde_json";
1355813563
packageId = "serde_json";
@@ -13627,7 +13632,7 @@ rec {
1362713632
features = {
1362813633
"tonic-reflection" = [ "dep:tonic-reflection" "snix-castore/tonic-reflection" ];
1362913634
};
13630-
resolvedDefaultFeatures = [ "default" "tonic-reflection" ];
13635+
resolvedDefaultFeatures = [ "default" "embedded-busybox" "tonic-reflection" ];
1363113636
};
1363213637
"snix-castore" = rec {
1363313638
crateName = "snix-castore";

snix/build/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ data-encoding = "2.5.0"
2626
futures = "0.3.30"
2727
oci-spec = "0.7.0"
2828
nix = { version = "0.29.0", features = ["user"] }
29+
serde = { version = "1.0", features = ["derive"] }
2930
serde_json = "1.0.111"
3031
snix-tracing = { path = "../tracing" }
3132
uuid = { version = "1.7.0", features = ["v4"] }
@@ -37,6 +38,7 @@ tonic-build.workspace = true
3738
[features]
3839
default = []
3940
tonic-reflection = ["dep:tonic-reflection", "snix-castore/tonic-reflection"]
41+
embedded-busybox = []
4042

4143
[dev-dependencies]
4244
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: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,133 @@
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+
#[cfg(all(target_os = "linux", feature = "embedded-busybox"))]
6+
use std::sync::OnceLock;
37
use url::Url;
48

59
#[cfg(target_os = "linux")]
610
use super::oci::OCIBuildService;
711

12+
/// Extract the embedded busybox binary to a temporary location and return its path
13+
#[cfg(all(target_os = "linux", feature = "embedded-busybox"))]
14+
fn get_embedded_busybox_path() -> Result<PathBuf, std::io::Error> {
15+
use std::fs;
16+
use std::os::unix::fs::PermissionsExt;
17+
use std::sync::Mutex;
18+
19+
static EXTRACTED_BUSYBOX_PATH: OnceLock<Result<PathBuf, String>> = OnceLock::new();
20+
static INIT_MUTEX: Mutex<()> = Mutex::new(());
21+
22+
// The embedded busybox binary (included at compile time)
23+
static EMBEDDED_BUSYBOX_BINARY: &[u8] = include_bytes!(env!("SNIX_BUILD_SANDBOX_SHELL"));
24+
25+
let result = EXTRACTED_BUSYBOX_PATH.get_or_init(|| {
26+
let _guard = INIT_MUTEX.lock().unwrap();
27+
28+
let temp_dir = std::env::temp_dir();
29+
let busybox_path = temp_dir.join(format!("snix-busybox-{}", std::process::id()));
30+
31+
// Write the binary
32+
if let Err(e) = fs::write(&busybox_path, EMBEDDED_BUSYBOX_BINARY) {
33+
return Err(e.to_string());
34+
}
35+
36+
// Make it executable
37+
match fs::metadata(&busybox_path) {
38+
Ok(metadata) => {
39+
let mut perms = metadata.permissions();
40+
perms.set_mode(0o755);
41+
if let Err(e) = fs::set_permissions(&busybox_path, perms) {
42+
return Err(e.to_string());
43+
}
44+
}
45+
Err(e) => return Err(e.to_string()),
46+
}
47+
48+
tracing::debug!(?busybox_path, "extracted embedded busybox binary");
49+
50+
Ok(busybox_path)
51+
});
52+
53+
match result {
54+
Ok(path) => Ok(path.clone()),
55+
Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e.clone())),
56+
}
57+
}
58+
59+
/// Configuration for OCIBuildService
60+
#[cfg(target_os = "linux")]
61+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
62+
#[serde(deny_unknown_fields)]
63+
pub struct OCIBuildServiceConfig {
64+
/// Root path in which all bundles are created
65+
pub bundle_root: PathBuf,
66+
67+
/// Path to the sandbox shell to use.
68+
/// This needs to be a statically linked binary, or you must ensure all
69+
/// dependencies are part of the build (which they usually are not).
70+
#[serde(default = "default_sandbox_shell")]
71+
pub sandbox_shell: PathBuf,
72+
// TODO: make rootless_uid_gid configurable
73+
}
74+
75+
#[cfg(target_os = "linux")]
76+
fn default_sandbox_shell() -> PathBuf {
77+
// Priority: embedded busybox -> SNIX_BUILD_SANDBOX_SHELL env var -> error
78+
#[cfg(feature = "embedded-busybox")]
79+
{
80+
if let Ok(path) = get_embedded_busybox_path() {
81+
return path;
82+
}
83+
tracing::warn!("failed to extract embedded busybox, falling back to environment variable");
84+
}
85+
86+
// Check environment variable (for backward compatibility when feature is disabled)
87+
if let Ok(shell_path) = std::env::var("SNIX_BUILD_SANDBOX_SHELL") {
88+
return PathBuf::from(shell_path);
89+
}
90+
91+
panic!(
92+
"No sandbox shell found. The OCI build service requires a statically linked shell binary.\n\
93+
\n\
94+
To build snix with nixpkgs, use one of these methods:\n\
95+
1. Set SNIX_BUILD_SANDBOX_SHELL to a statically linked shell:\n\
96+
export SNIX_BUILD_SANDBOX_SHELL=$(nix-build '<nixpkgs>' -A busybox-sandbox-shell --no-out-link)/bin/busybox\n\
97+
\n\
98+
2. Build with the embedded-busybox feature (also requires SNIX_BUILD_SANDBOX_SHELL at compile time):\n\
99+
export SNIX_BUILD_SANDBOX_SHELL=$(nix-build '<nixpkgs>' -A busybox-sandbox-shell --no-out-link)/bin/busybox\n\
100+
cargo build --features embedded-busybox\n\
101+
\n\
102+
3. Pass the shell path via URL query parameter:\n\
103+
oci:///path/to/bundle?sandbox_shell=/path/to/static/shell"
104+
);
105+
}
106+
107+
#[cfg(target_os = "linux")]
108+
impl TryFrom<Url> for OCIBuildServiceConfig {
109+
type Error = Box<dyn std::error::Error + Send + Sync>;
110+
111+
fn try_from(url: Url) -> Result<Self, Self::Error> {
112+
// oci wants a path in which it creates bundles
113+
if url.path().is_empty() {
114+
return Err("oci needs a bundle dir as path".into());
115+
}
116+
117+
// Parse sandbox_shell from query parameters
118+
let query_pairs: std::collections::HashMap<_, _> = url.query_pairs().collect();
119+
let sandbox_shell = query_pairs
120+
.get("sandbox_shell")
121+
.map(|s| PathBuf::from(s.as_ref()))
122+
.unwrap_or_else(default_sandbox_shell);
123+
124+
Ok(OCIBuildServiceConfig {
125+
bundle_root: url.path().into(),
126+
sandbox_shell,
127+
})
128+
}
129+
}
130+
8131
/// Constructs a new instance of a [BuildService] from an URI.
9132
///
10133
/// The following schemes are supported by the following services:
@@ -32,17 +155,14 @@ where
32155
"dummy" => Box::<DummyBuildService>::default(),
33156
#[cfg(target_os = "linux")]
34157
"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
158+
let config = OCIBuildServiceConfig::try_from(url)
159+
.map_err(|e| std::io::Error::other(format!("invalid oci config: {}", e)))?;
41160

42161
Box::new(OCIBuildService::new(
43-
url.path().into(),
162+
config.bundle_root,
44163
blob_service,
45164
directory_service,
165+
config.sandbox_shell,
46166
))
47167
}
48168
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ in
6767
inherit cargoDeps src;
6868
name = "snix-rust-docs";
6969
PROTO_ROOT = protos;
70-
SNIX_BUILD_SANDBOX_SHELL = "/homeless-shelter";
70+
SNIX_BUILD_SANDBOX_SHELL = if pkgs.stdenv.isLinux then pkgs.busybox-sandbox-shell + "/bin/busybox" else "/bin/sh";
7171

7272
nativeBuildInputs = with pkgs; [
7373
cargo
@@ -93,7 +93,7 @@ in
9393
inherit cargoDeps src;
9494
name = "snix-clippy";
9595
PROTO_ROOT = protos;
96-
SNIX_BUILD_SANDBOX_SHELL = "/homeless-shelter";
96+
SNIX_BUILD_SANDBOX_SHELL = if pkgs.stdenv.isLinux then pkgs.busybox-sandbox-shell + "/bin/busybox" else "/bin/sh";
9797

9898
buildInputs = [
9999
pkgs.fuse

0 commit comments

Comments
 (0)