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
22 changes: 22 additions & 0 deletions cmd/nvidia-cdi-hook/update-ldcache/update-ldcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/moby/sys/reexec"
"github.com/urfave/cli/v3"

"github.com/NVIDIA/nvidia-container-toolkit/internal/config/image"
"github.com/NVIDIA/nvidia-container-toolkit/internal/ldconfig"
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
Expand Down Expand Up @@ -119,6 +120,7 @@ func (m command) run(_ *cli.Command, cfg *options) error {
reexecUpdateLdCacheCommandName,
cfg.ldconfigPath,
containerRootDir,
m.compat32Requested(s),
cfg.folders...,
)
if err != nil {
Expand All @@ -127,6 +129,26 @@ func (m command) run(_ *cli.Command, cfg *options) error {
return runner.Run()
}

// compat32Requested checks whether the container requested the compat32
// driver capability. This mirrors the nvidia-container-cli, where the 32-bit
// libraries are associated with this capability.
// Note that the environment of the container is read from its OCI spec, which
// is not always accessible, and a container whose capabilities cannot be
// determined is treated as not having requested this one.
func (m command) compat32Requested(s *oci.State) bool {
env, err := s.GetEnv()
if err != nil {
m.logger.Debugf("Failed to get the container environment: %v", err)
return false
}
containerImage, err := image.New(image.WithEnv(env))
if err != nil {
m.logger.Debugf("Failed to construct image from the container environment: %v", err)
return false
}
return containerImage.GetDriverCapabilities().Has(image.DriverCapabilityCompat32)
}

// updateLdCacheHandler wraps updateLdCache with error handling.
func updateLdCacheHandler() {
if err := updateLdCache(os.Args); err != nil {
Expand Down
17 changes: 16 additions & 1 deletion cmd/nvidia-ctk/cdi/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ type options struct {
disabledHooks []string
enabledHooks []string

featureFlags []string
disableCompat32 bool
featureFlags []string

csv struct {
files []string
Expand Down Expand Up @@ -236,6 +237,16 @@ func (m command) build() *cli.Command {
Destination: &opts.enabledHooks,
Sources: cli.EnvVars("NVIDIA_CTK_CDI_GENERATE_ENABLED_HOOKS"),
},
&cli.BoolFlag{
Name: "disable-compat32",
Usage: "Exclude the 32-bit compatibility driver libraries installed on the host from the " +
"generated CDI specification. These are included by default and are only exposed to a " +
"container that can run 32-bit applications: one that requests the 'compat32' driver " +
"capability or ships a 32-bit dynamic linker, and never a container that uses musl. " +
"This is equivalent to specifying the '" + string(nvcdi.FeatureDisableCompat32Libraries) + "' feature flag.",
Destination: &opts.disableCompat32,
Sources: cli.EnvVars("NVIDIA_CTK_CDI_GENERATE_DISABLE_COMPAT32"),
},
&cli.StringSliceFlag{
Name: "feature-flag",
Aliases: []string{"feature-flags"},
Expand Down Expand Up @@ -399,6 +410,10 @@ func (m command) generateSpecs(opts *options) ([]generatedSpecs, error) {
nvcdi.WithNvmlLib(opts.nvmllib),
}

if opts.disableCompat32 {
cdiOptions = append(cdiOptions, nvcdi.WithFeatureFlags(nvcdi.FeatureDisableCompat32Libraries))
}

cdilib, err := nvcdi.New(cdiOptions...)
if err != nil {
return nil, fmt.Errorf("failed to create CDI library: %v", err)
Expand Down
5 changes: 5 additions & 0 deletions deployments/systemd/nvidia-cdi-refresh.env
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
# uncomment the following line:
# NVIDIA_CTK_CDI_OUTPUT_FILE_PATH=/var/run/cdi/nvidia.yaml

# The 32-bit compatibility driver libraries installed on the host are included
# in the generated CDI specification. To leave these out, uncomment the
# following line:
# NVIDIA_CTK_CDI_GENERATE_DISABLE_COMPAT32=true

# The service also runs
# nvidia-ctk system create-device-nodes --control-devices --load-kernel-modules
# before generating the CDI specification. Its driver and device roots can be
Expand Down
93 changes: 93 additions & 0 deletions internal/ldconfig/compat32.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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 ldconfig

import (
"debug/elf"
"path/filepath"
"runtime"
)

// compat32Loaders maps the platform to the paths of the dynamic linkers that a
// container uses to run 32-bit applications.
var compat32Loaders = map[string][]string{
"amd64": {"/lib/ld-linux.so.2", "/lib32/ld-linux.so.2"},
"arm64": {"/lib/ld-linux-armhf.so.3", "/lib/ld-linux.so.3"},
}

// allowsCompat32 checks whether the 32-bit driver libraries are of use in the
// specified root.
//
// A container that uses musl never sees these. A musl .path file carries no
// architecture information and musl has no notion of a multiarch layout: it
// loads the first file matching the requested name and fails instead of
// searching on if that file is of another ELF class.
//
// Other containers see the 32-bit libraries if they requested the compat32
// driver capability -- as is the case for the nvidia-container-cli -- or if
// they ship a dynamic linker for 32-bit applications.
func allowsCompat32(root string, requested bool) bool {
if arch, ok := muslArchs[runtime.GOARCH]; ok && isMusl(root, arch) {
return false
}
if requested {
return true
}
for _, loader := range compat32Loaders[runtime.GOARCH] {
if isFile(filepath.Join(root, loader)) {
return true
}
}
return false
}

// excludeCompat32Directories returns the specified directories with those that
// hold 32-bit libraries removed. The directories are resolved relative to the
// specified root.
func excludeCompat32Directories(root string, dirs []string) []string {
var filtered []string
for _, dir := range dirs {
if isCompat32Dir(filepath.Join(root, dir)) {
continue
}
filtered = append(filtered, dir)
}
return filtered
}

// isCompat32Dir checks whether the specified directory holds 32-bit libraries
// and no 64-bit ones. Files that are not ELF files are ignored, as are
// directories that hold no libraries at all.
// Note that both supported platforms are 64-bit.
func isCompat32Dir(dir string) bool {
var compat32 bool
libraries, _ := filepath.Glob(filepath.Join(dir, "lib?*.so*"))
for _, library := range libraries {
f, err := elf.Open(library)
if err != nil {
continue
}
class := f.Class
_ = f.Close()
if class == elf.ELFCLASS64 {
return false
}
compat32 = compat32 || class == elf.ELFCLASS32
}
return compat32
}
191 changes: 191 additions & 0 deletions internal/ldconfig/compat32_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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 ldconfig

import (
"bytes"
"debug/elf"
"encoding/binary"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/require"
)

func TestAllowsCompat32(t *testing.T) {
loaders, ok := compat32Loaders[runtime.GOARCH]
if !ok {
t.Skip("32-bit libraries are not handled on this platform")
}
muslArch := muslArchs[runtime.GOARCH]

testCases := []struct {
description string
// compat32Loader adds a dynamic linker for 32-bit applications.
compat32Loader string
// isMusl adds the musl dynamic linker.
isMusl bool
// requested indicates that the compat32 driver capability was requested.
requested bool
expected bool
}{
{
description: "64-bit-only container is skipped",
},
{
description: "container with a 32-bit loader is allowed",
compat32Loader: loaders[0],
expected: true,
},
{
description: "container that requested the capability is allowed",
requested: true,
expected: true,
},
{
description: "musl container is skipped",
isMusl: true,
},
{
description: "musl container that requested the capability is skipped",
isMusl: true,
requested: true,
},
{
description: "musl container with a 32-bit loader is skipped",
isMusl: true,
compat32Loader: loaders[0],
},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
root := t.TempDir()
if tc.compat32Loader != "" {
makeFile(t, filepath.Join(root, tc.compat32Loader))
}
if tc.isMusl {
makeMuslLoader(t, root, muslArch)
}

require.Equal(t, tc.expected, allowsCompat32(root, tc.requested))
})
}
}

func TestAllowsCompat32DetectsAllLoaders(t *testing.T) {
for _, loader := range compat32Loaders[runtime.GOARCH] {
t.Run(loader, func(t *testing.T) {
root := t.TempDir()
makeFile(t, filepath.Join(root, loader))

require.True(t, allowsCompat32(root, false))
})
}
}

func TestExcludeCompat32Directories(t *testing.T) {
root := t.TempDir()
makeLibDir(t, root, "/native", elf.ELFCLASS64)
makeLibDir(t, root, "/compat32", elf.ELFCLASS32)
makeLibDir(t, root, "/mixed", elf.ELFCLASS64, elf.ELFCLASS32)
makeLibDir(t, root, "/empty")
notALib := makeLibDir(t, root, "/not-a-lib")
require.NoError(t, os.WriteFile(filepath.Join(notALib, "libnotelf.so.1"), []byte("#!/bin/sh\n"), 0o600))

testCases := []struct {
description string
dirs []string
expected []string
}{
{
description: "32-bit dirs are removed",
dirs: []string{"/native", "/compat32"},
expected: []string{"/native"},
},
{
description: "dirs with libraries of both classes are kept",
dirs: []string{"/mixed"},
expected: []string{"/mixed"},
},
{
description: "dirs without libraries are kept",
dirs: []string{"/empty", "/not-a-lib", "/does-not-exist"},
expected: []string{"/empty", "/not-a-lib", "/does-not-exist"},
},
{
description: "the order of the remaining dirs is maintained",
dirs: []string{"/compat32", "/mixed", "/native"},
expected: []string{"/mixed", "/native"},
},
{
description: "32-bit dirs alone leave no dirs",
dirs: []string{"/compat32"},
},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
require.Equal(t, tc.expected, excludeCompat32Directories(root, tc.dirs))
})
}
}

// makeLibDir creates a directory in the specified root holding a library of
// each of the specified ELF classes.
func makeLibDir(t *testing.T, root string, dir string, classes ...elf.Class) string {
t.Helper()

path := filepath.Join(root, dir)
require.NoError(t, os.MkdirAll(path, 0o755))
for _, class := range classes {
name := "libnvidia-" + class.String() + ".so.999.88.77"
require.NoError(t, os.WriteFile(filepath.Join(path, name), elfFile(t, class), 0o600))
}
return path
}

// makeFile creates an empty file at the specified path.
func makeFile(t *testing.T, path string) {
t.Helper()

require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, nil, 0o600))
}

// elfFile returns a minimal ELF file of the specified class: a header without
// program or section headers.
func elfFile(t *testing.T, class elf.Class) []byte {
t.Helper()

var ident [elf.EI_NIDENT]byte
copy(ident[:], elf.ELFMAG)
ident[elf.EI_CLASS] = byte(class)
ident[elf.EI_DATA] = byte(elf.ELFDATA2LSB)
ident[elf.EI_VERSION] = byte(elf.EV_CURRENT)

var header any = elf.Header64{Ident: ident, Version: uint32(elf.EV_CURRENT)}
if class == elf.ELFCLASS32 {
header = elf.Header32{Ident: ident, Version: uint32(elf.EV_CURRENT)}
}
var contents bytes.Buffer
require.NoError(t, binary.Write(&contents, binary.LittleEndian, header))
return contents.Bytes()
}
Loading