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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "third_party/mind-cluster"]
path = third_party/mind-cluster
url = https://github.com/Ascend/mind-cluster.git
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ BPF_TEST_BUILD_ARGS ?=
BPF_SOURCE_DIRS := bpf/bpfnat bpf/networkacl
BPF_C_SOURCES := $(shell find $(BPF_SOURCE_DIRS) -type f \( -name '*.c' -o -name '*.h' \) | sort)

.PHONY: all clean test storage-test e2e e2e-runtime-binaries e2e-runtime-case e2e-runtime-suite release release-binary release-cli runc-shim sandbox-logger firecracker-agent firecracker-initrd protobuf-image protos protos-local check-protos bpf-image bpf bpf-local bpf-format bpf-format-local check-bpf-format check-bpf-format-local check-bpf-generated check-bpf bpfnat-test-image bpfnat-test bpfnat-test-local networkacl-test networkacl-test-local tidy vendor fmt check-fmt vet help
.PHONY: all clean test storage-test e2e e2e-runtime-binaries e2e-runtime-case e2e-runtime-suite release release-binary release-cli runc-shim sandbox-logger firecracker-agent firecracker-initrd ascend-oci-adapter protobuf-image protos protos-local check-protos bpf-image bpf bpf-local bpf-format bpf-format-local check-bpf-format check-bpf-format-local check-bpf-generated check-bpf bpfnat-test-image bpfnat-test bpfnat-test-local networkacl-test networkacl-test-local tidy vendor fmt check-fmt vet help
.DEFAULT_GOAL := all

all: release ## build binaries
Expand Down Expand Up @@ -91,6 +91,12 @@ firecracker-agent:
@echo "Building output/firecracker-agent"
@CGO_ENABLED=0 GOOS=$(RELEASE_GOOS) GOARCH=$(RELEASE_GOARCH) $(GO) build -o output/firecracker-agent ./cmd/firecracker-agent

ascend-oci-adapter: ## build the optional external Ascend OCI adapter
@echo "Building output/ascend-oci-adapter"
@cd tools/ascend-oci-adapter && \
CGO_ENABLED=1 GOOS=$(RELEASE_GOOS) GOARCH=$(RELEASE_GOARCH) \
$(GO) build -trimpath -ldflags="-s -w" -o ../../output/ascend-oci-adapter .

# Minimal guest initrd for Firecracker microVMs: a newc cpio containing only
# /init = the statically linked firecracker-agent, gzip-compressed (speed over
# ratio; it is read once per boot into page cache).
Expand Down Expand Up @@ -251,7 +257,7 @@ fmt: ## format Go code
go fmt ./...

check-fmt: ## verify Go code is gofmt-clean
@files="$$(gofmt -l .)" || exit $$?; \
@files="$$(gofmt -l $$(git ls-files '*.go'))" || exit $$?; \
test -z "$$files" || { printf '%s\n' "$$files" >&2; exit 1; }

vet: ## run go vet
Expand Down
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ The `sbox` binary is an administrative CLI for managing sandboxes.

### NVIDIA GPU sandboxes

GPU support is experimental and currently uses gVisor runsc with nvproxy. The
scheduler passes concrete node-local device IDs through
GPU support is experimental. It supports gVisor runsc through nvproxy and
native runc through the NVIDIA OCI prestart hook. The scheduler passes concrete
node-local device IDs through
`StartRequest.xpu_allocations`; sandboxd resolves them to NVIDIA UUIDs and
maintains a local exclusive lease:

```bash
sbox start \
--runtime runc \
--rootfs /path/to/directory-rootfs \
--xpu-allocation gpu:0,2 \
/bin/sleep 300
Expand Down Expand Up @@ -103,6 +105,17 @@ make networkacl-test
make bpfnat-test
```

The Ascend OCI adapter is an optional external process and is not linked into
the default sandboxd binary. Initialize its vendor dependency and build it with:

```bash
git submodule update --init third_party/mind-cluster
make ascend-oci-adapter
```

The default read-only driver mount profile is
`configs/ascend/mounts.json`; deployments install it alongside the adapter.

