diff --git a/CubeAPI/src/cubemaster/mod.rs b/CubeAPI/src/cubemaster/mod.rs index fb4fe481f..8056a4990 100644 --- a/CubeAPI/src/cubemaster/mod.rs +++ b/CubeAPI/src/cubemaster/mod.rs @@ -1840,6 +1840,9 @@ pub struct CreateTemplateFromImageReq { skip_serializing_if = "Option::is_none" )] pub cube_network_config: Option, + /// Whether the template build sandbox should include ivshmem. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_ivshmem: Option, /// Whether CubeMaster bakes the CubeEgress root CA into the template rootfs. #[serde(skip_serializing_if = "Option::is_none")] pub with_cube_ca: Option, diff --git a/CubeAPI/src/models/mod.rs b/CubeAPI/src/models/mod.rs index 3d271c87b..be505b6d2 100644 --- a/CubeAPI/src/models/mod.rs +++ b/CubeAPI/src/models/mod.rs @@ -700,6 +700,9 @@ pub struct CreateTemplateRequest { /// Denied outbound CIDRs for CubeVS egress policy. #[serde(rename = "denyOut", default)] pub deny_out: Option>, + /// Enable ivshmem when building the template snapshot. + #[serde(rename = "enableIvshmem", default)] + pub enable_ivshmem: Option, /// Whether CubeMaster bakes the CubeEgress root CA into the template rootfs. /// Omitted or null defaults to true on CubeMaster. #[serde(rename = "with_cube_ca", default)] diff --git a/CubeAPI/src/services/templates.rs b/CubeAPI/src/services/templates.rs index 60c3f94df..ab4774c57 100644 --- a/CubeAPI/src/services/templates.rs +++ b/CubeAPI/src/services/templates.rs @@ -135,6 +135,7 @@ impl TemplateService { distribution_scope: non_empty_vec(body.nodes), container_overrides, cube_network_config, + enable_ivshmem: body.enable_ivshmem, with_cube_ca: body.with_cube_ca, }; @@ -533,6 +534,7 @@ mod tests { dns: Some(vec!["8.8.8.8".to_string(), "1.1.1.1".to_string()]), allow_out: Some(vec!["172.67.0.0/16".to_string()]), deny_out: Some(vec!["10.0.0.0/8".to_string()]), + enable_ivshmem: Some(true), with_cube_ca: Some(false), } } diff --git a/CubeMaster/cmd/cubemastercli/commands/cubebox/template.go b/CubeMaster/cmd/cubemastercli/commands/cubebox/template.go index cbbc5fa94..53533c31d 100644 --- a/CubeMaster/cmd/cubemastercli/commands/cubebox/template.go +++ b/CubeMaster/cmd/cubemastercli/commands/cubebox/template.go @@ -170,6 +170,14 @@ func mergeCreateFromImageCubeNetworkConfigFlags(c *cli.Context, existing *types. return mergeCubeNetworkConfigValues(existing, hasAllowInternetAccess, allowInternetAccess, allowOut, denyOut), nil } +func applyCreateFromImageIvshmemFlag(c *cli.Context, req *types.CreateTemplateFromImageReq) { + if !c.IsSet("enable-ivshmem") { + return + } + enableIvshmem := c.Bool("enable-ivshmem") + req.EnableIvshmem = &enableIvshmem +} + func parseCreateFromImageExtraNetworkFlags(c *cli.Context) (*createFromImageExtraNetworkFlags, error) { extraArgs := make([]string, 0, c.NArg()) for i := 0; i < c.NArg(); i++ { @@ -763,6 +771,7 @@ var TemplateCreateFromImageCommand = cli.Command{ cli.StringSliceFlag{Name: "deny-out-cidr", Usage: "append a denied egress CIDR to cube_network_config; repeat the flag to specify multiple CIDRs"}, cli.StringFlag{Name: "registry-username", Usage: "registry username"}, cli.StringFlag{Name: "registry-password", Usage: "registry password"}, + cli.BoolFlag{Name: "enable-ivshmem", Usage: "boot the template build sandbox with ivshmem enabled"}, cli.StringSliceFlag{Name: "cmd", Usage: "override container ENTRYPOINT (command); repeat for multiple elements, e.g. --cmd /bin/sh --cmd -c"}, cli.StringSliceFlag{Name: "arg", Usage: "override container CMD (args); repeat for multiple elements"}, @@ -816,6 +825,7 @@ var TemplateCreateFromImageCommand = cli.Command{ // SDKs) can still rely on `nil = server-side default`. withCubeCA := c.BoolT("with-cube-ca") req.WithCubeCA = &withCubeCA + applyCreateFromImageIvshmemFlag(c, req) req.CubeNetworkConfig, err = mergeCreateFromImageCubeNetworkConfigFlags(c, req.CubeNetworkConfig) if err != nil { return err diff --git a/CubeMaster/cmd/cubemastercli/commands/cubebox/template_test.go b/CubeMaster/cmd/cubemastercli/commands/cubebox/template_test.go index 02f495192..78f3d5ee0 100644 --- a/CubeMaster/cmd/cubemastercli/commands/cubebox/template_test.go +++ b/CubeMaster/cmd/cubemastercli/commands/cubebox/template_test.go @@ -123,6 +123,20 @@ func TestCreateFromImageCommandParsesNodeScope(t *testing.T) { } } +func TestApplyCreateFromImageIvshmemFlag(t *testing.T) { + withoutFlag := &types.CreateTemplateFromImageReq{} + applyCreateFromImageIvshmemFlag(newCreateFromImageContext(t, nil), withoutFlag) + if withoutFlag.EnableIvshmem != nil { + t.Fatalf("EnableIvshmem=%v, want nil when flag is not set", *withoutFlag.EnableIvshmem) + } + + withFlag := &types.CreateTemplateFromImageReq{} + applyCreateFromImageIvshmemFlag(newCreateFromImageContext(t, []string{"--enable-ivshmem"}), withFlag) + if withFlag.EnableIvshmem == nil || !*withFlag.EnableIvshmem { + t.Fatalf("EnableIvshmem=%v, want true", withFlag.EnableIvshmem) + } +} + func TestMergeCreateFromImageCubeNetworkConfigFlagsRejectsUnexpectedArgs(t *testing.T) { ctx := newCreateFromImageContext(t, []string{ "--allow-internet-access", "false", diff --git a/CubeMaster/pkg/base/constants/constants.go b/CubeMaster/pkg/base/constants/constants.go index b0c940896..efed01666 100644 --- a/CubeMaster/pkg/base/constants/constants.go +++ b/CubeMaster/pkg/base/constants/constants.go @@ -82,6 +82,7 @@ const ( // CubeAnnotationCreateTimeEnvVars stores the serialized create-time env map // that CubeMaster passes to cubelet for envd initialization. CubeAnnotationCreateTimeEnvVars = "cube.master.internal.create_time_env_vars" + CubeAnnotationEnableIvshmem = "cube.master.enable_ivshmem" CubeAnnotationsVirtiofsCache = "cube.master.virtiofs.cache" diff --git a/CubeMaster/pkg/service/sandbox/types/types.go b/CubeMaster/pkg/service/sandbox/types/types.go index 520d1320f..9285fbba0 100644 --- a/CubeMaster/pkg/service/sandbox/types/types.go +++ b/CubeMaster/pkg/service/sandbox/types/types.go @@ -565,6 +565,11 @@ type CreateTemplateFromImageReq struct { // false → skip the bake entirely // See design/cube-egress-ca-bake.md. WithCubeCA *bool `json:"with_cube_ca,omitempty"` + + // EnableIvshmem controls whether the template build sandbox should boot + // with ivshmem enabled so the captured snapshot already contains the + // device topology. + EnableIvshmem *bool `json:"enable_ivshmem,omitempty"` } type RedoTemplateFromImageReq struct { diff --git a/CubeMaster/pkg/templatecenter/request_validation.go b/CubeMaster/pkg/templatecenter/request_validation.go index 7db4d2892..20a128e61 100644 --- a/CubeMaster/pkg/templatecenter/request_validation.go +++ b/CubeMaster/pkg/templatecenter/request_validation.go @@ -97,6 +97,9 @@ func normalizeTemplateImageRequest(req *types.CreateTemplateFromImageReq) (*type if cloned.NetworkType == "" { cloned.NetworkType = cubeboxv1.NetworkType_tap.String() } + if cloned.EnableIvshmem != nil && !*cloned.EnableIvshmem { + cloned.EnableIvshmem = nil + } if err := validateTemplateCubeNetworkConfig(cloned.CubeNetworkConfig); err != nil { return nil, err } diff --git a/CubeMaster/pkg/templatecenter/template_image_test.go b/CubeMaster/pkg/templatecenter/template_image_test.go index ba00fa3ec..bdf9686bb 100644 --- a/CubeMaster/pkg/templatecenter/template_image_test.go +++ b/CubeMaster/pkg/templatecenter/template_image_test.go @@ -68,6 +68,22 @@ func TestNormalizeTemplateImageRequestIgnoresProvidedTemplateID(t *testing.T) { } } +func TestNormalizeTemplateImageRequestDropsDisabledIvshmemFlag(t *testing.T) { + disabled := false + req, err := normalizeTemplateImageRequest(&types.CreateTemplateFromImageReq{ + Request: &types.Request{RequestID: "req-1"}, + SourceImageRef: "docker.io/library/nginx:latest", + WritableLayerSize: "20Gi", + EnableIvshmem: &disabled, + }) + if err != nil { + t.Fatalf("normalizeTemplateImageRequest failed: %v", err) + } + if req.EnableIvshmem != nil { + t.Fatal("EnableIvshmem should be canonicalized to nil when false") + } +} + func TestNormalizeTemplateImageRequestNormalizesExposedPorts(t *testing.T) { req, err := normalizeTemplateImageRequest(&types.CreateTemplateFromImageReq{ @@ -663,6 +679,57 @@ func TestGenerateTemplateCreateRequestClonesCubeNetworkRules(t *testing.T) { } } +func TestGenerateTemplateCreateRequestAddsIvshmemAnnotation(t *testing.T) { + enabled := true + req := &types.CreateTemplateFromImageReq{ + Request: &types.Request{RequestID: "req-1"}, + SourceImageRef: "docker.io/library/nginx:latest", + TemplateID: "template-1", + WritableLayerSize: "20Gi", + InstanceType: cubeboxv1.InstanceType_cubebox.String(), + NetworkType: cubeboxv1.NetworkType_tap.String(), + EnableIvshmem: &enabled, + } + artifact := &models.RootfsArtifact{ + ArtifactID: "artifact-1", + TemplateSpecFingerprint: "fingerprint-1", + Ext4SHA256: "sha256-1", + Ext4SizeBytes: 1024, + DownloadToken: "token-1", + } + got, err := generateTemplateCreateRequest(req, artifact, image.DockerImageConfig{}, "http://master.example") + if err != nil { + t.Fatalf("generateTemplateCreateRequest failed: %v", err) + } + if got.Annotations[constants.CubeAnnotationEnableIvshmem] != "true" { + t.Fatalf("expected ivshmem annotation to be set, got %q", got.Annotations[constants.CubeAnnotationEnableIvshmem]) + } +} + +func TestBuildTemplateSpecFingerprintIgnoresIvshmemFlag(t *testing.T) { + enabled := true + reqA := &types.CreateTemplateFromImageReq{ + Request: &types.Request{RequestID: "req-a"}, + SourceImageRef: "docker.io/library/nginx:latest", + WritableLayerSize: "20Gi", + InstanceType: cubeboxv1.InstanceType_cubebox.String(), + NetworkType: cubeboxv1.NetworkType_tap.String(), + } + reqB := &types.CreateTemplateFromImageReq{ + Request: &types.Request{RequestID: "req-b"}, + SourceImageRef: reqA.SourceImageRef, + WritableLayerSize: reqA.WritableLayerSize, + InstanceType: reqA.InstanceType, + NetworkType: reqA.NetworkType, + EnableIvshmem: &enabled, + } + gotA := buildTemplateSpecFingerprint(reqA, "sha256:source") + gotB := buildTemplateSpecFingerprint(reqB, "sha256:source") + if gotA != gotB { + t.Fatalf("ivshmem should not affect rootfs artifact fingerprint: %q vs %q", gotA, gotB) + } +} + func TestMarshalTemplateImageJobRequestIgnoresRequestIDAndPassword(t *testing.T) { reqA := &types.CreateTemplateFromImageReq{ Request: &types.Request{RequestID: "req-a"}, diff --git a/CubeMaster/pkg/templatecenter/template_request.go b/CubeMaster/pkg/templatecenter/template_request.go index 116db4a41..06b062e3a 100644 --- a/CubeMaster/pkg/templatecenter/template_request.go +++ b/CubeMaster/pkg/templatecenter/template_request.go @@ -37,6 +37,9 @@ func generateTemplateCreateRequest(req *types.CreateTemplateFromImageReq, artifa if len(req.ExposedPorts) > 0 { annotations[constants.AnnotationsExposedPort] = formatExposedPortsAnnotation(req.ExposedPorts) } + if req.EnableIvshmem != nil && *req.EnableIvshmem { + annotations[constants.CubeAnnotationEnableIvshmem] = "true" + } rootVolume := &types.Volume{ Name: rootfsWritableVolumeName, VolumeSource: &types.VolumeSource{ diff --git a/CubeShim/shim/src/common/utils.rs b/CubeShim/shim/src/common/utils.rs index 93b41a8db..40732e582 100644 --- a/CubeShim/shim/src/common/utils.rs +++ b/CubeShim/shim/src/common/utils.rs @@ -20,7 +20,7 @@ use serde_json; use std::fs::{self, File}; use std::io::{ErrorKind, Read, Write}; use std::os::unix::io::AsRawFd; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process; use std::str::FromStr; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -41,9 +41,51 @@ const DEV_URANDOM: &str = "/dev/urandom"; pub const NET_DEVICE_ID_PRE: &str = "tap"; pub const DISK_DEVICE_ID_PRE: &str = "disk"; +// ivshmem shared memory configuration +const IVSHMEM_SHM_DIR: &str = "/dev/shm"; +const IVSHMEM_PREFIX: &str = "ivshmem-"; + pub struct Utils {} pub struct AsyncUtils {} impl Utils { + /// Validate sandbox_id before using it in filesystem paths. + fn validate_sandbox_id(id: &str) -> CResult<()> { + if id.is_empty() || id.len() > 255 { + return Err("invalid sandbox_id length".into()); + } + if id.contains("..") || id.contains('/') || id.contains('\\') { + return Err("sandbox_id contains invalid path characters".into()); + } + Ok(()) + } + + /// Return `/dev/shm/ivshmem-{sandbox_id}` for the sandbox. + pub fn ivshmem_path(sandbox_id: &str) -> CResult { + Self::validate_sandbox_id(sandbox_id)?; + Ok(PathBuf::from(format!( + "{}/{}{}", + IVSHMEM_SHM_DIR, IVSHMEM_PREFIX, sandbox_id + ))) + } + + /// Create or truncate an ivshmem backend file with 0o600 permissions. + pub fn create_ivshmem_file(path: &Path, size: usize) -> CResult<()> { + use std::os::unix::fs::OpenOptionsExt; + + let f = File::options() + .create(true) + .write(true) + .truncate(true) + .mode(0o600) + .open(path) + .map_err(|e| format!("failed to create ivshmem file {}: {}", path.display(), e))?; + + f.set_len(size as u64) + .map_err(|e| format!("failed to set ivshmem file size: {}", e))?; + + Ok(()) + } + pub fn load_spec(bundle: &str) -> CResult { let mut conf_path = PathBuf::from(bundle); conf_path.push("config.json"); @@ -163,6 +205,20 @@ impl Utils { )), }; + //delete ivshmem shared memory file + let ret_ivshmem: Result<(), String> = match Self::ivshmem_path(sandbox_id) { + Ok(ivshmem_path) => match fs::remove_file(&ivshmem_path) { + Ok(_) => Ok(()), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), + Err(e) => Err(format!( + "failed to delete {}: {}", + ivshmem_path.display(), + e + )), + }, + Err(e) => Err(format!("resolve ivshmem path failed:{}", e)), + }; + let mut err_msg = String::new(); if let Err(e) = ret_vmdir { err_msg = err_msg + e.as_str(); @@ -172,6 +228,10 @@ impl Utils { err_msg = err_msg + e.as_str(); } + if let Err(e) = ret_ivshmem { + err_msg = err_msg + e.as_str(); + } + if err_msg.is_empty() { return Ok(()); } @@ -580,4 +640,110 @@ mod tests { let base = Utils::virtiofs_guest_base("123".to_string()); assert_eq!(base, format!("{}/{}", GUEST_VIRTIOFS_MNT_PATH, "123")); } + + // ivshmem path validation tests + #[test] + fn test_ivshmem_path_valid() { + let path = Utils::ivshmem_path("sb-12345").unwrap(); + assert_eq!(path, PathBuf::from("/dev/shm/ivshmem-sb-12345")); + } + + #[test] + fn test_ivshmem_path_empty() { + let result = Utils::ivshmem_path(""); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("invalid sandbox_id length")); + } + + #[test] + fn test_ivshmem_path_too_long() { + let long_id = "a".repeat(256); + let result = Utils::ivshmem_path(&long_id); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("invalid sandbox_id length")); + } + + #[test] + fn test_ivshmem_path_traversal_dotdot() { + let result = Utils::ivshmem_path("../etc/passwd"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("invalid path characters")); + } + + #[test] + fn test_ivshmem_path_traversal_slash() { + let result = Utils::ivshmem_path("foo/bar"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("invalid path characters")); + } + + #[test] + fn test_ivshmem_path_traversal_backslash() { + let result = Utils::ivshmem_path("foo\\bar"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("invalid path characters")); + } + + #[test] + fn test_ivshmem_path_embedded_dotdot() { + let result = Utils::ivshmem_path("foo..bar"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("invalid path characters")); + } + + #[test] + fn test_ivshmem_path_max_length() { + let id = "a".repeat(255); + let result = Utils::ivshmem_path(&id); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + PathBuf::from(format!("/dev/shm/ivshmem-{}", "a".repeat(255))) + ); + } + + #[test] + fn test_create_ivshmem_file_success() { + use std::os::unix::fs::PermissionsExt; + + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test-ivshmem-{}", std::process::id())); + + // Clean up in case of previous failed test + let _ = fs::remove_file(&test_file); + + // Create file + let result = Utils::create_ivshmem_file(&test_file, 1024 * 1024); + assert!(result.is_ok(), "Failed to create file: {:?}", result.err()); + + // Verify file exists + assert!(test_file.exists(), "File was not created"); + + // Verify size + let metadata = fs::metadata(&test_file).unwrap(); + assert_eq!(metadata.len(), 1024 * 1024, "File size incorrect"); + + // Verify permissions (0o600) + let mode = metadata.permissions().mode(); + assert_eq!( + mode & 0o777, + 0o600, + "File permissions incorrect: {:o}", + mode & 0o777 + ); + + // Clean up + fs::remove_file(&test_file).unwrap(); + } + + #[test] + fn test_create_ivshmem_file_invalid_path() { + let invalid_path = PathBuf::from("/nonexistent/impossible/directory/test-ivshmem"); + let result = Utils::create_ivshmem_file(&invalid_path, 1024); + + assert!(result.is_err(), "Should fail with invalid path"); + assert!(result + .unwrap_err() + .contains("failed to create ivshmem file")); + } } diff --git a/CubeShim/shim/src/hypervisor/config.rs b/CubeShim/shim/src/hypervisor/config.rs index f0b4db0ad..78e157cbc 100644 --- a/CubeShim/shim/src/hypervisor/config.rs +++ b/CubeShim/shim/src/hypervisor/config.rs @@ -17,8 +17,8 @@ use crate::sandbox::pmem::Pmem; use cube_hypervisor::config::{RateLimiterConfig, TokenBucketConfig}; use cube_hypervisor::vm_config::{ - ConsoleConfig, ConsoleOutputMode, CpuTopology, DiskConfig, FsConfig, MacAddr, NetConfig, - PayloadConfig, PmemConfig, RngConfig, VmConfig as VC, VsockConfig, + ConsoleConfig, ConsoleOutputMode, CpuTopology, DiskConfig, FsConfig, IvshmemConfig, MacAddr, + NetConfig, PayloadConfig, PmemConfig, RngConfig, VmConfig as VC, VsockConfig, }; use cube_hypervisor::vmm_config::VmmConfig; @@ -52,6 +52,7 @@ impl HypConfig { #[derive(Clone, Debug)] pub struct VmConfig { + pub ivshmem: Option, pub vcpus: u32, pub memory_size: u64, pub dirty_log: bool, @@ -106,6 +107,7 @@ impl Default for VmConfig { ..Default::default() }; VmConfig { + ivshmem: None, vcpus: 0, memory_size: 0, dirty_log: false, @@ -169,9 +171,20 @@ impl VmConfig { if let Some(vs) = self.vsock.clone() { vc.vsock = Some(vs) } + if let Some(ivshmem) = self.ivshmem.clone() { + vc.ivshmem = Some(ivshmem) + } vc } + /// Enable ivshmem shared memory channel with a caller-supplied backend + /// file path. The caller is responsible for creating and sizing the + /// file before calling this. This keeps the shim agnostic of the + /// backend naming convention. + pub fn enable_ivshmem(&mut self, path: PathBuf, size: usize) { + self.ivshmem = Some(IvshmemConfig { path, size }); + } + pub fn add_cmdline(&mut self, cmd: String) -> &mut Self { self.cmdlines.push(cmd); self diff --git a/CubeShim/shim/src/sandbox/sb.rs b/CubeShim/shim/src/sandbox/sb.rs index 71ef17aba..58fc9a9d4 100644 --- a/CubeShim/shim/src/sandbox/sb.rs +++ b/CubeShim/shim/src/sandbox/sb.rs @@ -23,7 +23,7 @@ use containerd_shim::protos::events::task::TaskOOM; use containerd_shim::protos::protobuf::MessageDyn; use containerd_shim::{Error, Result}; use cube_hypervisor::config::RestoreConfig; -use cube_hypervisor::vm_config::{DeviceConfig, FsConfig}; +use cube_hypervisor::vm_config::{DeviceConfig, FsConfig, IvshmemConfig}; use cube_hypervisor::{SnapshotType, VmRemoveDeviceData}; use oci_spec::runtime::{LinuxResources, Process, Spec}; use protoc::{agent, agent_ttrpc, health, health_ttrpc}; @@ -47,6 +47,8 @@ use super::pmem::Pmem; //use tokio_uring::fs::UnixStream; const ANNO_SANDBOX_DNS: &str = "cube.sandbox.dns"; +const ANNO_ENABLE_IVSHMEM: &str = "cube.master.enable_ivshmem"; +const IVSHMEM_DEFAULT_SIZE: usize = 1 * 1024 * 1024; // 1MB #[derive(PartialEq, Eq)] enum SandBoxState { @@ -721,6 +723,12 @@ impl SandBox { .add_virtiofs(&self.conf.virtiofs) .add_vsock(self.id.clone()); + // Enable ivshmem device when the template build path sets the internal annotation. + if self.is_ivshmem_enabled() { + Self::enable_default_ivshmem(&mut vc, &self.id) + .map_err(|e| format!("failed to enable ivshmem: {}", e))?; + } + if let Some(fs) = self.conf.fs.as_ref() { vc.add_fs(fs); } @@ -761,6 +769,48 @@ impl SandBox { Ok(()) } + fn is_ivshmem_enabled(&self) -> bool { + self.spec + .annotations() + .as_ref() + .and_then(|anno| anno.get(ANNO_ENABLE_IVSHMEM)) + .map(|v| v == "true" || v == "1") + .unwrap_or(false) + } + + /// Enable the default ivshmem backend at `/dev/shm/ivshmem-{sandbox_id}`. + fn enable_default_ivshmem(vc: &mut VmConfig, sandbox_id: &str) -> CResult<()> { + let path = Utils::ivshmem_path(sandbox_id)?; + Utils::create_ivshmem_file(&path, IVSHMEM_DEFAULT_SIZE)?; + vc.enable_ivshmem(path, IVSHMEM_DEFAULT_SIZE); + Ok(()) + } + + /// Build restore-time ivshmem config with the default backend path. + fn default_ivshmem_config(sandbox_id: &str) -> CResult { + let path = Utils::ivshmem_path(sandbox_id)?; + Ok(IvshmemConfig { + path, + size: IVSHMEM_DEFAULT_SIZE, + }) + } + + /// Ensure the default ivshmem backend file exists before restore. + fn ensure_ivshmem_file(sandbox_id: &str) -> CResult<()> { + let path = Utils::ivshmem_path(sandbox_id)?; + match stdfs::metadata(&path) { + Ok(_) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + Utils::create_ivshmem_file(&path, IVSHMEM_DEFAULT_SIZE) + } + Err(e) => Err(format!( + "failed to stat ivshmem file {}: {}", + path.display(), + e + )), + } + } + fn by_snapshot(&self) -> bool { let anno = self.spec.annotations().as_ref().unwrap(); if anno.contains_key(config::ANNO_SNAPSHOT_DISABLE) { @@ -836,6 +886,13 @@ impl SandBox { } async fn restore_vm(&mut self) -> CResult<()> { + // Ensure the sandbox-specific ivshmem shm file exists when enabled by template annotation. + let enable_ivshmem = self.is_ivshmem_enabled(); + + if enable_ivshmem { + Self::ensure_ivshmem_file(&self.id)?; + } + let ss_file = SnapshotInfo::load( self.conf.snapshot_base.as_str(), self.conf.vm_res.cpu, @@ -882,6 +939,11 @@ impl SandBox { pmem: Some(pmems), vsock: Some(vsock), memory_vol_url: restore_memory_vol_url, + ivshmem: if enable_ivshmem { + Some(Self::default_ivshmem_config(&self.id)?) + } else { + None + }, ..Default::default() }; diff --git a/CubeShim/shim/src/service/update_ext.rs b/CubeShim/shim/src/service/update_ext.rs index ac6359d00..27e0ac30a 100644 --- a/CubeShim/shim/src/service/update_ext.rs +++ b/CubeShim/shim/src/service/update_ext.rs @@ -82,6 +82,7 @@ impl From for RestoreConfig { prefault: r.prefault, dirty_log: r.dirty_log, memory_vol_url: r.memory_vol_url, + ivshmem: None, } } } diff --git a/examples/ivshmem/README.md b/examples/ivshmem/README.md new file mode 100644 index 000000000..9cbf45a0f --- /dev/null +++ b/examples/ivshmem/README.md @@ -0,0 +1,228 @@ +# ivshmem + +Minimal examples for using `ivshmem` as an optional host/guest shared-memory +channel in CubeSandbox. + +These examples are meant to answer three practical questions: + +1. How do I enable `ivshmem` for a template? +2. Where does the shared-memory region appear on the host and in the guest? +3. How can I build a simple protocol on top of that shared memory? + +## What you get + +| File | What it shows | +| --- | --- | +| `ivshmem_ring_demo.py` | A minimal host/guest request-reply flow over two small ring buffers | +| `ivshmem_benchmark.py` | Host-side mmap throughput against `/dev/shm/ivshmem-{sandbox_id}` | + +## 1. Background + +CubeSandbox keeps `ivshmem` as a template-level opt-in. + +When a template is built with `enableIvshmem=true`, each sandbox created from +that template gets its own host-side backend file: + +```text +/dev/shm/ivshmem-{sandbox_id} +``` + +Inside the guest, the same shared-memory region is exposed as an `ivshmem` PCI +device. A guest process can open the BAR resource file and use `mmap()` to +exchange data with the host. + +This makes `ivshmem` a good fit for custom data paths where you want to define +your own in-memory layout or ring-buffer protocol between host and guest. + +## 2. Prerequisites + +- A running CubeSandbox deployment +- Python 3.8+ +- An image that can be used to build a template + +Install dependencies: + +```bash +pip install "cubesandbox>=0.5.0" +``` + +Or install from the repository helper files if you prefer: + +```bash +pip install -r ../code-sandbox-quickstart/requirements.txt +``` + +## 3. Quick Start + +### Step 1 — Build an ivshmem-enabled Template + +Use the Python SDK: + +```python +from cubesandbox import Template + +job = Template.build( + image="cube-sandbox-cn.tencentcloudcr.com/cube-sandbox/sandbox-code:latest", + enable_ivshmem=True, +) + +template_id = job.template_id +``` + +Or use `cubemastercli`: + +```bash +cubemastercli tpl create-from-image \ + --image cube-sandbox-cn.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \ + --enable-ivshmem +``` + +Note the `template_id` printed on success. + +### Step 2 — Run the Ring Demo + +Create a temporary sandbox from the template and run the end-to-end demo: + +```bash +python examples/ivshmem/ivshmem_ring_demo.py \ + --template \ + --message "ping from host" \ + --cleanup +``` + +Expected output: + +```text +sandbox_id: ... +host shm: /dev/shm/ivshmem-... +host sent: ping from host +guest received: ping from host +host recv: hello-from-guest: ping from host +``` + +### Step 3 — Run the Host-side mmap Benchmark + +```bash +python examples/ivshmem/ivshmem_benchmark.py \ + --template \ + --count 3 \ + --cleanup +``` + +This benchmark measures host access to `/dev/shm/ivshmem-{sandbox_id}`. It is +useful for checking host-side backend behavior across repeated sandbox runs. + +## 4. How the Shared Memory Appears + +### On the host + +Each sandbox gets its own backend file: + +```text +/dev/shm/ivshmem-{sandbox_id} +``` + +The host can open that file and map it directly: + +```python +import mmap + +shm_path = f"/dev/shm/ivshmem-{sandbox_id}" + +with open(shm_path, "r+b", buffering=0) as f: + mm = mmap.mmap(f.fileno(), 1024 * 1024) + mm[:16] = b"hello-from-host" + mm.flush() + mm.close() +``` + +### In the guest + +The guest sees an `ivshmem` PCI device with vendor/device `0x1af4/0x1110`. +The shared-memory BAR is exposed as `resource2`. + +This snippet finds the device and maps the BAR: + +```python +import mmap +import os + +resource = None +for name in os.listdir("/sys/bus/pci/devices"): + d = f"/sys/bus/pci/devices/{name}" + try: + vendor = open(f"{d}/vendor").read().strip() + device = open(f"{d}/device").read().strip() + except OSError: + continue + if vendor == "0x1af4" and device == "0x1110": + resource = f"{d}/resource2" + break + +if resource is None: + raise RuntimeError("ivshmem PCI device not found") + +with open(resource, "r+b", buffering=0) as f: + mm = mmap.mmap(f.fileno(), 1024 * 1024) + print(bytes(mm[:16])) + mm.close() +``` + +## 5. Example Design + +### `ivshmem_ring_demo.py` + +This script shows a minimal protocol shape for application code: + +- one host -> guest ring buffer +- one guest -> host ring buffer +- host writes one message +- guest reads it from `resource2` +- guest writes one reply +- host reads the reply from `/dev/shm/ivshmem-{sandbox_id}` + +Use this example when you want to see how to: + +- create or connect to a sandbox +- wait for the host shm file +- map the same memory on both sides +- define offsets and slot layout +- exchange messages over a small shared-memory protocol + +### `ivshmem_benchmark.py` + +This script focuses only on the host-side backend file. It does not run a +guest protocol. Use it when you want to: + +- benchmark repeated host-side mmap writes +- compare multiple temporary sandboxes +- run several host-side benchmarks in parallel + +## 6. When to Build on This Example + +These examples are intentionally small and readable. They are a starting point +for building your own shared-memory data path, for example: + +- request-reply control messages +- ring-buffered streaming between host and guest +- shared descriptors, indexes, or frame metadata +- custom zero-copy application protocols + +For production guest-side hot paths, you will usually want to move the guest +logic into a lower-overhead implementation such as C, C++, or Rust and choose +your own synchronization strategy. + +## 7. Troubleshooting + +| Symptom | Likely Cause | What to check | +| --- | --- | --- | +| `/dev/shm/ivshmem-{sandbox_id}` does not appear | The template was not built with `enableIvshmem=true` | Rebuild the template with ivshmem enabled | +| Guest cannot find vendor/device `0x1af4/0x1110` | The sandbox was created from a non-ivshmem template | Confirm the sandbox template ID | +| `resource2` is missing | The guest did not expose the ivshmem BAR as expected | Check the PCI device under `/sys/bus/pci/devices/*` | +| Ring demo times out | Host and guest are not using the same layout or offsets | Check ring offsets, slot size, and payload size assumptions | + +## See also + +- [examples/snapshot-rollback-clone](../snapshot-rollback-clone) for lifecycle-oriented Python SDK examples +- [examples/code-sandbox-quickstart](../code-sandbox-quickstart) for the basic SDK flow +- SDK source: [`sdk/python/cubesandbox`](../../sdk/python/cubesandbox) diff --git a/examples/ivshmem/ivshmem_benchmark.py b/examples/ivshmem/ivshmem_benchmark.py new file mode 100755 index 000000000..2ff1b3487 --- /dev/null +++ b/examples/ivshmem/ivshmem_benchmark.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Tencent Inc. +# SPDX-License-Identifier: Apache-2.0 + +""" +Host-side ivshmem mmap benchmark. + +This script measures host access to `/dev/shm/ivshmem-{sandbox_id}`. +It does not, by itself, prove guest communication. +""" + +from __future__ import annotations + +import argparse +import mmap +import os +import sys +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +from cubesandbox import Sandbox + + +IVSHMEM_SIZE = 1024 * 1024 +DEFAULT_SHM_WAIT_TIMEOUT = 60 + + +class IvshmemBenchmark: + def __init__(self, sandbox_id: str, ivshmem_path: str, iterations: int = 10000): + self.sandbox_id = sandbox_id + self.ivshmem_path = ivshmem_path + self.iterations = iterations + + def run_all_tests(self) -> dict: + if not os.path.exists(self.ivshmem_path): + return {"error": f"ivshmem file not found: {self.ivshmem_path}"} + + with open(self.ivshmem_path, "r+b", buffering=0) as f, mmap.mmap(f.fileno(), IVSHMEM_SIZE) as mm: + return { + "single_byte": self._test_single_byte(mm), + "block_100b": self._test_block(mm, 100, self.iterations), + "block_1kb": self._test_block(mm, 1024, self.iterations), + "block_100kb": self._test_block(mm, 100 * 1024, min(self.iterations, 1000)), + } + + def _test_single_byte(self, mm: mmap.mmap) -> dict: + start = time.perf_counter() + for i in range(self.iterations): + mm[i % IVSHMEM_SIZE] = 65 + elapsed = time.perf_counter() - start + return { + "latency_us": round((elapsed / self.iterations) * 1_000_000, 3), + "ops_per_sec": int(self.iterations / elapsed), + } + + def _test_block(self, mm: mmap.mmap, block_size: int, iterations: int) -> dict: + block = b"X" * block_size + start = time.perf_counter() + for i in range(iterations): + offset = (i * block_size) % (IVSHMEM_SIZE - block_size) + mm[offset : offset + block_size] = block + elapsed = time.perf_counter() - start + return { + "block_size": block_size, + "latency_us": round((elapsed / iterations) * 1_000_000, 3), + "throughput_mb": round((block_size * iterations) / elapsed / (1024 * 1024), 2), + } + + +def wait_for_shm_file(sandbox_id: str, timeout: int = DEFAULT_SHM_WAIT_TIMEOUT) -> str: + path = f"/dev/shm/ivshmem-{sandbox_id}" + deadline = time.time() + timeout + while time.time() < deadline: + if os.path.exists(path): + return path + time.sleep(1) + raise FileNotFoundError(f"ivshmem file not created: {path}") + + +def create_sandbox(template_id: str) -> tuple[str, str]: + sandbox = Sandbox.create(template=template_id) + try: + path = wait_for_shm_file(sandbox.sandbox_id) + return sandbox.sandbox_id, path + except Exception: + sandbox.kill() + raise + + +def cleanup_sandbox(sandbox_id: str) -> None: + try: + Sandbox.connect(sandbox_id).kill() + except Exception as exc: # noqa: BLE001 + print(f"cleanup failed for {sandbox_id}: {exc}", file=sys.stderr) + + +def run_benchmark_single(sandbox_id: str, ivshmem_path: str, iterations: int) -> tuple[str, dict]: + benchmark = IvshmemBenchmark(sandbox_id, ivshmem_path, iterations) + return sandbox_id, benchmark.run_all_tests() + + +def print_results(sandbox_id: str, results: dict) -> None: + print(f"\n{'=' * 70}") + print(f"Sandbox: {sandbox_id}") + print("=" * 70) + + if "error" in results: + print(f"ERROR: {results['error']}") + return + + print("\nSingle-byte write:") + print(f" Latency: {results['single_byte']['latency_us']} us/op") + print(f" Throughput: {results['single_byte']['ops_per_sec']:,} ops/s") + + for key, result in results.items(): + if not key.startswith("block_"): + continue + size = result["block_size"] + size_str = f"{size}B" if size < 1024 else f"{size // 1024}KB" + print(f"\n{size_str} block write:") + print(f" Latency: {result['latency_us']} us/op") + print(f" Throughput: {result['throughput_mb']} MB/s") + + +def print_summary(all_results: list[tuple[str, dict]]) -> None: + success = [r for _, r in all_results if "error" not in r] + if not success: + return + + avg_latency = sum(r["single_byte"]["latency_us"] for r in success) / len(success) + avg_1kb = sum(r["block_1kb"]["throughput_mb"] for r in success) / len(success) + avg_100kb = sum(r["block_100kb"]["throughput_mb"] for r in success) / len(success) + + print(f"\n{'=' * 70}") + print("Summary") + print("=" * 70) + print(f"Sandboxes tested: {len(all_results)}") + print(f"Successful runs: {len(success)}") + print(f"Average single-byte latency: {avg_latency:.3f} us") + print(f"Average 1KB throughput: {avg_1kb:.2f} MB/s") + print(f"Average 100KB throughput: {avg_100kb:.2f} MB/s") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Host-side ivshmem mmap benchmark") + parser.add_argument("--sandbox-id", help="Benchmark an existing sandbox") + parser.add_argument("--template", help="ivshmem-enabled template ID used to create temporary sandboxes") + parser.add_argument("--count", type=int, default=1, help="Number of sandboxes to benchmark") + parser.add_argument("--iterations", type=int, default=10000, help="Benchmark iterations") + parser.add_argument("--parallel", action="store_true", help="Run multiple benchmarks in parallel") + parser.add_argument("--cleanup", action="store_true", help="Delete temporary sandboxes after the run") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + all_results: list[tuple[str, dict]] = [] + sandboxes_to_cleanup: list[str] = [] + + try: + if args.sandbox_id: + path = f"/dev/shm/ivshmem-{args.sandbox_id}" + sandbox_id, results = run_benchmark_single(args.sandbox_id, path, args.iterations) + all_results.append((sandbox_id, results)) + print_results(sandbox_id, results) + return 0 + + if not args.template: + raise SystemExit("--template is required unless --sandbox-id is provided") + + sandboxes = [] + print(f"Creating {args.count} sandbox(es) from template {args.template}...") + for index in range(args.count): + sandbox_id, path = create_sandbox(args.template) + sandboxes.append((sandbox_id, path)) + sandboxes_to_cleanup.append(sandbox_id) + print(f" [{index + 1}/{args.count}] {sandbox_id}") + + if args.parallel and len(sandboxes) > 1: + with ThreadPoolExecutor(max_workers=len(sandboxes)) as executor: + futures = { + executor.submit(run_benchmark_single, sandbox_id, path, args.iterations): sandbox_id + for sandbox_id, path in sandboxes + } + for future in as_completed(futures): + sandbox_id, results = future.result() + all_results.append((sandbox_id, results)) + print_results(sandbox_id, results) + else: + for sandbox_id, path in sandboxes: + sandbox_id, results = run_benchmark_single(sandbox_id, path, args.iterations) + all_results.append((sandbox_id, results)) + print_results(sandbox_id, results) + + if len(all_results) > 1: + print_summary(all_results) + return 0 + finally: + if args.cleanup: + for sandbox_id in sandboxes_to_cleanup: + cleanup_sandbox(sandbox_id) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/ivshmem/ivshmem_ring_demo.py b/examples/ivshmem/ivshmem_ring_demo.py new file mode 100755 index 000000000..a888ea6b1 --- /dev/null +++ b/examples/ivshmem/ivshmem_ring_demo.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Tencent Inc. +# SPDX-License-Identifier: Apache-2.0 + +""" +Minimal ivshmem ring-buffer communication demo. + +This script demonstrates a tiny end-to-end protocol over ivshmem: +1. host writes one message into a host->guest ring +2. guest reads it from `resource2` +3. guest writes one reply into a guest->host ring +4. host reads the reply from `/dev/shm/ivshmem-{sandbox_id}` + +It is a communication example, not a performance benchmark. +The guest helper is intentionally written in Python for readability. Production +guest-side datapaths that care about latency or throughput should use a lower- +overhead implementation such as C, C++, or Rust. +""" + +from __future__ import annotations + +import argparse +import mmap +import os +import struct +import time + +from cubesandbox import Sandbox +from cubesandbox._config import Config + + +IVSHMEM_SIZE = 1024 * 1024 +RING_SLOT_COUNT = 8 +RING_SLOT_DATA_SIZE = 256 +RING_HEADER_FORMAT = " str: + path = f"/dev/shm/ivshmem-{sandbox_id}" + deadline = time.time() + timeout + while time.time() < deadline: + if os.path.exists(path): + return path + time.sleep(1) + raise FileNotFoundError(f"ivshmem file not found: {path}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Minimal ivshmem ring-buffer communication demo") + parser.add_argument("--sandbox-id", help="Use an existing sandbox") + parser.add_argument("--template", help="Create a temporary sandbox from this ivshmem-enabled template") + parser.add_argument("--message", default="hello-from-host", help="Message sent from host to guest") + parser.add_argument( + "--reply-prefix", + default="hello-from-guest", + help="Prefix used by the guest when replying", + ) + parser.add_argument("--api-url", default="http://127.0.0.1:3000") + parser.add_argument("--proxy-node-ip", default="127.0.0.1") + parser.add_argument("--proxy-port", type=int, default=80) + parser.add_argument("--sandbox-domain", default="cube.app") + parser.add_argument("--timeout", type=int, default=300) + parser.add_argument("--request-timeout", type=int, default=45) + parser.add_argument("--cleanup", action="store_true", help="Delete a temporary sandbox after the run") + return parser.parse_args() + + +def build_config(args: argparse.Namespace) -> Config: + return Config( + api_url=args.api_url, + proxy_node_ip=args.proxy_node_ip, + proxy_port=args.proxy_port, + sandbox_domain=args.sandbox_domain, + timeout=args.timeout, + request_timeout=args.request_timeout, + ) + + +def wait_cmd(sb: Sandbox, timeout: int = 60) -> None: + deadline = time.time() + timeout + last = None + while time.time() < deadline: + try: + res = sb.commands.run("echo ready", timeout=10) + if res.exit_code == 0 and "ready" in res.stdout: + return + last = (res.exit_code, res.stdout, res.stderr) + except Exception as exc: # noqa: BLE001 + last = repr(exc) + time.sleep(2) + raise RuntimeError(f"sandbox command not ready: {last}") + + +def create_or_connect_sandbox(args: argparse.Namespace, cfg: Config) -> tuple[Sandbox, bool]: + if args.sandbox_id: + return Sandbox.connect(args.sandbox_id, config=cfg), False + if not args.template: + raise SystemExit("--template is required unless --sandbox-id is provided") + return Sandbox.create(template=args.template, timeout=args.timeout, config=cfg), True + + +def reset_ring(mm: mmap.mmap, ring_offset: int) -> None: + struct.pack_into( + RING_HEADER_FORMAT, + mm, + ring_offset, + 0, + 0, + RING_SLOT_COUNT, + RING_SLOT_DATA_SIZE, + ) + + +def read_head_tail(mm: mmap.mmap, ring_offset: int) -> tuple[int, int]: + head, tail, _, _ = struct.unpack_from(RING_HEADER_FORMAT, mm, ring_offset) + return head, tail + + +def write_head(mm: mmap.mmap, ring_offset: int, value: int) -> None: + struct.pack_into(" None: + struct.pack_into(" int: + return ring_offset + RING_HEADER_SIZE + (index % RING_SLOT_COUNT) * RING_SLOT_SIZE + + +def ring_send(mm: mmap.mmap, ring_offset: int, payload: bytes, timeout: float = 5.0) -> None: + if len(payload) > RING_SLOT_DATA_SIZE: + raise ValueError(f"payload too large: {len(payload)} > {RING_SLOT_DATA_SIZE}") + + deadline = time.time() + timeout + while time.time() < deadline: + head, tail = read_head_tail(mm, ring_offset) + if tail - head < RING_SLOT_COUNT: + off = slot_offset(ring_offset, tail) + struct.pack_into(" bytes: + deadline = time.time() + timeout + while time.time() < deadline: + head, tail = read_head_tail(mm, ring_offset) + if tail > head: + off = slot_offset(ring_offset, head) + length = struct.unpack_from(" str: + return f"""import mmap +import os +import struct +import time + +IVSHMEM_SIZE = {IVSHMEM_SIZE} +RING_SLOT_COUNT = {RING_SLOT_COUNT} +RING_SLOT_DATA_SIZE = {RING_SLOT_DATA_SIZE} +RING_HEADER_FORMAT = {RING_HEADER_FORMAT!r} +RING_HEADER_SIZE = struct.calcsize(RING_HEADER_FORMAT) +RING_SLOT_SIZE = 4 + RING_SLOT_DATA_SIZE +HOST_TO_GUEST_OFFSET = {HOST_TO_GUEST_OFFSET} +GUEST_TO_HOST_OFFSET = {GUEST_TO_HOST_OFFSET} +REPLY_PREFIX = {reply_prefix!r} + + +def find_resource(): + for name in os.listdir('/sys/bus/pci/devices'): + d = f'/sys/bus/pci/devices/{{name}}' + try: + vendor = open(f'{{d}}/vendor').read().strip() + device = open(f'{{d}}/device').read().strip() + except OSError: + continue + if vendor == '0x1af4' and device == '0x1110': + return f'{{d}}/resource2' + raise RuntimeError('ivshmem PCI device not found') + + +def read_head_tail(mm, ring_offset): + head, tail, _, _ = struct.unpack_from(RING_HEADER_FORMAT, mm, ring_offset) + return head, tail + + +def write_head(mm, ring_offset, value): + struct.pack_into(' head: + off = slot_offset(ring_offset, head) + length = struct.unpack_from(' int: + args = parse_args() + cfg = build_config(args) + sandbox, temporary = create_or_connect_sandbox(args, cfg) + + try: + wait_cmd(sandbox) + shm_path = wait_for_shm_file(sandbox.sandbox_id) + + print(f"sandbox_id: {sandbox.sandbox_id}") + print(f"host shm: {shm_path}") + + with open(shm_path, "r+b", buffering=0) as f, mmap.mmap(f.fileno(), IVSHMEM_SIZE) as mm: + reset_ring(mm, HOST_TO_GUEST_OFFSET) + reset_ring(mm, GUEST_TO_HOST_OFFSET) + mm.flush() + + outbound = args.message.encode() + ring_send(mm, HOST_TO_GUEST_OFFSET, outbound) + print(f"host sent: {args.message}") + + guest = sandbox.commands.run( + "python3 - <<'PY'\n" + guest_script(args.reply_prefix) + "\nPY", + timeout=20, + ) + if guest.exit_code != 0: + raise RuntimeError(guest.stderr or guest.stdout) + + inbound = ring_recv(mm, GUEST_TO_HOST_OFFSET).decode("utf-8", "replace") + print(guest.stdout.strip()) + print(f"host recv: {inbound}") + + return 0 + finally: + if temporary and args.cleanup: + sandbox.kill() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/hypervisor/vmm/src/config.rs b/hypervisor/vmm/src/config.rs index 169751e6d..2a6872808 100644 --- a/hypervisor/vmm/src/config.rs +++ b/hypervisor/vmm/src/config.rs @@ -2123,6 +2123,10 @@ pub struct RestoreConfig { /// source_url/. #[serde(default, skip_serializing_if = "Option::is_none")] pub memory_vol_url: Option, + /// Optional ivshmem shared memory device configuration. + /// When present, the VM will have an ivshmem device for host-guest communication. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ivshmem: Option, } impl RestoreConfig { @@ -2167,6 +2171,7 @@ impl RestoreConfig { pmem: None, dirty_log, memory_vol_url, + ivshmem: None, }) } } @@ -2408,6 +2413,13 @@ impl VmConfig { } } + pub fn update_ivshmem(&mut self, ivshmem_cfg: &IvshmemConfig) { + if let Some(ivshmem) = &mut self.ivshmem { + ivshmem.path = ivshmem_cfg.path.clone(); + ivshmem.size = ivshmem_cfg.size; + } + } + pub fn update_pmem(&mut self, pmem_cfgs: &[PmemConfig]) { if let Some(pmems) = &mut self.pmem { let mut new_cfgs: HashMap = HashMap::default(); @@ -3401,6 +3413,41 @@ mod tests { Ok(()) } + #[test] + fn test_update_ivshmem_ignores_missing_snapshot_device() { + let mut vm_config = VmConfig { + ivshmem: None, + ..Default::default() + }; + let restore_ivshmem = IvshmemConfig { + path: PathBuf::from("/dev/shm/ivshmem-sandbox"), + size: 1024 * 1024, + }; + + vm_config.update_ivshmem(&restore_ivshmem); + + assert_eq!(vm_config.ivshmem, None); + } + + #[test] + fn test_update_ivshmem_updates_existing_backend() { + let mut vm_config = VmConfig { + ivshmem: Some(IvshmemConfig { + path: PathBuf::from("/dev/shm/ivshmem-template"), + size: 512 * 1024, + }), + ..Default::default() + }; + let restore_ivshmem = IvshmemConfig { + path: PathBuf::from("/dev/shm/ivshmem-sandbox"), + size: 1024 * 1024, + }; + + vm_config.update_ivshmem(&restore_ivshmem); + + assert_eq!(vm_config.ivshmem, Some(restore_ivshmem)); + } + #[test] fn test_config_validation() { let mut valid_config = VmConfig { diff --git a/hypervisor/vmm/src/lib.rs b/hypervisor/vmm/src/lib.rs index c14fed540..1a1d7bd49 100644 --- a/hypervisor/vmm/src/lib.rs +++ b/hypervisor/vmm/src/lib.rs @@ -678,6 +678,9 @@ impl Vmm { if let Some(pmems) = &restore_cfg.pmem { vm_config.update_pmem(pmems); } + if let Some(ivshmem) = &restore_cfg.ivshmem { + vm_config.update_ivshmem(ivshmem); + } vm_config.memory.dirty_log = restore_cfg.dirty_log; vm_config diff --git a/sdk/python/cubesandbox/_template.py b/sdk/python/cubesandbox/_template.py index 1e4593801..0804340cd 100644 --- a/sdk/python/cubesandbox/_template.py +++ b/sdk/python/cubesandbox/_template.py @@ -232,6 +232,7 @@ def build( dns: list[str] | None = None, allow_out: list[str] | None = None, deny_out: list[str] | None = None, + enable_ivshmem: bool | None = None, config: Config | None = None, **kwargs: Any, ) -> TemplateBuild: @@ -267,6 +268,7 @@ def build( dns: Container DNS nameservers. allow_out: Allowed outbound CIDRs for CubeVS egress policy. deny_out: Denied outbound CIDRs for CubeVS egress policy. + enable_ivshmem: Whether the template build sandbox should boot with ivshmem enabled. config: SDK config. Uses default (env-based) config if omitted. **kwargs: Extra fields forwarded verbatim to the request body. @@ -327,6 +329,8 @@ def build( payload["allowOut"] = allow_out if deny_out is not None: payload["denyOut"] = deny_out + if enable_ivshmem is not None: + payload["enableIvshmem"] = enable_ivshmem payload.update(kwargs) s = requests.Session() diff --git a/sdk/python/tests/test_sandbox.py b/sdk/python/tests/test_sandbox.py index 9489801fc..8faa36adc 100644 --- a/sdk/python/tests/test_sandbox.py +++ b/sdk/python/tests/test_sandbox.py @@ -2181,6 +2181,7 @@ def test_build_forwards_create_from_image_options(self): dns=["8.8.8.8", "1.1.1.1"], allow_out=["172.67.0.0/16"], deny_out=["10.0.0.0/8"], + enable_ivshmem=True, config=config, ) @@ -2198,6 +2199,7 @@ def test_build_forwards_create_from_image_options(self): "dns": ["8.8.8.8", "1.1.1.1"], "allowOut": ["172.67.0.0/16"], "denyOut": ["10.0.0.0/8"], + "enableIvshmem": True, }, headers={"Content-Type": "application/json"}, )