From 3ad2db2efa2fc084548c145dcebf1766fe1db187 Mon Sep 17 00:00:00 2001 From: Utkarsh-sharma47 Date: Wed, 5 Aug 2026 05:05:43 +0530 Subject: [PATCH] fix(unikontainers): reject OCI specs missing root section --- pkg/unikontainers/unikontainers.go | 21 ++++- pkg/unikontainers/unikontainers_test.go | 107 ++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 pkg/unikontainers/unikontainers_test.go diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index b218c4cea..c68c72259 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -63,6 +63,19 @@ type Unikontainer struct { Conn *net.UnixConn } +// requireLinuxAndRoot ensures the OCI spec has the sections urunc later +// dereferences. Matching the shim's Root check keeps create/start validation +// consistent across entry points. +func requireLinuxAndRoot(spec *specs.Spec) error { + if spec == nil || spec.Linux == nil { + return fmt.Errorf("invalid OCI spec: linux section is required") + } + if spec.Root == nil { + return fmt.Errorf("invalid OCI spec: root section is required") + } + return nil +} + // New parses the bundle and creates a new Unikontainer object func New(bundlePath string, containerID string, rootDir string, cfg *UruncConfig) (*Unikontainer, error) { spec, err := loadSpec(bundlePath) @@ -70,8 +83,8 @@ func New(bundlePath string, containerID string, rootDir string, cfg *UruncConfig return nil, err } - if spec == nil || spec.Linux == nil { - return nil, fmt.Errorf("invalid OCI spec: linux section is required") + if err := requireLinuxAndRoot(spec); err != nil { + return nil, err } containerName := spec.Annotations["io.kubernetes.cri.container-name"] @@ -129,8 +142,8 @@ func Get(containerID string, rootDir string) (*Unikontainer, error) { if err != nil { return nil, err } - if spec == nil || spec.Linux == nil { - return nil, fmt.Errorf("invalid OCI spec: linux section is required") + if err := requireLinuxAndRoot(spec); err != nil { + return nil, err } u.BaseDir = containerDir u.RootDir = rootDir diff --git a/pkg/unikontainers/unikontainers_test.go b/pkg/unikontainers/unikontainers_test.go new file mode 100644 index 000000000..a148edd79 --- /dev/null +++ b/pkg/unikontainers/unikontainers_test.go @@ -0,0 +1,107 @@ +// Copyright (c) 2023-2026, Nubificus LTD +// +// 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 +// +// http://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 unikontainers + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" +) + +func writeBundleConfig(t *testing.T, bundleDir string, configJSON string) { + t.Helper() + err := os.WriteFile(filepath.Join(bundleDir, configFilename), []byte(configJSON), 0o600) + assert.NoError(t, err) +} + +func TestNewRejectsMissingRoot(t *testing.T) { + cfg := UruncConfigFromMap(map[string]string{}) + + t.Run("linux present without root returns root validation error", func(t *testing.T) { + t.Parallel() + bundleDir := t.TempDir() + writeBundleConfig(t, bundleDir, `{"linux":{}}`) + + u, err := New(bundleDir, "test-container", t.TempDir(), cfg) + assert.Nil(t, u) + assert.Error(t, err) + assert.EqualError(t, err, "invalid OCI spec: root section is required") + }) + + t.Run("valid annotations cannot bypass missing root validation", func(t *testing.T) { + t.Parallel() + bundleDir := t.TempDir() + writeBundleConfig(t, bundleDir, `{ + "linux": {}, + "annotations": { + "com.urunc.unikernel.unikernelType": "unikraft", + "com.urunc.unikernel.hypervisor": "qemu", + "com.urunc.unikernel.binary": "/kernel" + } + }`) + + u, err := New(bundleDir, "test-container", t.TempDir(), cfg) + assert.Nil(t, u) + assert.Error(t, err) + assert.EqualError(t, err, "invalid OCI spec: root section is required") + }) + + t.Run("missing linux still returns linux validation error", func(t *testing.T) { + t.Parallel() + bundleDir := t.TempDir() + writeBundleConfig(t, bundleDir, `{"root":{"path":"rootfs"}}`) + + u, err := New(bundleDir, "test-container", t.TempDir(), cfg) + assert.Nil(t, u) + assert.Error(t, err) + assert.EqualError(t, err, "invalid OCI spec: linux section is required") + }) +} + +func TestGetRejectsMissingRoot(t *testing.T) { + t.Parallel() + + bundleDir := t.TempDir() + writeBundleConfig(t, bundleDir, `{"linux":{}}`) + + rootDir := t.TempDir() + containerID := "test-container" + containerDir := filepath.Join(rootDir, containerID) + err := os.MkdirAll(containerDir, 0o755) + assert.NoError(t, err) + + state := specs.State{ + Version: "1.0.0", + ID: containerID, + Status: "created", + Bundle: bundleDir, + Annotations: map[string]string{ + annotType: "unikraft", + }, + } + stateData, err := json.Marshal(state) + assert.NoError(t, err) + err = os.WriteFile(filepath.Join(containerDir, stateFilename), stateData, 0o600) + assert.NoError(t, err) + + u, err := Get(containerID, rootDir) + assert.Nil(t, u) + assert.Error(t, err) + assert.EqualError(t, err, "invalid OCI spec: root section is required") +}