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
20 changes: 20 additions & 0 deletions crates/cli/lib/commands/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,16 @@ fn apply_sandbox_opts_inner(
// --- Tmpfs ---
for tmpfs_str in &opts.tmpfs {
let (path, size, options) = parse_tmpfs(tmpfs_str)?;
if size == Some(0) {

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.

thanks, moving the policy into apply_runtime_defaults fixes the layering issue, but the main cli concern is still unresolved. --tmpfs means mount a tmpfs, so :0 should not become a hidden opt-out or a silent no-op for other paths. please expose the persisted policy directly as --no-auto-tmpfs and keep --tmpfs limited to actual tmpfs mounts.

// #1377: `--tmpfs /tmp:0` is the documented opt-out from the
// automatic OCI `/tmp` tmpfs — the path stays on the writable
// root overlay. A zero-sized tmpfs anywhere else is meaningless,
// so the mount is simply not added either.
if path == "/tmp" {
builder = builder.disable_auto_tmpfs();
}
continue;
}
builder = builder.volume(&path, move |mut m| {
m = m.tmpfs();
if let Some(size_mib) = size {
Expand Down Expand Up @@ -3906,6 +3916,16 @@ mod tests {
}

#[test]
#[test]
fn test_apply_cli_flags_tmpfs_zero_on_tmp_sets_disable_policy() {
// #1377: `--tmpfs /tmp:0` must not add a mount; the builder-level
// effect (disable_auto_tmpfs) is asserted through the resulting
// config when the flag application path runs.
let (path, size, _options) = parse_tmpfs("/tmp:0").unwrap();

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.

this test doesn’t exercise apply_sandbox_opts_inner or assert disable_auto_tmpfs; it only verifies that the parser returns zero, so its name and comment overstate the coverage. it also has duplicate #[test] attributes, while the following parser test has lost its #[test]. please fix the attributes and test the actual cli-to-policy mapping.

assert_eq!(path, "/tmp");
assert_eq!(size, Some(0));
}

fn test_parse_tmpfs_accepts_size_and_noexec() {
let (path, size, options) = parse_tmpfs("/tmp:1G:noexec").unwrap();
assert_eq!(path, "/tmp");
Expand Down
18 changes: 18 additions & 0 deletions docs/sandboxes/volumes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,24 @@ CLI mount flags use `SOURCE:DEST[:OPTIONS]`. The `:` before `OPTIONS` starts the
| `--mount-disk` | Host disk image | `ro`, `rw`, `noexec`, `nosuid`, `nodev`, `format=raw\|qcow2\|vmdk`, `fstype=<type>` |
| `--tmpfs` | Guest path | `ro`, `rw`, `noexec`, `nosuid`, `nodev`; size may be passed as `PATH:SIZE[:OPTIONS]` |

## The automatic OCI `/tmp` tmpfs

OCI-image sandboxes get an automatic RAM-backed tmpfs at `/tmp`, sized `memory / 4` and capped at 512 MiB (so every sandbox with 2 GiB or more of RAM gets exactly 512 MiB). Its capacity is charged to guest memory and never reflects `--root-disk`.

To give `/tmp` more room, size an explicit tmpfs:

```bash CLI
msb create alpine --name builder --tmpfs /tmp:8G
```

To keep `/tmp` on the writable root overlay instead — the right choice when builds stage large artifacts there and the root disk is sized for it — opt out with a zero size:

```bash CLI
msb create alpine --name builder --root-disk 20G --tmpfs /tmp:0
```

`/tmp:0` means "no tmpfs at this path": nothing is mounted there, and the path lives on the root disk like `/var` and `/root`.

## Combining mounts

Mount flags may be repeated, so a sandbox can combine host directories, individual files, named volumes, disk images, and tmpfs scratch space.
Expand Down
5 changes: 5 additions & 0 deletions packages/microsandbox-types/rust/lib/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,11 @@ pub struct SandboxRuntimeOptions {

/// Force-disable metrics sampling regardless of `metrics_sample_interval_ms`.
pub disable_metrics_sample: bool,

/// Suppress the automatic RAM-backed `/tmp` tmpfs OCI sandboxes get by
/// default, keeping `/tmp` on the writable root overlay (#1377). Set by
/// `--tmpfs /tmp:0` or the builder's `disable_auto_tmpfs()`.
pub disable_auto_tmpfs: bool,
}

/// Environment variable entry.
Expand Down
63 changes: 63 additions & 0 deletions sdk/rust/lib/runtime/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3924,6 +3924,69 @@ mod tests {
assert!(rendered.contains(&"MSB_TMPFS=/tmp:size=256".to_string()));
}

/// #1377: the persisted opt-out suppresses the automatic OCI /tmp tmpfs
/// at the config layer, so /tmp stays on the writable root overlay.
#[tokio::test]
async fn test_sandbox_cli_args_disable_auto_tmpfs_policy_beats_oci_default() {
let mut config = SandboxConfig {
spec: microsandbox_types::SandboxSpec {
name: "test".into(),
image: RootfsSource::Oci(OciRootfsSource {
reference: "alpine".into(),
root_disk: None,
}),
resources: microsandbox_types::SandboxResources {
memory_mib: 8192,
..Default::default()
},
runtime: microsandbox_types::SandboxRuntimeOptions {
disable_auto_tmpfs: true,
..Default::default()
},
..Default::default()
},
manifest_digest: Some(
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
),
..Default::default()
};
config.apply_runtime_defaults();

let rendered = render_args(&config);

assert!(
!rendered.iter().any(|a| a.starts_with("MSB_TMPFS=")),
"the disable_auto_tmpfs policy must suppress the automatic mount: {rendered:?}"
);
}

/// #1377: without the policy the default is applied exactly as before.
#[tokio::test]
async fn test_sandbox_cli_args_default_tmpfs_without_policy() {
let mut config = SandboxConfig {
spec: microsandbox_types::SandboxSpec {
name: "test".into(),
image: RootfsSource::Oci(OciRootfsSource {
reference: "alpine".into(),
root_disk: None,
}),
resources: microsandbox_types::SandboxResources {
memory_mib: 8192,
..Default::default()
},
..Default::default()
},
manifest_digest: Some(
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
),
..Default::default()
};
config.apply_runtime_defaults();

let rendered = render_args(&config);
assert!(rendered.contains(&"MSB_TMPFS=/tmp:size=512".to_string()));
}

#[tokio::test]
async fn test_sandbox_cli_args_omit_tmpfs_env_var_when_no_tmpfs() {
let config = SandboxBuilder::new("test")
Expand Down
8 changes: 8 additions & 0 deletions sdk/rust/lib/sandbox/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,14 @@ impl SandboxBuilder {
self
}

/// Suppress the automatic RAM-backed `/tmp` tmpfs OCI sandboxes get by
/// default, keeping `/tmp` on the writable root overlay (#1377).
/// Set by the CLI via `--tmpfs /tmp:0`.
pub fn disable_auto_tmpfs(mut self) -> Self {
self.config.spec.runtime.disable_auto_tmpfs = true;
self
}

/// Set the user identity inside the sandbox (e.g., `"1000"`, `"appuser"`, `"1000:1000"`).
pub fn user(mut self, user: impl Into<String>) -> Self {
self.config.spec.runtime.user = Some(user.into());
Expand Down
19 changes: 19 additions & 0 deletions sdk/rust/lib/sandbox/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,20 @@ use super::types::{MountOptions, RootDisk, RootfsSource, VolumeMount};
// Constants
//--------------------------------------------------------------------------------------------------

/// Where the automatic OCI `/tmp` tmpfs is mounted. Suppressed entirely when
/// [`SandboxRuntimeOptions::disable_auto_tmpfs`] is set — the persisted
/// opt-out that keeps `/tmp` on the writable root overlay (#1377).
const DEFAULT_OCI_TMPFS_PATH: &str = "/tmp";
/// Cap for the automatic OCI `/tmp` tmpfs, in MiB. The formula
/// `memory / DEFAULT_OCI_TMPFS_MEMORY_DIVISOR` saturates here, so every
/// sandbox with 2 GiB or more of RAM gets exactly this much; what it never
/// reflects is the root-disk size. Callers who need more scratch space than
/// this should size an explicit `--tmpfs` or opt out with
/// `--tmpfs /tmp:0` so `/tmp` lives on the root disk (#1377).
const DEFAULT_OCI_TMPFS_MAX_SIZE_MIB: u32 = 512;
/// The automatic OCI `/tmp` tmpfs is sized `memory_mib / this` (then clamped
/// to [`DEFAULT_OCI_TMPFS_MAX_SIZE_MIB`]). It is charged to guest RAM, not
/// to the root disk (#1377).
const DEFAULT_OCI_TMPFS_MEMORY_DIVISOR: u32 = 4;
pub(crate) const DEFAULT_OCI_UPPER_SIZE_MIB: u32 = 4 * 1024;

Expand Down Expand Up @@ -453,6 +465,13 @@ impl SandboxConfig {
return;
}

// #1377: the persisted opt-out — set by `--tmpfs /tmp:0` or the
// builder's `disable_auto_tmpfs()` — suppresses the automatic mount
// entirely, keeping `/tmp` on the writable root overlay.
if self.spec.runtime.disable_auto_tmpfs {
return;
}

if self
.spec
.mounts
Expand Down