Skip to content

Commit a1a40c9

Browse files
committed
Add support for injecting additional GIDs
This change adds support for injecting additional GIDs using the internal CDI representations. (Applicable for Tegra-based systems and /dev/dri devices) This is disabled by default, but can be opted in to by setting the features.allow-additional-gids feature flag. This can also be done by running sudo nvidia-ctk config --in-place --set features.allow-additional-gids Signed-off-by: Evan Lezar <[email protected]>
1 parent 50278cb commit a1a40c9

File tree

14 files changed

+255
-27
lines changed

14 files changed

+255
-27
lines changed

internal/config/features.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@
1717
package config
1818

1919
type featureName string
20+
type feature bool
2021

2122
const (
22-
FeatureGDS = featureName("gds")
23-
FeatureMOFED = featureName("mofed")
24-
FeatureNVSWITCH = featureName("nvswitch")
25-
FeatureGDRCopy = featureName("gdrcopy")
23+
FeatureAllowAdditionalGIDs = featureName("allow-additional-gids")
24+
FeatureGDRCopy = featureName("gdrcopy")
25+
FeatureGDS = featureName("gds")
26+
FeatureMOFED = featureName("mofed")
27+
FeatureNVSWITCH = featureName("nvswitch")
28+
29+
featureEnabled feature = true
30+
featureDisabled feature = false
2631
)
2732

2833
// features specifies a set of named features.
@@ -31,19 +36,23 @@ type features struct {
3136
MOFED *feature `toml:"mofed,omitempty"`
3237
NVSWITCH *feature `toml:"nvswitch,omitempty"`
3338
GDRCopy *feature `toml:"gdrcopy,omitempty"`
39+
// AllowAdditionalGIDs triggers the additionalGIDs field in internal CDI
40+
// specifications to be populated if required. This can be useful when
41+
// running the container as a user id that may not have access to device
42+
// nodes.
43+
AllowAdditionalGIDs *feature `toml:"allow-additional-gids,omitempty"`
3444
}
3545

