From ef99627001e74c6726ab2e85486fee4ce901adbe Mon Sep 17 00:00:00 2001 From: unknown <1784931579@qq.com> Date: Tue, 18 Aug 2026 20:57:40 +0800 Subject: [PATCH] feat(sandbox): persisted disable_auto_tmpfs policy opts /tmp onto the root overlay (#1377) Every OCI-image sandbox gets an automatic RAM-backed tmpfs at /tmp, sized memory/4 capped at 512 MiB. The cap makes the formula inert above 2 GiB of RAM, the space is charged to guest memory rather than the root disk, and the one place builds stage their largest artifacts gets 512 MiB no matter how large --root-disk is. There was no opt-out: --tmpfs /tmp:0 suppressed the default but then mounted a useless 0-byte RAM disk. The opt-out lives where the default is applied, as a persisted policy: - SandboxRuntimeOptions gains disable_auto_tmpfs (serde-defaulted, sibling of disable_metrics_sample), settable from the builder via disable_auto_tmpfs(). - apply_runtime_defaults returns before pushing the automatic mount when the policy is set, so the config and the actual mount state stay consistent. - The CLI maps --tmpfs /tmp:0 to the policy (and adds no mount); a zero-sized tmpfs at any other path simply adds no mount. Also documents the automatic tmpfs, its sizing, and both tuning shapes in docs/sandboxes/volumes.mdx, and gives the three sizing constants doc comments. Tests: the policy suppresses the automatic mount at the config layer; without it the default is applied exactly as before; the CLI parses /tmp:0 as size zero for the policy mapping. --- crates/cli/lib/commands/common.rs | 20 ++++++ docs/sandboxes/volumes.mdx | 18 ++++++ .../microsandbox-types/rust/lib/domain.rs | 5 ++ sdk/rust/lib/runtime/spawn.rs | 63 +++++++++++++++++++ sdk/rust/lib/sandbox/builder.rs | 8 +++ sdk/rust/lib/sandbox/config.rs | 19 ++++++ 6 files changed, 133 insertions(+) diff --git a/crates/cli/lib/commands/common.rs b/crates/cli/lib/commands/common.rs index de3411dc2..42bc49d59 100644 --- a/crates/cli/lib/commands/common.rs +++ b/crates/cli/lib/commands/common.rs @@ -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) { + // #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 { @@ -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(); + 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"); diff --git a/docs/sandboxes/volumes.mdx b/docs/sandboxes/volumes.mdx index 9ea9c0f03..0c2985c65 100644 --- a/docs/sandboxes/volumes.mdx +++ b/docs/sandboxes/volumes.mdx @@ -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=` | | `--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. diff --git a/packages/microsandbox-types/rust/lib/domain.rs b/packages/microsandbox-types/rust/lib/domain.rs index 37cd0e293..5eed3c7ba 100644 --- a/packages/microsandbox-types/rust/lib/domain.rs +++ b/packages/microsandbox-types/rust/lib/domain.rs @@ -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. diff --git a/sdk/rust/lib/runtime/spawn.rs b/sdk/rust/lib/runtime/spawn.rs index 51556cd85..8a1038bac 100644 --- a/sdk/rust/lib/runtime/spawn.rs +++ b/sdk/rust/lib/runtime/spawn.rs @@ -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") diff --git a/sdk/rust/lib/sandbox/builder.rs b/sdk/rust/lib/sandbox/builder.rs index 83e26d99d..14e632b9e 100644 --- a/sdk/rust/lib/sandbox/builder.rs +++ b/sdk/rust/lib/sandbox/builder.rs @@ -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) -> Self { self.config.spec.runtime.user = Some(user.into()); diff --git a/sdk/rust/lib/sandbox/config.rs b/sdk/rust/lib/sandbox/config.rs index c45fcecfa..943a1ab08 100644 --- a/sdk/rust/lib/sandbox/config.rs +++ b/sdk/rust/lib/sandbox/config.rs @@ -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; @@ -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