Skip to content

Commit 61d2b03

Browse files
authored
Merge pull request #1491 from elezar/strict-oci-spec
Default to strict decoding of OCI runtime spec
2 parents f845983 + 1bea5f3 commit 61d2b03

File tree

8 files changed

+105
-30
lines changed

8 files changed

+105
-30
lines changed

internal/config/features.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ type features struct {
2525
// If this feature flag is not set to 'true' only host-rooted config paths
2626
// (i.e. paths starting with an '@' are considered valid)
2727
AllowLDConfigFromContainer *feature `toml:"allow-ldconfig-from-container,omitempty"`
28+
// AllowUnknownOCISpecFields allows the nvidia-container-runtime to ignore
29+
// unknown fields when loading the config (OCI spec) associated with a
30+
// container.
31+
// If this is enabled, these fields are silently dropped.
32+
AllowUnknownOCISpecFields *feature `toml:"allow-unknown-oci-spec-fields,omitempty"`
2833
// DisableCUDACompatLibHook, when enabled skips the injection of a specific
2934
// hook to process CUDA compatibility libraries.
3035
//

internal/oci/options.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
**/
17+
18+
package oci
19+
20+
import "github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
21+
22+
type options struct {
23+
logger logger.Interface
24+
allowUnkownFields bool
25+
}
26+
27+
type Option func(*options)
28+
29+
func WithLogger(logger logger.Interface) Option {
30+
return func(o *options) {
31+
o.logger = logger
32+
}
33+
}
34+
35+
func WithAllowUnknownFields(allowUnknownFields bool) Option {
36+
return func(o *options) {
37+
o.allowUnkownFields = allowUnknownFields
38+
}
39+
}

internal/oci/spec.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import (
2020
"fmt"
2121

2222
"github.com/opencontainers/runtime-spec/specs-go"
23-
24-
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
2523
)
2624

2725
// SpecModifier defines an interface for modifying a (raw) OCI spec
@@ -49,17 +47,24 @@ type Spec interface {
4947

5048
// NewSpec creates fileSpec based on the command line arguments passed to the
5149
// application using the specified logger.
52-
func NewSpec(logger logger.Interface, args []string) (Spec, error) {
50+
func NewSpec(args []string, opts ...Option) (Spec, error) {
51+
o := &options{
52+
allowUnkownFields: false,
53+
}
54+
for _, opt := range opts {
55+
opt(o)
56+
}
57+
5358
bundleDir, err := GetBundleDir(args)
5459
if err != nil {
5560
return nil, fmt.Errorf("error getting bundle directory: %v", err)
5661
}
57-
logger.Debugf("Using bundle directory: %v", bundleDir)
62+
o.logger.Debugf("Using bundle directory: %v", bundleDir)
5863

5964
ociSpecPath := GetSpecFilePath(bundleDir)
60-
logger.Infof("Using OCI specification file path: %v", ociSpecPath)
65+
o.logger.Infof("Using OCI specification file path: %v", ociSpecPath)
6166

62-
ociSpec := NewFileSpec(ociSpecPath)
67+
ociSpec := NewFileSpec(ociSpecPath, !o.allowUnkownFields)
6368

6469
return ociSpec, nil
6570
}

internal/oci/spec_file.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,28 @@ import (
2828
type fileSpec struct {
2929
memorySpec
3030
path string
31+
loader
3132
}
3233

34+
type loader bool
35+
36+
const (
37+
strictLoader = loader(true)
38+
)
39+
3340
var _ Spec = (*fileSpec)(nil)
3441

3542
// NewFileSpec creates an object that encapsulates a file-backed OCI spec.
3643
// This can be used to read from the file, modify the spec, and write to the
3744
// same file.
38-
func NewFileSpec(filepath string) Spec {
45+
func NewFileSpec(filepath string, isStrict bool) Spec {
46+
var loader loader
47+
if isStrict {
48+
loader = strictLoader
49+
}
3950
oci := fileSpec{
40-
path: filepath,
51+
path: filepath,
52+
loader: loader,
4153
}
4254

4355
return &oci
@@ -52,7 +64,7 @@ func (s *fileSpec) Load() (*specs.Spec, error) {
5264
}
5365
defer specFile.Close()
5466

55-
spec, err := LoadFrom(specFile)
67+
spec, err := s.loadFrom(specFile)
5668
if err != nil {
5769
return nil, fmt.Errorf("error loading OCI specification from file: %v", err)
5870
}
@@ -61,8 +73,11 @@ func (s *fileSpec) Load() (*specs.Spec, error) {
6173
}
6274

6375
// LoadFrom reads the contents of the OCI spec from the specified io.Reader.
64-
func LoadFrom(reader io.Reader) (*specs.Spec, error) {
76+
func (isStrict loader) loadFrom(reader io.Reader) (*specs.Spec, error) {
6577
decoder := json.NewDecoder(reader)
78+
if isStrict {
79+
decoder.DisallowUnknownFields()
80+
}
6681

6782
var spec specs.Spec
6883

internal/oci/spec_file_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestLoadFrom(t *testing.T) {
4444

4545
for i, tc := range testCases {
4646
var spec *specs.Spec
47-
spec, err := LoadFrom(bytes.NewReader(tc.contents))
47+
spec, err := strictLoader.loadFrom(bytes.NewReader(tc.contents))
4848

4949
if tc.isError {
5050
require.Error(t, err, "%d: %v", i, tc)

internal/oci/spec_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func TestMaintainSpec(t *testing.T) {
2121
for _, f := range files {
2222
inputSpecPath := filepath.Join(moduleRoot, "tests/input", f)
2323

24-
spec := NewFileSpec(inputSpecPath).(*fileSpec)
24+
spec := NewFileSpec(inputSpecPath, true).(*fileSpec)
2525

2626
_, err := spec.Load()
2727
require.NoError(t, err)

internal/oci/state.go

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -56,26 +56,10 @@ func ReadContainerState(reader io.Reader) (*State, error) {
5656
return &s, nil
5757
}
5858

59-
// LoadSpec loads the OCI spec associated with the container state
60-
func (s *State) LoadSpec() (*specs.Spec, error) {
61-
specFilePath := GetSpecFilePath(s.Bundle)
62-
specFile, err := os.Open(specFilePath)
63-
if err != nil {
64-
return nil, fmt.Errorf("failed to open OCI spec file: %v", err)
65-
}
66-
defer specFile.Close()
67-
68-
spec, err := LoadFrom(specFile)
69-
if err != nil {
70-
return nil, fmt.Errorf("failed to load OCI spec: %v", err)
71-
}
72-
return spec, nil
73-
}
74-
7559
// GetContainerRoot returns the root for the container from the associated spec. If the spec is not yet loaded, it is
7660
// loaded and cached.
7761
func (s *State) GetContainerRoot() (string, error) {
78-
spec, err := s.LoadSpec()
62+
spec, err := s.loadMinimalSpec()
7963
if err != nil {
8064
return "", err
8165
}
@@ -91,3 +75,27 @@ func (s *State) GetContainerRoot() (string, error) {
9175

9276
return filepath.Join(s.Bundle, containerRoot), nil
9377
}
78+
79+
// loadMinimalSpec loads a reduced OCI spec associated with the container state.
80+
func (s *State) loadMinimalSpec() (*minimalSpec, error) {
81+
specFilePath := GetSpecFilePath(s.Bundle)
82+
specFile, err := os.Open(specFilePath)
83+
if err != nil {
84+
return nil, fmt.Errorf("failed to open OCI spec file: %v", err)
85+
}
86+
defer specFile.Close()
87+
88+
ms := &minimalSpec{}
89+
if err := json.NewDecoder(specFile).Decode(ms); err != nil {
90+
return nil, fmt.Errorf("failed to load minimal OCI spec: %v", err)
91+
}
92+
return ms, nil
93+
}
94+
95+
// A minimalSpec is used to return desired properties from the container config.
96+
// We define this here instead of using specs.Spec as is to avoid decoding
97+
// unneeded fields in container lifecycle hooks.
98+
type minimalSpec struct {
99+
// Root configures the container's root filesystem.
100+
Root *specs.Root `json:"root,omitempty"`
101+
}

internal/runtime/runtime_factory.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ func newNVIDIAContainerRuntime(logger logger.Interface, cfg *config.Config, argv
4242
return lowLevelRuntime, nil
4343
}
4444

45-
ociSpec, err := oci.NewSpec(logger, argv)
45+
ociSpec, err := oci.NewSpec(argv,
46+
oci.WithLogger(logger),
47+
oci.WithAllowUnknownFields(cfg.Features.AllowUnknownOCISpecFields.IsEnabled()),
48+
)
4649
if err != nil {
4750
return nil, fmt.Errorf("error constructing OCI specification: %v", err)
4851
}

0 commit comments

Comments
 (0)