Skip to content

Commit 598b974

Browse files
authored
Merge pull request #941 from elezar/seal-ldconfig
Use memfd when running ldconfig
2 parents 968e2cc + 52b9631 commit 598b974

35 files changed

+4025
-43
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
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 ldcache
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
23+
"github.com/moby/sys/symlink"
24+
)
25+
26+
// A containerRoot represents the root filesystem of a container.
27+
type containerRoot string
28+
29+
// hasPath checks whether the specified path exists in the root.
30+
func (r containerRoot) hasPath(path string) bool {
31+
resolved, err := r.resolve(path)
32+
if err != nil {
33+
return false
34+
}
35+
if _, err := os.Stat(resolved); err != nil && os.IsNotExist(err) {
36+
return false
37+
}
38+
return true
39+
}
40+
41+
// resolve returns the absolute path including root path.
42+
// Symlinks are resolved, but are guaranteed to resolve in the root.
43+
func (r containerRoot) resolve(path string) (string, error) {
44+
absolute := filepath.Clean(filepath.Join(string(r), path))
45+
return symlink.FollowSymlinkInScope(absolute, string(r))
46+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
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 ldcache
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"strconv"
23+
"syscall"
24+
25+
"github.com/opencontainers/runc/libcontainer/dmz"
26+
)
27+
28+
// SafeExec attempts to clone the specified binary (as an memfd, for example) before executing it.
29+
func (m command) SafeExec(path string, args []string, envv []string) error {
30+
safeExe, err := cloneBinary(path)
31+
if err != nil {
32+
m.logger.Warningf("Failed to clone binary %q: %v; falling back to Exec", path, err)
33+
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
34+
return syscall.Exec(path, args, envv)
35+
}
36+
defer safeExe.Close()
37+
38+
exePath := "/proc/self/fd/" + strconv.Itoa(int(safeExe.Fd()))
39+
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
40+
return syscall.Exec(exePath, args, envv)
41+
}
42+
43+
func cloneBinary(path string) (*os.File, error) {
44+
exe, err := os.Open(path)
45+
if err != nil {
46+
return nil, fmt.Errorf("opening current binary: %w", err)
47+
}
48+
defer exe.Close()
49+
50+
stat, err := exe.Stat()
51+
if err != nil {
52+
return nil, fmt.Errorf("checking %v size: %w", path, err)
53+
}
54+
size := stat.Size()
55+
56+
return dmz.CloneBinary(exe, size, path, os.TempDir())
57+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//go:build !linux
2+
// +build !linux
3+
4+
/**
5+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
**/
19+
20+
package ldcache
21+
22+
import "syscall"
23+
24+
// SafeExec is not implemented on non-linux systems and forwards directly to the
25+
// Exec syscall.
26+
func (m *command) SafeExec(path string, args []string, envv []string) error {
27+
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
28+
return syscall.Exec(path, args, envv)
29+
}

cmd/nvidia-cdi-hook/update-ldcache/update-ldcache.go

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"os"
2323
"path/filepath"
2424
"strings"
25-
"syscall"
2625

2726
"github.com/urfave/cli/v2"
2827

@@ -31,6 +30,15 @@ import (
3130
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
3231
)
3332

33+
const (
34+
// ldsoconfdFilenamePattern specifies the pattern for the filename
35+
// in ld.so.conf.d that includes references to the specified directories.
36+
// The 00-nvcr prefix is chosen to ensure that these libraries have a
37+
// higher precedence than other libraries on the system, but lower than
38+
// the 00-cuda-compat that is included in some containers.
39+
ldsoconfdFilenamePattern = "00-nvcr-*.conf"
40+
)
41+
3442
type command struct {
3543
logger logger.Interface
3644
}
@@ -100,27 +108,29 @@ func (m command) run(c *cli.Context, cfg *options) error {
100108
return fmt.Errorf("failed to load container state: %v", err)
101109
}
102110

103-
containerRoot, err := s.GetContainerRoot()
111+
containerRootDir, err := s.GetContainerRoot()
104112
if err != nil {
105113
return fmt.Errorf("failed to determined container root: %v", err)
106114
}
107115

108116
ldconfigPath := m.resolveLDConfigPath(cfg.ldconfigPath)
109117
args := []string{filepath.Base(ldconfigPath)}
110-
if containerRoot != "" {
111-
args = append(args, "-r", containerRoot)
118+
if containerRootDir != "" {
119+
args = append(args, "-r", containerRootDir)
112120
}
113121

114-
if root(containerRoot).hasPath("/etc/ld.so.cache") {
122+
containerRoot := containerRoot(containerRootDir)
123+
124+
if containerRoot.hasPath("/etc/ld.so.cache") {
115125
args = append(args, "-C", "/etc/ld.so.cache")
116126
} else {
117127
m.logger.Debugf("No ld.so.cache found, skipping update")
118128
args = append(args, "-N")
119129
}
120130

121131
folders := cfg.folders.Value()
122-
if root(containerRoot).hasPath("/etc/ld.so.conf.d") {
123-
err := m.createConfig(containerRoot, folders)
132+
if containerRoot.hasPath("/etc/ld.so.conf.d") {
133+
err := m.createLdsoconfdFile(containerRoot, ldsoconfdFilenamePattern, folders...)
124134
if err != nil {
125135
return fmt.Errorf("failed to update ld.so.conf.d: %v", err)
126136
}
@@ -132,18 +142,7 @@ func (m command) run(c *cli.Context, cfg *options) error {
132142
// be configured to use a different config file by default.
133143
args = append(args, "-f", "/etc/ld.so.conf")
134144

135-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
136-
return syscall.Exec(ldconfigPath, args, nil)
137-
}
138-
139-
type root string
140-
141-
func (r root) hasPath(path string) bool {
142-
_, err := os.Stat(filepath.Join(string(r), path))
143-
if err != nil && os.IsNotExist(err) {
144-
return false
145-
}
146-
return true
145+
return m.SafeExec(ldconfigPath, args, nil)
147146
}
148147

149148
// resolveLDConfigPath determines the LDConfig path to use for the system.
@@ -153,44 +152,46 @@ func (m command) resolveLDConfigPath(path string) string {
153152
return strings.TrimPrefix(config.NormalizeLDConfigPath("@"+path), "@")
154153
}
155154

156-
// createConfig creates (or updates) /etc/ld.so.conf.d/00-nvcr-<RANDOM_STRING>.conf in the container
157-
// to include the required paths.
158-
// Note that the 00-nvcr prefix is chosen to ensure that these libraries have
159-
// a higher precedence than other libraries on the system but are applied AFTER
160-
// 00-cuda-compat.conf.
161-
func (m command) createConfig(root string, folders []string) error {
162-
if len(folders) == 0 {
163-
m.logger.Debugf("No folders to add to /etc/ld.so.conf")
155+
// createLdsoconfdFile creates a file at /etc/ld.so.conf.d/ in the specified root.
156+
// The file is created at /etc/ld.so.conf.d/{{ .pattern }} using `CreateTemp` and
157+
// contains the specified directories on each line.
158+
func (m command) createLdsoconfdFile(in containerRoot, pattern string, dirs ...string) error {
159+
if len(dirs) == 0 {
160+
m.logger.Debugf("No directories to add to /etc/ld.so.conf")
164161
return nil
165162
}
166163

167-
if err := os.MkdirAll(filepath.Join(root, "/etc/ld.so.conf.d"), 0755); err != nil {
168-
return fmt.Errorf("failed to create ld.so.conf.d: %v", err)
164+
ldsoconfdDir, err := in.resolve("/etc/ld.so.conf.d")
165+
if err != nil {
166+
return err
167+
}
168+
if err := os.MkdirAll(ldsoconfdDir, 0755); err != nil {
169+
return fmt.Errorf("failed to create ld.so.conf.d: %w", err)
169170
}
170171

171-
configFile, err := os.CreateTemp(filepath.Join(root, "/etc/ld.so.conf.d"), "00-nvcr-*.conf")
172+
configFile, err := os.CreateTemp(ldsoconfdDir, pattern)
172173
if err != nil {
173-
return fmt.Errorf("failed to create config file: %v", err)
174+
return fmt.Errorf("failed to create config file: %w", err)
174175
}
175176
defer configFile.Close()
176177

177-
m.logger.Debugf("Adding folders %v to %v", folders, configFile.Name())
178+
m.logger.Debugf("Adding directories %v to %v", dirs, configFile.Name())
178179

179-
configured := make(map[string]bool)
180-
for _, folder := range folders {
181-
if configured[folder] {
180+
added := make(map[string]bool)
181+
for _, dir := range dirs {
182+
if added[dir] {
182183
continue
183184
}
184-
_, err = configFile.WriteString(fmt.Sprintf("%s\n", folder))
185+
_, err = configFile.WriteString(fmt.Sprintf("%s\n", dir))
185186
if err != nil {
186-
return fmt.Errorf("failed to update ld.so.conf.d: %v", err)
187+
return fmt.Errorf("failed to update config file: %w", err)
187188
}
188-
configured[folder] = true
189+
added[dir] = true
189190
}
190191

191192
// The created file needs to be world readable for the cases where the container is run as a non-root user.
192-
if err := os.Chmod(configFile.Name(), 0644); err != nil {
193-
return fmt.Errorf("failed to chmod config file: %v", err)
193+
if err := configFile.Chmod(0644); err != nil {
194+
return fmt.Errorf("failed to chmod config file: %w", err)
194195
}
195196

196197
return nil

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/NVIDIA/go-nvlib v0.7.1
77
github.com/NVIDIA/go-nvml v0.12.4-1
88
github.com/moby/sys/symlink v0.3.0
9+
github.com/opencontainers/runc v1.2.5
910
github.com/opencontainers/runtime-spec v1.2.0
1011
github.com/pelletier/go-toml v1.9.5
1112
github.com/sirupsen/logrus v1.9.3
@@ -19,13 +20,13 @@ require (
1920

2021
require (
2122
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
23+
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
2224
github.com/davecgh/go-spew v1.1.1 // indirect
2325
github.com/fsnotify/fsnotify v1.7.0 // indirect
2426
github.com/google/uuid v1.6.0 // indirect
2527
github.com/hashicorp/errwrap v1.1.0 // indirect
2628
github.com/kr/pretty v0.3.1 // indirect
2729
github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect
28-
github.com/opencontainers/selinux v1.11.0 // indirect
2930
github.com/pmezard/go-difflib v1.0.0 // indirect
3031
github.com/russross/blackfriday/v2 v2.1.0 // indirect
3132
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2y
77
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
88
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
99
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
10+
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
11+
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
1012
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1113
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1214
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -31,6 +33,8 @@ github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34
3133
github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU=
3234
github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
3335
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
36+
github.com/opencontainers/runc v1.2.5 h1:8KAkq3Wrem8bApgOHyhRI/8IeLXIfmZ6Qaw6DNSLnA4=
37+
github.com/opencontainers/runc v1.2.5/go.mod h1:dOQeFo29xZKBNeRBI0B19mJtfHv68YgCTh1X+YphA+4=
3438
github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
3539
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
3640
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=

0 commit comments

Comments
 (0)