`networkacl-test` runs one backend-neutral conformance suite against native
iptables and TC eBPF enforcement in isolated network namespaces. It covers
allow and deny precedence, exact and wildcard peers, peer and sandbox ports,
Expand Down Expand Up @@ -165,11 +178,12 @@ tools/ pinned protobuf code-generation image
KVM continue to support gVisor. Firecracker additionally requires a compatible
guest kernel/initrd, an EROFS root image, and the ext4 image tool. Nodes that
enable OCI/Nydus rootfs materialization also require `mkfs.erofs`.
- NVIDIA GPU sandboxes require runsc, a directory/lisafs-backed rootfs,
`nvidia-container-cli`, accessible NVIDIA devices and userspace driver
libraries, and a host driver supported by the pinned runsc nvproxy. Kata,
Firecracker, runc, MIG, fractional GPUs, and regular-file/EROFS rootfs are
not supported.
- NVIDIA GPU sandboxes require `nvidia-container-cli`, the executable NVIDIA
OCI runtime hook, accessible NVIDIA devices and userspace driver libraries.
Runsc additionally requires a host driver supported by its pinned nvproxy
compatibility gate and a directory/lisafs-backed rootfs. Runc accepts the
normal directory or EROFS rootfs through its writable host overlay. Kata,
Firecracker, MIG, and fractional GPUs are not supported.
- sandboxd detects the local cgroup mode at startup. Legacy and hybrid hosts use cgroup v1; unified hosts use cgroup v2. The gRPC API and resource-cache behavior are identical in both modes.
- `[plugin.resource].disable_cgroup = true` enables an experimental/debug
compatibility mode for environments where sandboxd cannot write the
Expand Down
2 changes: 1 addition & 1 deletion api/runtime/v1/sandbox-api.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/runtime/v1/sandbox-api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ message Mount {
// XpuAllocation describes concrete accelerator devices assigned by the
// scheduler on the selected node.
message XpuAllocation {
// Type is the normalized accelerator type, such as gpu.
// Type is the normalized accelerator type, such as gpu or npu.
string type = 1;
// DeviceIDs contains node-local physical device IDs selected by the scheduler.
repeated uint32 device_ids = 2;
Expand Down
14 changes: 14 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,23 @@ type PluginConfig struct {

NodeResourceConfig `toml:"node_resource" json:"nodeResource"`

XPUConfig `toml:"xpu" json:"xpu"`

ImageManagerConfig `toml:"image" json:"image"`
}

// XPUConfig contains optional vendor accelerator providers.
type XPUConfig struct {
Ascend AscendConfig `toml:"ascend" json:"ascend"`
}

// AscendConfig enables the external Ascend OCI adapter for runc sandboxes.
type AscendConfig struct {
Enabled bool `toml:"enabled" json:"enabled"`
Adapter string `toml:"adapter" json:"adapter"`
MountProfile string `toml:"mount_profile" json:"mountProfile"`
}

// ImageManagerConfig configures image and mount lifecycle management.
type ImageManagerConfig struct {
ImageManagerRoot string `toml:"root" json:"root"`
Expand Down
14 changes: 14 additions & 0 deletions configs/ascend/mounts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"default": [
{
"path": [
"/usr/local/Ascend/driver/lib64",
"/usr/local/Ascend/driver/include",
"/usr/local/dcmi",
"/usr/local/bin/npu-smi",
"/usr/sbin/dmidecode",
"/var/queue_schedule"
]
}
]
}
7 changes: 7 additions & 0 deletions configs/sandboxd.toml
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,10 @@ cgroup_memory_limit = "0"
# [plugin.node_resource]
# provider = "kubernetes" # or "cgroup"
# sock_path = "/run/sandboxd/resource.sock"

# Ascend is optional and fail-closed. Enabling it requires the runc runtime,
# a root-owned adapter, and a versioned read-only mount profile.
# [plugin.xpu.ascend]
# enabled = true
# adapter = "/usr/local/bin/ascend-oci-adapter"
# mount_profile = "/etc/akernel/ascend/mounts.json"
8 changes: 7 additions & 1 deletion doc/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ binaries, boot artifacts, and host prerequisites pass validation.
| Published-port DNAT | Supported | Supported | Supported | Supported |
| Writable-layer quota | Supported | Not supported | Not supported | Supported |
| Checkpoint and restore | Supported (systrap and KVM) | Not supported | Not supported | Supported |
| NVIDIA GPU | Experimental nvproxy support | Not supported | Not supported | Not supported |
| NVIDIA GPU | Experimental nvproxy support | Experimental NVIDIA OCI-hook support | Not supported | Not supported |
| Cgroup-disabled mode | Experimental | Not supported | Not supported | Not supported |
| KVM | Optional execution platform; not exposed to the sandbox | Optional guest exposure | Required by the runtime | Required by the runtime; nested KVM is not exposed |

See [Checkpoint and restore](checkpoint-restore.md) for the API design,
artifact ownership, failure semantics, and compatibility requirements.

The NVIDIA provider owns a single physical-device inventory and exclusive
lease table shared by runsc and runc. Runsc remains subject to its nvproxy
driver-compatibility gate, while an unsupported nvproxy driver does not disable
runc GPU support. Both runtimes consume the provider's UUIDs and OCI environment
through the NVIDIA prestart hook.

## Selection and configuration

A start request selects a runtime by name. Each adapter must have an entry
Expand Down
23 changes: 18 additions & 5 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@ func NewSandboxService(root, configPath string) (result SandboxService, retErr e
}
xpuMgr := xpumanager.New(
cfg.RuntimeConfig.RuntimeBinary[config.RuntimeNameRunsc],
cfg.RuntimeConfig.RuntimeBinary[config.RuntimeNameRunc] != "",
sandboxRoot,
cfg.XPUConfig.Ascend,
)

// The optional node-resource module comes up first so its external resource
Expand Down Expand Up @@ -1238,10 +1240,21 @@ func (h *sandboxService) Start(ctx context.Context, request *runtime.StartReques
return &runtime.StartResponse{Code: -1, Message: err.Error()},
errord.ToGRPC(errord.ErrInvalidArgument)
}
if len(startReq.XpuAllocations) > 0 && startReq.Runtime != config.RuntimeNameRunsc {
err := fmt.Errorf("XPU allocations require runtime %q", config.RuntimeNameRunsc)
return &runtime.StartResponse{Code: -1, Message: err.Error()},
errord.ToGRPC(errord.ErrInvalidArgument)
if len(startReq.XpuAllocations) > 0 {
if startReq.Runtime != config.RuntimeNameRunsc && startReq.Runtime != config.RuntimeNameRunc {
err := fmt.Errorf("XPU allocations require runtime %q or %q", config.RuntimeNameRunsc, config.RuntimeNameRunc)
return &runtime.StartResponse{Code: -1, Message: err.Error()},
errord.ToGRPC(errord.ErrInvalidArgument)
}
if h.xpuMgr == nil {
err := errors.New("XPU manager is not configured")
return &runtime.StartResponse{Code: -1, Message: err.Error()},
errord.ToGRPC(errord.ErrFailedPrecondition)
}
if err := h.xpuMgr.ValidateRuntime(startReq.Runtime, startReq.XpuAllocations); err != nil {
return &runtime.StartResponse{Code: -1, Message: err.Error()},
errord.ToGRPC(errord.ErrInvalidArgument)
}
}
if startReq.WritableLayerLimitBytes > 0 {
if startReq.Runtime != config.RuntimeNameRunsc &&
Expand Down Expand Up @@ -1455,7 +1468,7 @@ func (h *sandboxService) Start(ctx context.Context, request *runtime.StartReques
return &runtime.StartResponse{Code: -1, Message: err.Error()},
errord.ToGRPC(errord.ErrFailedPrecondition)
}
specUpdates, err = h.xpuMgr.Acquire(sandboxID, startReq.XpuAllocations)
specUpdates, err = h.xpuMgr.Acquire(sandboxID, startReq.Runtime, startReq.XpuAllocations)
if err != nil {
return &runtime.StartResponse{
Code: -1,
Expand Down
14 changes: 13 additions & 1 deletion internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,6 @@ func TestStartRejectsFirecrackerOCIImageBeforeFilesystemPrepare(t *testing.T) {

func TestStartRejectsXPUForUnsupportedRuntimes(t *testing.T) {
for _, runtimeName := range []string{
config.RuntimeNameRunc,
config.RuntimeNameKata,
config.RuntimeNameFirecracker,
} {
Expand All @@ -303,6 +302,19 @@ func TestStartRejectsXPUForUnsupportedRuntimes(t *testing.T) {
}
}

func TestStartRejectsXPUWhenManagerIsUnavailable(t *testing.T) {
s := newTestService(t, map[string]svc.Handler{
config.RuntimeNameRunc: svc.NewFakeRuntimeHandler(),
})
response, err := s.Start(context.Background(), &runtime.StartRequest{
Runtime: config.RuntimeNameRunc,
Rootfs: &runtime.RootfsConfig{},
XpuAllocations: []*runtime.XpuAllocation{{Type: "npu"}},
})
assert.Equal(t, codes.FailedPrecondition, status.Code(err))
assert.Contains(t, response.Message, "XPU manager is not configured")
}

func TestStartRejectsEnableKVMForRunsc(t *testing.T) {
s := newTestService(t, map[string]svc.Handler{
config.RuntimeNameRunsc: svc.NewFakeRuntimeHandler(),
Expand Down
10 changes: 7 additions & 3 deletions pkg/runtime/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,13 @@ type StartConfig struct {
// this boundary so vendor-specific discovery and authorization do not leak
// into the runsc client.
type SpecUpdates struct {
Envs []*runtime.KeyValue
Prestart []Hook
Annotations map[string]string
Envs []*runtime.KeyValue
Prestart []Hook
Annotations map[string]string
AdditionalCapabilities []string
LinuxDevices []LinuxDevice
DeviceCgroupRules []LinuxDeviceCgroup
Mounts []Mount
// RequiresHostWritableRootfs requests a private writable rootfs view
// before provider hooks execute. It is separate from the writable layer
// visible to workloads after the sandbox starts.
Expand Down
101 changes: 101 additions & 0 deletions pkg/runtime/oci_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ func (r *BundleLoader) GenerateOci(options OciLoadOptions) (string, *Spec, error
ociSpec.Hooks.Prestart = append(ociSpec.Hooks.Prestart, updates.Prestart...)
}
ociSpec.Annotations = combineAnnotations(ociSpec.Annotations, updates.Annotations)
if err := applyProviderCapabilities(ociSpec, updates.AdditionalCapabilities); err != nil {
return "", ociSpec, err
}
if err := applyProviderDevicesAndMounts(ociSpec, updates); err != nil {
return "", ociSpec, err
}
}

ociFile := filepath.Join(bundleDir, config.SandboxSpecFile)
Expand All @@ -211,6 +217,101 @@ func (r *BundleLoader) GenerateOci(options OciLoadOptions) (string, *Spec, error
return bundleDir, ociSpec, os.WriteFile(ociFile, buf, 0644)
}

func applyProviderCapabilities(spec *Spec, capabilities []string) error {
if len(capabilities) == 0 {
return nil
}
if spec.Process == nil || spec.Process.Capabilities == nil {
return errors.New("provider capabilities require a process capability set")
}
for _, capability := range capabilities {
// Ascend 310P management calls traverse driver-owned paths that require
// DAC override. Keep this provider boundary fail-closed instead of
// accepting arbitrary capability names from device implementations.
if capability != "CAP_DAC_OVERRIDE" {
return fmt.Errorf("provider capability %q is not allowed", capability)
}
sets := []*[]string{
&spec.Process.Capabilities.Bounding,
&spec.Process.Capabilities.Effective,
&spec.Process.Capabilities.Inheritable,
&spec.Process.Capabilities.Permitted,
}
for _, set := range sets {
if !containsCapability(*set, capability) {
*set = append(*set, capability)
}
}
}
return nil
}

func containsCapability(values []string, expected string) bool {
for _, value := range values {
if value == expected {
return true
}
}
return false
}

func applyProviderDevicesAndMounts(spec *Spec, updates *SpecUpdates) error {
if spec.Linux == nil {
spec.Linux = &Linux{}
}
if spec.Linux.Resources == nil {
spec.Linux.Resources = &LinuxResources{}
}
devicePaths := make(map[string]struct{}, len(spec.Linux.Devices)+len(updates.LinuxDevices))
for _, device := range spec.Linux.Devices {
devicePaths[device.Path] = struct{}{}
}
for _, device := range updates.LinuxDevices {
if device.Path == "" || !filepath.IsAbs(device.Path) || device.Type != "c" ||
device.Major < 0 || device.Minor < 0 {
return fmt.Errorf("invalid provider device %q", device.Path)
}
if _, duplicate := devicePaths[device.Path]; duplicate {
return fmt.Errorf("provider device conflicts at %s", device.Path)
}
devicePaths[device.Path] = struct{}{}
spec.Linux.Devices = append(spec.Linux.Devices, device)
}
for _, rule := range updates.DeviceCgroupRules {
if !rule.Allow || rule.Type != "c" || rule.Major == nil || rule.Minor == nil ||
*rule.Major < 0 || *rule.Minor < 0 || rule.Access != "rwm" {
return errors.New("invalid provider device cgroup rule")
}
spec.Linux.Resources.Devices = append(spec.Linux.Resources.Devices, rule)
}
mountTargets := make(map[string]struct{}, len(spec.Mounts)+len(updates.Mounts))
for _, mount := range spec.Mounts {
mountTargets[filepath.Clean(mount.Destination)] = struct{}{}
}
for _, mount := range updates.Mounts {
destination := filepath.Clean(mount.Destination)
if !filepath.IsAbs(destination) || destination == "/" || !filepath.IsAbs(mount.Source) ||
mount.Type != "bind" || !containsMountOption(mount.Options, "ro") {
return fmt.Errorf("invalid provider mount %s -> %s", mount.Source, mount.Destination)
}
if _, conflict := mountTargets[destination]; conflict {
return fmt.Errorf("provider mount conflicts at %s", destination)
}
mountTargets[destination] = struct{}{}
spec.Mounts = append(spec.Mounts, mount)
}
return nil
}

func containsMountOption(options []string, expected string) bool {
for _, option := range options {
if option == expected {
return true
}
}
return false
}

func setNetworkNamespace(linux *Linux, path string) {
for index := range linux.Namespaces {
if linux.Namespaces[index].Type == NetworkNamespace {
Expand Down
Loading