// Copyright 2026 The gVisor Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package sandbox import ( "encoding/json" "fmt" "os" "path/filepath" specs "github.com/opencontainers/runtime-spec/specs-go" ) // BundleConfig holds configuration for creating an OCI bundle. type BundleConfig struct { ID string RuntimeDir string EnableNetworking bool Mounts []Mount Env []string Annotations map[string]string WorkingDir string Hostname string } // NewBundle creates a temporary OCI bundle on the fly with the given configuration. func NewBundle(cfg BundleConfig) (string, error) { // Create a bundle directory for the sandbox. bundleDir := filepath.Join(cfg.RuntimeDir, cfg.ID) rootfsDir := filepath.Join(bundleDir, "rootfs") if err := os.MkdirAll(rootfsDir, 0o755); err != nil { return "", fmt.Errorf("failed to create bundle directories: %w", err) } // Define the OCI Specification programmatically. namespaces := []specs.LinuxNamespace{ {Type: specs.PIDNamespace}, {Type: specs.MountNamespace}, {Type: specs.UTSNamespace}, {Type: specs.IPCNamespace}, } if os.Geteuid() != 0 { namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.UserNamespace}) } if cfg.EnableNetworking { namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.NetworkNamespace}) } spec := &specs.Spec{ Version: "1.0.0", Annotations: cfg.Annotations, Root: &specs.Root{ Path: "rootfs", Readonly: false, }, Process: &specs.Process{ Terminal: false, User: specs.User{UID: 0, GID: 0}, // Keeps the sandbox alive in the background. Args: []string{"sleep", "infinity"}, Cwd: cfg.WorkingDir, }, Mounts: []specs.Mount{ // Mandatory Linux API Filesystems {Destination: "/proc", Type: "proc", Source: "proc"}, {Destination: "/dev", Type: "tmpfs", Source: "tmpfs"}, }, // enable basic namespaces for gVisor. Linux: &specs.Linux{ Namespaces: namespaces, }, Hostname: cfg.Hostname, } baseEnv := []string{"PATH=/bin:/usr/bin:/usr/local/bin"} spec.Process.Env = append(baseEnv, cfg.Env...) if os.Geteuid() != 0 { spec.Linux.UIDMappings = []specs.LinuxIDMapping{ {ContainerID: 0, HostID: uint32(os.Geteuid()), Size: 1}, } spec.Linux.GIDMappings = []specs.LinuxIDMapping{ {ContainerID: 0, HostID: uint32(os.Getegid()), Size: 1}, } } // Map host binaries & libraries as readonly. The binaries will be // executed in gVisor sandbox, not on the host. for _, p := range []string{"/bin", "/usr", "/lib", "/lib64", "/etc/alternatives"} { if _, err := os.Stat(p); err == nil { opts := []string{"rbind", "ro", "nosuid", "nodev"} if p == "/etc/alternatives" { opts = []string{"rbind", "ro"} } spec.Mounts = append(spec.Mounts, specs.Mount{ Destination: p, Type: "bind", Source: p, Options: opts, }) } } // Add custom mounts. Custom mounts overriding default host mounts create duplicate OCI // entries. The later entry overrides the earlier one, as expected by OCI specs. for _, m := range cfg.Mounts { switch m.Type { case MountTypeBind: opts := []string{"rbind"} if m.ReadOnly { opts = append(opts, "ro") } else { opts = append(opts, "rw") } spec.Mounts = append(spec.Mounts, specs.Mount{ Destination: filepath.Clean(m.Destination), Source: filepath.Clean(m.Source), Type: "bind", Options: opts, }) case MountTypeTmpfs: spec.Mounts = append(spec.Mounts, specs.Mount{ Destination: filepath.Clean(m.Destination), Source: "tmpfs", Type: "tmpfs", }) case MountTypeProc: spec.Mounts = append(spec.Mounts, specs.Mount{ Destination: filepath.Clean(m.Destination), Source: "proc", Type: "proc", }) } } // Write the spec to config.json configPath := filepath.Join(bundleDir, "config.json") configFile, err := os.Create(configPath) if err != nil { return "", fmt.Errorf("failed to create config.json: %w", err) } defer configFile.Close() encoder := json.NewEncoder(configFile) encoder.SetIndent("", " ") if err := encoder.Encode(spec); err != nil { return "", fmt.Errorf("failed to encode config.json: %w", err) } return bundleDir, nil }