Skip to content

Commit 7d75b39

Browse files
domenkozarclaude
andcommitted
feat(build): add embedded-sandbox-shell support for OCI builds
This adds an optional "embedded-sandbox-shell" feature that embeds the sandbox shell 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-sandbox-shell feature flag in Cargo.toml - Requires SNIX_BUILD_SANDBOX_SHELL at compile time for all Linux builds - When feature is enabled: embeds binary contents at compile time - When feature is disabled: bakes the path at compile time via option_env\!() - Extracts embedded binary to temp directory at runtime (when feature enabled) - Makes sandbox_shell configurable via URL query parameter for OCIBuildService - Updates OCIBuildService to accept sandbox shell path as constructor parameter This ensures the sandbox shell is always available without runtime environment dependencies, whether as a baked path or embedded binary. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Change-Id: Ia5121f7872902d0d0b6afd2b99f45cb5d6537c77
1 parent abb2daa commit 7d75b39

File tree

7 files changed

+182
-13
lines changed

7 files changed

+182
-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-sandbox-shell" "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-sandbox-shell = []
4042

4143
[dev-dependencies]
4244
rstest.workspace = true

snix/build/build.rs

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

33
fn main() -> Result<()> {
4+
// SNIX_BUILD_SANDBOX_SHELL is required at compile time for Linux builds
5+
#[cfg(target_os = "linux")]
6+
{
7+
if let Ok(shell_path) = std::env::var("SNIX_BUILD_SANDBOX_SHELL") {
8+
// Tell cargo to rerun if the sandbox shell binary changes
9+
println!("cargo:rerun-if-changed={}", shell_path);
10+
11+
// When embedded-sandbox-shell feature is enabled, verify the file exists
12+
#[cfg(feature = "embedded-sandbox-shell")]
13+
{
14+
if !std::path::Path::new(&shell_path).exists() {
15+
panic!(
16+
"SNIX_BUILD_SANDBOX_SHELL points to non-existent file: {}",
17+
shell_path
18+
);
19+
}
20+
}
21+
} else {
22+
panic!(
23+
"SNIX_BUILD_SANDBOX_SHELL environment variable must be set at compile time for Linux builds"
24+
);
25+
}
26+
}
427
#[allow(unused_mut)]
528
let mut builder = tonic_build::configure();
629

snix/build/src/buildservice/from_addr.rs

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,143 @@
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-sandbox-shell"))]
6+
use std::sync::OnceLock;
37
use url::Url;
48

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

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

42171
Box::new(OCIBuildService::new(
43-
url.path().into(),
172+
config.bundle_root,
44173
blob_service,
45174
directory_service,
175+
config.sandbox_shell,
46176
))
47177
}
48178
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)