Skip to content
Draft
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
15 changes: 15 additions & 0 deletions sdks/c/include/boxlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,21 @@ void boxlite_options_set_auto_remove(CBoxliteOptions *opts, int val);

void boxlite_options_set_detach(CBoxliteOptions *opts, int val);

// Pick a sandbox security preset by name.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be consistent with boxlite_options_new, boxlite_options_set_rootfs_path, etc.

//
// `preset` is a C string — `"development"`, `"standard"`, or
// `"maximum"` (case-insensitive; `"dev"` / `"default"` / `"max"` /
// `"strict"` also accepted). NULL clears any previously-set preset.
//
// The preset name is validated at `boxlite_create_box` time, not
// here — an unknown name surfaces from create as
// `BoxliteErrorCode::InvalidArgument` with the offending value in
// `out_error`. Silent fallback to the default is never acceptable.
//
// Signature matches sibling `boxlite_options_set_*` setters so the
// C SDK stays uniform; the error path lives at create.
void boxlite_options_set_security_preset(CBoxliteOptions *opts, const char *preset);

void boxlite_options_set_entrypoint(CBoxliteOptions *opts, const char *const *args, int argc);

void boxlite_options_set_cmd(CBoxliteOptions *opts, const char *const *args, int argc);
Expand Down
20 changes: 19 additions & 1 deletion sdks/c/src/box_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,26 @@ unsafe fn create_box(
}
let cb = crate::unwrap_cb_or_return!(cb, out_error);

// Resolve any pending security preset before we take ownership
// of `opts`. On a bad name the caller still owns + frees opts,
// matching the null-runtime / null-cb error paths above.
let resolved_security = match (*opts).pending_security_preset.as_ref() {
Some(preset) => match boxlite::SecurityOptions::from_preset(preset) {
Ok(sec) => Some(sec),
Err(e) => {
write_error(out_error, e);
return BoxliteErrorCode::InvalidArgument;
}
},
None => None,
};

let runtime_ref = &*runtime;
let opts_handle = Box::from_raw(opts);
let mut opts_handle = Box::from_raw(opts);
opts_handle.pending_security_preset = None;
if let Some(sec) = resolved_security {
opts_handle.options.advanced.security = sec;
}
let runtime_clone = runtime_ref.runtime.clone();
let tokio_rt = runtime_ref.tokio_rt.clone();
let queue = runtime_ref.queue.clone();
Expand Down
47 changes: 47 additions & 0 deletions sdks/c/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ use crate::{CBoxliteError, CBoxliteOptions};
pub struct OptionsHandle {
pub options: BoxOptions,
pub name: Option<String>,
/// Security preset name (e.g. "maximum") stashed by
/// `boxlite_options_set_security_preset`. Resolved in
/// `boxlite_create_box`; unknown names surface as
/// `InvalidArgument` from there, not from the setter.
pub pending_security_preset: Option<String>,
}

#[unsafe(no_mangle)]
Expand Down Expand Up @@ -129,6 +134,27 @@ pub unsafe extern "C" fn boxlite_options_set_detach(opts: *mut CBoxliteOptions,
options_set_detach(opts, val)
}

/// Pick a sandbox security preset by name.
///
/// `preset` is a C string — `"development"`, `"standard"`, or
/// `"maximum"` (case-insensitive; `"dev"` / `"default"` / `"max"` /
/// `"strict"` also accepted). NULL clears any previously-set preset.
///
/// The preset name is validated at `boxlite_create_box` time, not
/// here — an unknown name surfaces from create as
/// `BoxliteErrorCode::InvalidArgument` with the offending value in
/// `out_error`. Silent fallback to the default is never acceptable.
///
/// Signature matches sibling `boxlite_options_set_*` setters so the
/// C SDK stays uniform; the error path lives at create.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn boxlite_options_set_security_preset(
opts: *mut CBoxliteOptions,
preset: *const c_char,
) {
unsafe { options_set_security_preset(opts, preset) }
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn boxlite_options_set_entrypoint(
opts: *mut CBoxliteOptions,
Expand Down Expand Up @@ -177,6 +203,7 @@ pub unsafe fn options_new(
..Default::default()
},
name: None,
pending_security_preset: None,
});

