Skip to content
Open
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: 2 additions & 0 deletions snix/Cargo.lock

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

11 changes: 10 additions & 1 deletion snix/Cargo.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13553,10 +13553,19 @@ rec {
name = "prost";
packageId = "prost";
}
{
name = "serde";
packageId = "serde";
features = [ "derive" ];
}
{
name = "serde_json";
packageId = "serde_json";
}
{
name = "serde_qs";
packageId = "serde_qs";
}
{
name = "snix-castore";
packageId = "snix-castore";
Expand Down Expand Up @@ -13627,7 +13636,7 @@ rec {
features = {
"tonic-reflection" = [ "dep:tonic-reflection" "snix-castore/tonic-reflection" ];
};
resolvedDefaultFeatures = [ "default" "tonic-reflection" ];
resolvedDefaultFeatures = [ "default" "embedded-sandbox-shell" "tonic-reflection" ];
};
"snix-castore" = rec {
crateName = "snix-castore";
Expand Down
3 changes: 3 additions & 0 deletions snix/build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ data-encoding = "2.5.0"
futures = "0.3.30"
oci-spec = "0.7.0"
nix = { version = "0.29.0", features = ["user"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.111"
serde_qs.workspace = true
snix-tracing = { path = "../tracing" }
uuid = { version = "1.7.0", features = ["v4"] }

Expand All @@ -37,6 +39,7 @@ tonic-build.workspace = true
[features]
default = []
tonic-reflection = ["dep:tonic-reflection", "snix-castore/tonic-reflection"]
embedded-sandbox-shell = []

[dev-dependencies]
rstest.workspace = true
Expand Down
23 changes: 23 additions & 0 deletions snix/build/build.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
use std::io::Result;

fn main() -> Result<()> {
// SNIX_BUILD_SANDBOX_SHELL is required at compile time for Linux builds
#[cfg(target_os = "linux")]
{
if let Ok(shell_path) = std::env::var("SNIX_BUILD_SANDBOX_SHELL") {
// Tell cargo to rerun if the sandbox shell binary changes
println!("cargo:rerun-if-changed={}", shell_path);

// When embedded-sandbox-shell feature is enabled, verify the file exists
#[cfg(feature = "embedded-sandbox-shell")]
{
if !std::path::Path::new(&shell_path).exists() {
panic!(
"SNIX_BUILD_SANDBOX_SHELL points to non-existent file: {}",
shell_path
);
}
}
} else {
panic!(
"SNIX_BUILD_SANDBOX_SHELL environment variable must be set at compile time for Linux builds"
);
}
}
#[allow(unused_mut)]
let mut builder = tonic_build::configure();

Expand Down
13 changes: 5 additions & 8 deletions snix/build/src/buildservice/from_addr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use snix_castore::{blobservice::BlobService, directoryservice::DirectoryService}
use url::Url;

#[cfg(target_os = "linux")]
use super::oci::OCIBuildService;
use super::oci::{OCIBuildService, OCIBuildServiceConfig};

/// Constructs a new instance of a [BuildService] from an URI.
///
Expand Down Expand Up @@ -32,17 +32,14 @@ where
"dummy" => Box::<DummyBuildService>::default(),
#[cfg(target_os = "linux")]
"oci" => {
// oci wants a path in which it creates bundles.
if url.path().is_empty() {
Err(std::io::Error::other("oci needs a bundle dir as path"))?
}

// TODO: make sandbox shell and rootless_uid_gid
let config = OCIBuildServiceConfig::try_from(url)
.map_err(|e| std::io::Error::other(format!("invalid oci config: {}", e)))?;

Box::new(OCIBuildService::new(
url.path().into(),
config.bundle_root,
blob_service,
directory_service,
config.sandbox_shell,
))
}
scheme => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
mod sandbox_shell;

use anyhow::Context;
use bstr::BStr;
use serde::Deserialize;
use snix_castore::{
blobservice::BlobService,
directoryservice::DirectoryService,
Expand All @@ -10,6 +13,7 @@ use snix_castore::{
use tokio::process::{Child, Command};
use tonic::async_trait;
use tracing::{Span, debug, instrument, warn};
use url::Url;
use uuid::Uuid;

use crate::buildservice::{BuildOutput, BuildRequest, BuildResult};
Expand All @@ -18,9 +22,44 @@ use std::{ffi::OsStr, path::PathBuf, process::Stdio};

use super::BuildService;

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

/// Configuration for OCIBuildService
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OCIBuildServiceConfig {
/// Root path in which all bundles are created
pub bundle_root: PathBuf,

/// Path to the sandbox shell to use.
/// This needs to be a statically linked binary, or you must ensure all
/// dependencies are part of the build (which they usually are not).
#[serde(default = "sandbox_shell::default_sandbox_shell")]
pub sandbox_shell: PathBuf,
// TODO: make rootless_uid_gid configurable
}

impl TryFrom<Url> for OCIBuildServiceConfig {
type Error = Box<dyn std::error::Error + Send + Sync>;

fn try_from(mut url: Url) -> Result<Self, Self::Error> {
// oci wants a path in which it creates bundles
if url.path().is_empty() {
return Err("oci needs a bundle dir as path".into());
}

// Add the bundle_root from the URL path to the query parameters
let path = url.path().to_string();
url.query_pairs_mut().append_pair("bundle_root", &path);

// Parse the query string into the config struct using serde_qs
let config: OCIBuildServiceConfig = serde_qs::from_str(url.query().unwrap_or_default())
.map_err(|e| format!("failed to parse OCI service parameters: {}", e))?;

Ok(config)
}
}

pub struct OCIBuildService<BS, DS> {
/// Root path in which all bundles are created in
bundle_root: PathBuf,
Expand All @@ -30,13 +69,21 @@ pub struct OCIBuildService<BS, DS> {
/// Handle to a [DirectoryService], used by filesystems spawned during builds.
directory_service: DS,

/// Path to the sandbox shell to use
sandbox_shell: PathBuf,

// semaphore to track number of concurrently running builds.
// this is necessary, as otherwise we very quickly run out of open file handles.
concurrent_builds: tokio::sync::Semaphore,
}

impl<BS, DS> OCIBuildService<BS, DS> {
pub fn new(bundle_root: PathBuf, blob_service: BS, directory_service: DS) -> Self {
pub fn new(
bundle_root: PathBuf,
blob_service: BS,
directory_service: DS,
sandbox_shell: PathBuf,
) -> Self {
// We map root inside the container to the uid/gid this is running at,
// and allocate one for uid 1000 into the container from the range we
// got in /etc/sub{u,g}id.
Expand All @@ -45,6 +92,7 @@ impl<BS, DS> OCIBuildService<BS, DS> {
bundle_root,
blob_service,
directory_service,
sandbox_shell,
concurrent_builds: tokio::sync::Semaphore::new(MAX_CONCURRENT_BUILDS),
}
}
Expand All @@ -66,7 +114,7 @@ where
let span = Span::current();
span.record("bundle_name", bundle_name.to_string());

let mut runtime_spec = make_spec(&request, true, SANDBOX_SHELL)
let mut runtime_spec = make_spec(&request, true, &self.sandbox_shell)
.context("failed to create spec")
.map_err(std::io::Error::other)?;

Expand Down
87 changes: 87 additions & 0 deletions snix/build/src/buildservice/oci/sandbox_shell.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::path::PathBuf;

/// Compile-time path to sandbox shell (when embedded-sandbox-shell feature is disabled)
#[cfg(not(feature = "embedded-sandbox-shell"))]
const SNIX_BUILD_SANDBOX_SHELL: &str = env!("SNIX_BUILD_SANDBOX_SHELL");

/// Extract the embedded sandbox shell binary to a temporary location and return its path
fn get_embedded_sandbox_shell_path() -> Result<PathBuf, std::io::Error> {
#[cfg(feature = "embedded-sandbox-shell")]
{
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::sync::{Mutex, OnceLock};

static EXTRACTED_SANDBOX_SHELL_PATH: OnceLock<Result<PathBuf, String>> = OnceLock::new();
static INIT_MUTEX: Mutex<()> = Mutex::new(());

// The embedded sandbox shell binary (included at compile time)
static EMBEDDED_SANDBOX_SHELL_BINARY: &[u8] =
include_bytes!(env!("SNIX_BUILD_SANDBOX_SHELL"));

let result = EXTRACTED_SANDBOX_SHELL_PATH.get_or_init(|| {
let _guard = INIT_MUTEX.lock().expect("mutex lock failed");

let temp_dir = std::env::temp_dir();
let sandbox_shell_path =
temp_dir.join(format!("snix-sandbox-shell-{}", std::process::id()));

// Write the binary
if let Err(e) = fs::write(&sandbox_shell_path, EMBEDDED_SANDBOX_SHELL_BINARY) {
return Err(e.to_string());
}

// Make it executable
match fs::metadata(&sandbox_shell_path) {
Ok(metadata) => {
let mut perms = metadata.permissions();
perms.set_mode(0o755);
if let Err(e) = fs::set_permissions(&sandbox_shell_path, perms) {
return Err(e.to_string());
}
}
Err(e) => return Err(e.to_string()),
}

tracing::debug!(
sandbox_shell.path = ?sandbox_shell_path,
"extracted embedded sandbox shell binary"
);

Ok(sandbox_shell_path)
});

match result {
Ok(path) => Ok(path.clone()),
Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e.clone())),
}
}

#[cfg(not(feature = "embedded-sandbox-shell"))]
{
unreachable!(
"get_embedded_sandbox_shell_path called without embedded-sandbox-shell feature"
)
}
}

pub(crate) fn default_sandbox_shell() -> PathBuf {
if cfg!(feature = "embedded-sandbox-shell") {
// Extract and use the embedded binary
match get_embedded_sandbox_shell_path() {
Ok(path) => path,
Err(e) => {
panic!(
"Failed to extract embedded sandbox shell: {}\n\
\n\
The embedded sandbox shell could not be extracted to a temporary location.\n\
This might be due to insufficient permissions or disk space in the temp directory.",
e
);
}
}
} else {
// Use the compile-time path
PathBuf::from(SNIX_BUILD_SANDBOX_SHELL)
}
}
4 changes: 2 additions & 2 deletions snix/build/src/oci/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub enum SpecError {
pub(crate) fn make_spec(
request: &BuildRequest,
rootless: bool,
sandbox_shell: &str,
sandbox_shell: &Path,
) -> Result<oci_spec::runtime::Spec, SpecError> {
let allow_network = request
.constraints
Expand All @@ -64,7 +64,7 @@ pub(crate) fn make_spec(
.constraints
.contains(&BuildConstraints::ProvideBinSh)
{
ro_host_mounts.push((Path::new(sandbox_shell), Path::new("/bin/sh")))
ro_host_mounts.push((sandbox_shell, Path::new("/bin/sh")))
}

oci_spec::runtime::SpecBuilder::default()
Expand Down
6 changes: 4 additions & 2 deletions snix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ in
inherit cargoDeps src;
name = "snix-rust-docs";
PROTO_ROOT = protos;
SNIX_BUILD_SANDBOX_SHELL = "/homeless-shelter";
# This path is resolved at build time in the Nix build environment
SNIX_BUILD_SANDBOX_SHELL = if pkgs.stdenv.isLinux then pkgs.busybox-sandbox-shell + "/bin/busybox" else "/bin/sh";

nativeBuildInputs = with pkgs; [
cargo
Expand All @@ -93,7 +94,8 @@ in
inherit cargoDeps src;
name = "snix-clippy";
PROTO_ROOT = protos;
SNIX_BUILD_SANDBOX_SHELL = "/homeless-shelter";
# This path is resolved at build time in the Nix build environment
SNIX_BUILD_SANDBOX_SHELL = if pkgs.stdenv.isLinux then pkgs.busybox-sandbox-shell + "/bin/busybox" else "/bin/sh";

buildInputs = [
pkgs.fuse
Expand Down