From 28bdcc03d3d4c4bfc0976dc84cca3af5cabef35b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:54:50 +0000 Subject: [PATCH 1/2] Try parsing rootfs from container state JSON Signed-off-by: Henry Wang (cherry picked from commit 7df27d08ebe6de4215bcf85dfb54166f8bcf69f0) --- internal/oci/state.go | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/internal/oci/state.go b/internal/oci/state.go index 411bb306c..9e6740f0a 100644 --- a/internal/oci/state.go +++ b/internal/oci/state.go @@ -27,7 +27,14 @@ import ( ) // State stores an OCI container state. This includes the spec path and the environment -type State specs.State +type State struct { + specs.State + // Root is a non-standard extension included in the container state JSON by some + // OCI runtimes (e.g., crun). When present it provides the rootfs path directly, + // avoiding the need to open config.json — which may be permission-denied when + // running with user namespaces such as --userns=nomap (issue #648). + Root string `json:"root,omitempty"` +} // LoadContainerState loads the container state from the specified filename. If the filename is empty or '-' the state is loaded from STDIN func LoadContainerState(filename string) (*State, error) { @@ -56,17 +63,26 @@ func ReadContainerState(reader io.Reader) (*State, error) { return &s, nil } -// GetContainerRoot returns the root for the container from the associated spec. If the spec is not yet loaded, it is -// loaded and cached. -func (s *State) GetContainerRoot() (string, error) { +func (s *State) getRoot() (string, error) { + if s.Root != "" { + return s.Root, nil + } spec, err := s.loadMinimalSpec() if err != nil { return "", err } - - var containerRoot string if spec.Root != nil { - containerRoot = spec.Root.Path + return spec.Root.Path, nil + } + return "", nil +} + +// GetContainerRoot returns the root for the container from the associated spec. If the spec is not yet loaded, it is +// loaded and cached. +func (s *State) GetContainerRoot() (string, error) { + containerRoot, err := s.getRoot() + if err != nil { + return "", err } if filepath.IsAbs(containerRoot) { From 90a8e9506dac89c90c223c60537424db99e13f74 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:54:50 +0000 Subject: [PATCH 2/2] Add unit tests for oci state processing Signed-off-by: Henry Wang (cherry picked from commit 4a614fe65259ab8895b29701e864c4dda654a932) --- internal/oci/state_test.go | 258 +++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 internal/oci/state_test.go diff --git a/internal/oci/state_test.go b/internal/oci/state_test.go new file mode 100644 index 000000000..9a52d0008 --- /dev/null +++ b/internal/oci/state_test.go @@ -0,0 +1,258 @@ +/** +# Copyright (c), NVIDIA CORPORATION. All rights reserved. +# +# 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 oci + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/require" +) + +func TestReadContainerState(t *testing.T) { + testCases := []struct { + description string + contents string + isError bool + expected *State + }{ + { + description: "invalid json returns error", + contents: "not json", + isError: true, + }, + { + description: "empty object decodes to empty state", + contents: "{}", + expected: &State{}, + }, + { + description: "standard fields are decoded", + contents: `{"bundle": "/foo/bar"}`, + expected: &State{ + State: specs.State{ + Bundle: "/foo/bar", + }, + }, + }, + { + description: "non-standard root extension is decoded", + contents: `{"bundle": "/foo/bar", "root": "/foo/bar/rootfs"}`, + expected: &State{ + State: specs.State{ + Bundle: "/foo/bar", + }, + Root: "/foo/bar/rootfs", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + s, err := ReadContainerState(bytes.NewBufferString(tc.contents)) + + if tc.isError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.EqualValues(t, tc.expected, s) + }) + } +} + +func TestLoadContainerState(t *testing.T) { + testCases := []struct { + description string + useStdin bool + useMissingFile bool + stateJSON string + isError bool + expectedBundle string + }{ + { + description: "reads from stdin when filename is empty", + useStdin: true, + stateJSON: `{"bundle": "/from/stdin"}`, + expectedBundle: "/from/stdin", + }, + { + description: "reads from a file when filename is specified", + stateJSON: `{"bundle": "/from/file"}`, + expectedBundle: "/from/file", + }, + { + description: "returns an error when the file does not exist", + useMissingFile: true, + isError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + var filename string + switch { + case tc.useStdin: + oldStdin := os.Stdin + r, w, err := os.Pipe() + require.NoError(t, err) + _, err = w.WriteString(tc.stateJSON) + require.NoError(t, err) + require.NoError(t, w.Close()) + os.Stdin = r + t.Cleanup(func() { os.Stdin = oldStdin }) + case tc.useMissingFile: + filename = filepath.Join(t.TempDir(), "does-not-exist.json") + default: + filename = filepath.Join(t.TempDir(), "state.json") + require.NoError(t, os.WriteFile(filename, []byte(tc.stateJSON), 0600)) + } + + s, err := LoadContainerState(filename) + + if tc.isError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectedBundle, s.Bundle) + }) + } +} + +func TestGetContainerRoot(t *testing.T) { + testCases := []struct { + description string + root string + specJSON string + writeSpec bool + isError bool + expectedRoot func(bundle string) string + }{ + { + description: "absolute root extension is returned as-is", + root: "/absolute/rootfs", + expectedRoot: func(bundle string) string { + return "/absolute/rootfs" + }, + }, + { + description: "relative root extension is joined with the bundle", + root: "rootfs", + expectedRoot: func(bundle string) string { + return filepath.Join(bundle, "rootfs") + }, + }, + { + description: "falls back to the spec file when root extension is not set", + writeSpec: true, + specJSON: `{"root": {"path": "rootfs"}}`, + expectedRoot: func(bundle string) string { + return filepath.Join(bundle, "rootfs") + }, + }, + { + description: "returns an empty string when neither root extension nor spec root are set", + writeSpec: true, + specJSON: `{}`, + expectedRoot: func(bundle string) string { + return bundle + }, + }, + { + description: "returns an error when the spec file cannot be loaded", + writeSpec: false, + isError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + dir := t.TempDir() + if tc.writeSpec { + require.NoError(t, os.WriteFile(GetSpecFilePath(dir), []byte(tc.specJSON), 0600)) + } + s := &State{ + State: specs.State{Bundle: dir}, + Root: tc.root, + } + + root, err := s.GetContainerRoot() + + if tc.isError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectedRoot(dir), root) + }) + } +} + +func TestLoadMinimalSpec(t *testing.T) { + testCases := []struct { + description string + specJSON string + writeSpec bool + isError bool + expectedRoot string + }{ + { + description: "returns an error when the spec file does not exist", + writeSpec: false, + isError: true, + }, + { + description: "returns an error for invalid json", + writeSpec: true, + specJSON: "not json", + isError: true, + }, + { + description: "decodes the root field", + writeSpec: true, + specJSON: `{"root": {"path": "/some/rootfs"}}`, + expectedRoot: "/some/rootfs", + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + dir := t.TempDir() + if tc.writeSpec { + require.NoError(t, os.WriteFile(GetSpecFilePath(dir), []byte(tc.specJSON), 0600)) + } + s := &State{State: specs.State{Bundle: dir}} + + ms, err := s.loadMinimalSpec() + + if tc.isError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectedRoot, ms.Root.Path) + }) + } +}