*out_opts = Box::into_raw(handle);
Expand Down Expand Up @@ -241,6 +268,26 @@ pub unsafe fn options_set_workdir(handle: *mut OptionsHandle, workdir: *const c_
}
}

/// Internal helper backing `boxlite_options_set_security_preset`.
///
/// Stashes the name; `boxlite_create_box` calls
/// `SecurityOptions::from_preset` to validate + apply. NULL preset
/// clears any previously-stashed name.
pub unsafe fn options_set_security_preset(handle: *mut OptionsHandle, preset: *const c_char) {
unsafe {
if handle.is_null() {
return;
}
if preset.is_null() {
(*handle).pending_security_preset = None;
return;
}
if let Ok(s) = c_str_to_string(preset) {
(*handle).pending_security_preset = Some(s);
}
}
}

pub unsafe fn options_add_env(handle: *mut OptionsHandle, key: *const c_char, val: *const c_char) {
unsafe {
if handle.is_null() || key.is_null() || val.is_null() {
Expand Down
57 changes: 57 additions & 0 deletions sdks/c/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,63 @@ fn create_box_rejects_null_callback() {
let _ = std::fs::remove_dir_all(home_dir);
}

// Pins the second half of the deferred-validation contract for
// security presets: `boxlite_options_set_security_preset` accepts any
// C string without complaint, but `boxlite_create_box` synchronously
// rejects an unknown preset with InvalidArgument + offending value in
// the error — so the typo can never silently fall through to the
// default. Companion to `security_from_preset_unknown_surfaces_invalid_argument`
// in boxlite::runtime::options (which pins the underlying parser).
extern "C" fn create_cb_must_not_fire(
_box_handle: *mut crate::CBoxHandle,
_error: *mut crate::CBoxliteError,
_user_data: *mut c_void,
) {
panic!("callback must not fire when preset is rejected synchronously");
}

#[test]
fn create_box_rejects_unknown_security_preset() {
let (runtime, home_dir) = unsafe { new_test_runtime_handle("bad-preset-create") };

let image = CString::new("alpine:latest").expect("image cstring");
let mut opts: *mut CBoxliteOptions = ptr::null_mut();
let mut error = FFIError::default();
let opts_code =
unsafe { boxlite_options_new(image.as_ptr(), &mut opts as *mut _, &mut error as *mut _) };
assert_eq!(opts_code, BoxliteErrorCode::Ok);

let preset = CString::new("ultra").expect("preset cstring");
unsafe { boxlite_options_set_security_preset(opts, preset.as_ptr()) };

let code = unsafe {
boxlite_create_box(
runtime,
opts,
Some(create_cb_must_not_fire),
ptr::null_mut(),
&mut error as *mut _,
)
};
assert_eq!(code, BoxliteErrorCode::InvalidArgument);
assert!(!error.message.is_null());
let msg = unsafe { CStr::from_ptr(error.message) }
.to_string_lossy()
.into_owned();
assert!(
msg.contains("ultra"),
"error should echo the offending preset: {msg}"
);

// Sync failure: opts not consumed; caller still owns + frees.
unsafe {
boxlite_error_free(&mut error as *mut _);
boxlite_options_free(opts);
boxlite_runtime_free(runtime);
}
let _ = std::fs::remove_dir_all(home_dir);
}

#[test]
fn runtime_metrics_rejects_null_callback() {
let (runtime, home_dir) = unsafe { new_test_runtime_handle("null-cb-rtmet") };
Expand Down
34 changes: 34 additions & 0 deletions sdks/go/boxlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,40 @@ func TestBuildCOptions_MissingImageAndPath(t *testing.T) {
}
}

// ============================================================================
// Security preset
// ============================================================================
//
// `WithSecurityPreset` stashes the name on the boxConfig; `buildCOptions`
// forwards it to the C SDK, which resolves it at Create time. So
// `buildCOptions` itself can't see a bad preset — the rejection path
// lives at Create. We pin what buildCOptions CAN observe here:
// valid presets round-trip without error, and empty is a no-op.
//
// The "unknown preset → InvalidArgument" policy is covered by
// `security_from_preset_unknown_surfaces_invalid_argument` (Rust
// boxlite::runtime::options) and `create_box_rejects_unknown_security_preset`
// (C SDK integration).

func TestBuildCOptions_SecurityPresetValid(t *testing.T) {
for _, preset := range []string{"development", "standard", "maximum", "STANDARD", "max"} {
cfg := &boxConfig{}
WithSecurityPreset(preset)(cfg)
if err := buildAndFreeCOptions("alpine:latest", cfg); err != nil {
t.Fatalf("WithSecurityPreset(%q) must apply cleanly; got error: %v", preset, err)
}
}
}

func TestBuildCOptions_SecurityPresetEmptyKeepsDefault(t *testing.T) {
// Empty string = WithSecurityPreset never called effectively;
// leaves the runtime default in place. Must not error.
cfg := &boxConfig{securityPreset: ""}
if err := buildAndFreeCOptions("alpine:latest", cfg); err != nil {
t.Fatalf("empty preset must be a no-op; got error: %v", err)
}
}

// ============================================================================
// State constants
// ============================================================================
Expand Down
74 changes: 60 additions & 14 deletions sdks/go/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,21 @@ type Secret struct {
}

type boxConfig struct {
name string
cpus int
memoryMiB int
diskSizeGB int
rootfsPath string
env [][2]string
volumes []volumeEntry
workDir string
entrypoint []string
cmd []string
autoRemove *bool
detach *bool
network *NetworkSpec
secrets []Secret
name string
cpus int
memoryMiB int
diskSizeGB int
rootfsPath string
env [][2]string
volumes []volumeEntry
workDir string
entrypoint []string
cmd []string
autoRemove *bool
detach *bool
network *NetworkSpec
secrets []Secret
securityPreset string // empty = server default; else "development" / "standard" / "maximum"
}

type volumeEntry struct {
Expand Down Expand Up @@ -204,6 +205,42 @@ func WithDetach(v bool) BoxOption {
return func(c *boxConfig) { c.detach = &v }
}

// buildAndFreeCOptions runs buildCOptions, immediately frees the C
// handle on success, and returns just the error. Exists for unit
// tests in `_test.go` files, which Go forbids from using cgo
// directly — without this helper they can't exercise buildCOptions
// because the caller has to free `*C.CBoxliteOptions`.
func buildAndFreeCOptions(image string, cfg *boxConfig) error {
opts, err := buildCOptions(image, cfg)
if err != nil {
return err
}
if opts != nil {
C.boxlite_options_free(opts)
}
return nil
}

// WithSecurityPreset picks a sandbox security preset by name.
//
// Accepts one of "development" / "standard" / "maximum" (case-
// insensitive). Empty string (the zero value) leaves the box on the
// server / runtime default, which is the standard preset.
//
// Examples:
//
// box, err := runtime.Create(ctx, "alpine:latest",
// boxlite.WithSecurityPreset("maximum"))
// box, err := runtime.Create(ctx, "alpine:latest",
// boxlite.WithSecurityPreset("development")) // disable isolation for debugging
//
// An invalid preset name surfaces at box creation as an
// InvalidArgument error, not silently — silent fallback would be how
// operators end up unsandboxed by accident.
func WithSecurityPreset(preset string) BoxOption {
return func(c *boxConfig) { c.securityPreset = preset }
}

func buildCOptions(image string, cfg *boxConfig) (*C.CBoxliteOptions, error) {
image = strings.TrimSpace(image)
rootfsPath := strings.TrimSpace(cfg.rootfsPath)
Expand Down Expand Up @@ -311,6 +348,15 @@ func buildCOptions(image string, cfg *boxConfig) (*C.CBoxliteOptions, error) {
if cfg.detach != nil {
C.boxlite_options_set_detach(cOpts, boolToCInt(*cfg.detach))
}
if cfg.securityPreset != "" {
// Preset name is validated at Create, not here — a typo
// surfaces as InvalidArgument from Runtime.Create with the
// offending value in the error. Silent fallthrough to default
// is never acceptable.
cPreset := toCString(cfg.securityPreset)
C.boxlite_options_set_security_preset(cOpts, cPreset)
C.free(unsafe.Pointer(cPreset))
}
if cfg.entrypoint != nil {
cArgs, argc := toCStringArray(cfg.entrypoint)
C.boxlite_options_set_entrypoint(cOpts, cArgs, C.int(argc))
Expand Down
Loading
Loading