36-
type feature bool
37-
3846
// IsEnabled checks whether a specified named feature is enabled.
3947
// An optional list of environments to check for feature-specific environment
4048
// variables can also be supplied.
4149
func (fs features) IsEnabled(n featureName, in ...getenver) bool {
4250
featureEnvvars := map[featureName]string{
43-
FeatureGDS: "NVIDIA_GDS",
44-
FeatureMOFED: "NVIDIA_MOFED",
45-
FeatureNVSWITCH: "NVIDIA_NVSWITCH",
46-
FeatureGDRCopy: "NVIDIA_GDRCOPY",
51+
FeatureGDS: "NVIDIA_GDS",
52+
FeatureMOFED: "NVIDIA_MOFED",
53+
FeatureNVSWITCH: "NVIDIA_NVSWITCH",
54+
FeatureGDRCopy: "NVIDIA_GDRCOPY",
55+
FeatureAllowAdditionalGIDs: "NVIDIA_ALLOW_ADDITIONAL_GIDS",
4756
}
4857

4958
envvar := featureEnvvars[n]
@@ -56,6 +65,8 @@ func (fs features) IsEnabled(n featureName, in ...getenver) bool {
5665
return fs.NVSWITCH.isEnabled(envvar, in...)
5766
case FeatureGDRCopy:
5867
return fs.GDRCopy.isEnabled(envvar, in...)
68+
case FeatureAllowAdditionalGIDs:
69+
return fs.AllowAdditionalGIDs.isEnabled(envvar, in...)
5970
default:
6071
return false
6172
}
@@ -73,7 +84,7 @@ func (f *feature) isEnabled(envvar string, ins ...getenver) bool {
7384
return false
7485
}
7586
for _, in := range ins {
76-
if in.Getenv(envvar) == "enabled" {
87+
if e := in.Getenv(envvar); e == "enabled" || e == "true" {
7788
return true
7889
}
7990
}

internal/config/features_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
# Copyright 2024 NVIDIA CORPORATION
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
**/
16+
17+
package config
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestFeatures(t *testing.T) {
26+
testCases := []struct {
27+
description string
28+
features features
29+
expected map[featureName]bool
30+
envs []getenver
31+
}{
32+
{
33+
description: "empty features",
34+
expected: map[featureName]bool{
35+
FeatureAllowAdditionalGIDs: false,
36+
FeatureGDRCopy: false,
37+
FeatureGDS: false,
38+
FeatureMOFED: false,
39+
FeatureNVSWITCH: false,
40+
},
41+
},
42+
{
43+
description: "envvar sets features if enabled",
44+
expected: map[featureName]bool{
45+
FeatureAllowAdditionalGIDs: true,
46+
FeatureGDRCopy: false,
47+
FeatureGDS: false,
48+
FeatureMOFED: false,
49+
FeatureNVSWITCH: false,
50+
},
51+
envs: []getenver{
52+
mockEnver{
53+
"NVIDIA_ALLOW_ADDITIONAL_GIDS": "enabled",
54+
},
55+
},
56+
},
57+
{
58+
description: "envvar sets features if true",
59+
expected: map[featureName]bool{
60+
FeatureAllowAdditionalGIDs: true,
61+
FeatureGDRCopy: false,
62+
FeatureGDS: false,
63+
FeatureMOFED: false,
64+
FeatureNVSWITCH: false,
65+
},
66+
envs: []getenver{
67+
mockEnver{
68+
"NVIDIA_ALLOW_ADDITIONAL_GIDS": "true",
69+
},
70+
},
71+
},
72+
{
73+
description: "feature sets feature",
74+
features: features{
75+
AllowAdditionalGIDs: ptr(featureEnabled),
76+
},
77+
},
78+
}
79+
80+
for _, tc := range testCases {
81+
t.Run(tc.description, func(t *testing.T) {
82+
for n, v := range tc.expected {
83+
t.Run(tc.description+"-"+string(n), func(t *testing.T) {
84+
require.EqualValues(t, v, tc.features.IsEnabled(n, tc.envs...))
85+
})
86+
}
87+
88+
})
89+
}
90+
}
91+
92+
func TestFeature(t *testing.T) {
93+
testCases := []struct {
94+
description string
95+
feature *feature
96+
envvar string
97+
envs []getenver
98+
expected bool
99+
}{
100+
{
101+
description: "nil feature is false",
102+
feature: nil,
103+
expected: false,
104+
},
105+
{
106+
description: "feature enables",
107+
feature: ptr(featureEnabled),
108+
expected: true,
109+
},
110+
{
111+
description: "feature disabled",
112+
feature: ptr(featureDisabled),
113+
expected: false,
114+
},
115+
}
116+
117+
for _, tc := range testCases {
118+
t.Run(tc.description, func(t *testing.T) {
119+
require.EqualValues(t, tc.expected, tc.feature.isEnabled(tc.envvar, tc.envs...))
120+
})
121+
}
122+
}
123+
124+
type mockEnver map[string]string
125+
126+
func (m mockEnver) Getenv(k string) string {
127+
return m[k]
128+
}
129+
130+
func ptr[T any](x T) *T {
131+
return &x
132+
}

internal/edits/device.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
package edits
1818

1919
import (
20+
"os"
21+
22+
"golang.org/x/sys/unix"
2023
"tags.cncf.io/container-device-interface/pkg/cdi"
2124
"tags.cncf.io/container-device-interface/specs-go"
2225

@@ -26,15 +29,23 @@ import (
2629
type device discover.Device
2730

2831
// toEdits converts a discovered device to CDI Container Edits.
29-
func (d device) toEdits() (*cdi.ContainerEdits, error) {
32+
func (d device) toEdits(allowAdditionalGIDs bool) (*cdi.ContainerEdits, error) {
3033
deviceNode, err := d.toSpec()
3134
if err != nil {
3235
return nil, err
3336
}
3437

38+
var additionalGIDs []uint32
39+
if allowAdditionalGIDs {
40+
if requiredGID := d.getRequiredGID(); requiredGID != 0 {
41+
additionalGIDs = append(additionalGIDs, requiredGID)
42+
}
43+
}
44+
3545
e := cdi.ContainerEdits{
3646
ContainerEdits: &specs.ContainerEdits{
37-
DeviceNodes: []*specs.DeviceNode{deviceNode},
47+
DeviceNodes: []*specs.DeviceNode{deviceNode},
48+
AdditionalGIDs: additionalGIDs,
3849
},
3950
}
4051
return &e, nil
@@ -52,10 +63,37 @@ func (d device) toSpec() (*specs.DeviceNode, error) {
5263
if hostPath == d.Path {
5364
hostPath = ""
5465
}
66+
5567
s := specs.DeviceNode{
5668
HostPath: hostPath,
5769
Path: d.Path,
5870
}
5971

6072
return &s, nil
6173
}
74+
75+
// getRequiredGID returns the group id of the device if the device is not world read/writable.
76+
// If the information cannot be extracted or an error occurs, 0 is returned.
77+
func (d device) getRequiredGID() uint32 {
78+
path := d.HostPath
79+
if path == "" {
80+
path = d.Path
81+
}
82+
if path == "" {
83+
return 0
84+
}
85+
86+
var stat unix.Stat_t
87+
if err := unix.Lstat(path, &stat); err != nil {
88+
return 0
89+
}
90+
// This is only supported for char devices
91+
if stat.Mode&unix.S_IFMT != unix.S_IFCHR {
92+
return 0
93+
}
94+
95+
if permissionsForOther := os.FileMode(stat.Mode).Perm(); permissionsForOther&06 == 0 {
96+
return stat.Gid
97+
}
98+
return 0
99+
}

internal/edits/lib.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func (o *options) EditsFromDiscoverer(d discover.Discover) (*cdi.ContainerEdits,
5858

5959
c := NewContainerEdits()
6060
for _, d := range devices {
61-
edits, err := device(d).toEdits()
61+
edits, err := device(d).toEdits(o.allowAdditionalGIDs)
6262
if err != nil {
6363
return nil, fmt.Errorf("failed to create container edits for device: %w", err)
6464
}

internal/edits/options.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ package edits
1919
import "github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
2020

2121
type options struct {
22-
logger logger.Interface
22+
logger logger.Interface
23+
allowAdditionalGIDs bool
2324
}
2425

2526
type Option func(*options)
@@ -29,3 +30,9 @@ func WithLogger(logger logger.Interface) Option {
2930
o.logger = logger
3031
}
3132
}
33+
34+
func WithAllowAdditionalGIDs(allowAdditionalGIDs bool) Option {
35+
return func(o *options) {
36+
o.allowAdditionalGIDs = allowAdditionalGIDs
37+
}
38+
}

internal/modifier/discover.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,18 @@ import (
2828
)
2929

3030
type discoverModifier struct {
31-
logger logger.Interface
32-
discoverer discover.Discover
31+
logger logger.Interface
32+
discoverer discover.Discover
33+
allowAdditionalGIDs bool
3334
}
3435

3536
// NewModifierFromDiscoverer creates a modifier that applies the discovered
3637
// modifications to an OCI spec if required by the runtime wrapper.
37-
func NewModifierFromDiscoverer(logger logger.Interface, d discover.Discover) (oci.SpecModifier, error) {
38+
func NewModifierFromDiscoverer(logger logger.Interface, d discover.Discover, allowAdditionalGIDs bool) (oci.SpecModifier, error) {
3839
m := discoverModifier{
39-
logger: logger,
40-
discoverer: d,
40+
logger: logger,
41+
discoverer: d,
42+
allowAdditionalGIDs: allowAdditionalGIDs,
4143
}
4244
return &m, nil
4345
}
@@ -47,6 +49,7 @@ func NewModifierFromDiscoverer(logger logger.Interface, d discover.Discover) (oc
4749
func (m discoverModifier) Modify(spec *specs.Spec) error {
4850
e := edits.New(
4951
edits.WithLogger(m.logger),
52+
edits.WithAllowAdditionalGIDs(m.allowAdditionalGIDs),
5053
)
5154

5255
specEdits, err := e.SpecModifierFromDiscoverer(m.discoverer)

internal/modifier/discover_test.go

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

133133
for _, tc := range testCases {
134134
t.Run(tc.description, func(t *testing.T) {
135-
m, err := NewModifierFromDiscoverer(logger, tc.discover)
135+
m, err := NewModifierFromDiscoverer(logger, tc.discover, true)
136136
require.NoError(t, err)
137137

138138
err = m.Modify(tc.spec)

internal/modifier/gated.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,7 @@ func NewFeatureGatedModifier(logger logger.Interface, cfg *config.Config, image
7878
discoverers = append(discoverers, d)
7979
}
8080

81-
return NewModifierFromDiscoverer(logger, discover.Merge(discoverers...))
81+
allowAdditionalGIDs := !cfg.Features.IsEnabled(config.FeatureAllowAdditionalGIDs, image)
82+
83+
return NewModifierFromDiscoverer(logger, discover.Merge(discoverers...), allowAdditionalGIDs)
8284
}

internal/modifier/graphics.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ func NewGraphicsModifier(logger logger.Interface, cfg *config.Config, image imag
6262
drmNodes,
6363
mounts,
6464
)
65-
return NewModifierFromDiscoverer(logger, d)
65+
66+
allowAdditionalGIDs := cfg.Features.IsEnabled(config.FeatureAllowAdditionalGIDs, image)
67+
return NewModifierFromDiscoverer(logger, d, allowAdditionalGIDs)
6668
}
6769

6870
// requiresGraphicsModifier determines whether a graphics modifier is required.

internal/oci/spec_mock.go

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/nvcdi/lib.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ type nvcdilib struct {
6565
infolib info.Interface
6666

6767
mergedDeviceOptions []transform.MergedDeviceOption
68+
69+
allowAdditionalGIDs bool
6870
}
6971

7072
// New creates a new nvcdi library
@@ -196,6 +198,7 @@ func (m *wrapper) GetCommonEdits() (*cdi.ContainerEdits, error) {
196198
func (l *nvcdilib) editsFromDiscoverer(d discover.Discover) (*cdi.ContainerEdits, error) {
197199
e := edits.New(
198200
edits.WithLogger(l.logger),
201+
edits.WithAllowAdditionalGIDs(l.allowAdditionalGIDs),
199202
)
200203
return e.EditsFromDiscoverer(d)
201204
}

0 commit comments

Comments
 (0)