Skip to content

Commit 46a78fb

Browse files
authored
fix(remount): relocate libraries along with their symlinks (#255)
1 parent ed52e3c commit 46a78fb

File tree

7 files changed

+655
-19
lines changed

7 files changed

+655
-19
lines changed

envbuilder.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
438438
tempRemountDest := filepath.Join("/", MagicDir, "mnt")
439439
// ignorePrefixes is a superset of ignorePaths that we pass to kaniko's
440440
// IgnoreList.
441-
ignorePrefixes := append([]string{"/proc", "/sys"}, ignorePaths...)
441+
ignorePrefixes := append([]string{"/dev", "/proc", "/sys"}, ignorePaths...)
442442
restoreMounts, err := ebutil.TempRemount(options.Logger, tempRemountDest, ignorePrefixes...)
443443
defer func() { // restoreMounts should never be nil
444444
if err := restoreMounts(); err != nil {

internal/ebutil/libs.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package ebutil
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
// Container runtimes like NVIDIA mount individual libraries into the container
11+
// (e.g. `<libname>.so.<driver_version>`) and create symlinks for them
12+
// (e.g. `<libname>.so.1`). This code helps with finding the right library
13+
// directory for the target Linux distribution as well as locating the symlinks.
14+
//
15+
// Please see [#143 (comment)] for further details.
16+
//
17+
// [#143 (comment)]: https://github.com/coder/envbuilder/issues/143#issuecomment-2192405828
18+
19+
// Based on https://github.com/NVIDIA/libnvidia-container/blob/v1.15.0/src/common.h#L29
20+
const usrLibDir = "/usr/lib64"
21+
22+
const debianVersionFile = "/etc/debian_version"
23+
24+
// libraryDirectoryPath returns the library directory. It returns a multiarch
25+
// directory if the distribution is Debian or a derivative.
26+
//
27+
// Based on https://github.com/NVIDIA/libnvidia-container/blob/v1.15.0/src/nvc_container.c#L152-L165
28+
func libraryDirectoryPath(m mounter) (string, error) {
29+
// Debian and its derivatives use a multiarch directory scheme.
30+
if _, err := m.Stat(debianVersionFile); err != nil && !errors.Is(err, os.ErrNotExist) {
31+
return "", fmt.Errorf("check if debian: %w", err)
32+
} else if err == nil {
33+
return usrLibMultiarchDir, nil
34+
}
35+
36+
return usrLibDir, nil
37+
}
38+
39+
// libraryDirectorySymlinks returns a mapping of each library (basename) with a
40+
// list of their symlinks (basename). Libraries with no symlinks do not appear
41+
// in the mapping.
42+
func libraryDirectorySymlinks(m mounter, libDir string) (map[string][]string, error) {
43+
des, err := m.ReadDir(libDir)
44+
if err != nil {
45+
return nil, fmt.Errorf("read directory %s: %w", libDir, err)
46+
}
47+
48+
libsSymlinks := make(map[string][]string)
49+
for _, de := range des {
50+
if de.IsDir() {
51+
continue
52+
}
53+
54+
if de.Type()&os.ModeSymlink != os.ModeSymlink {
55+
// Not a symlink. Skip.
56+
continue
57+
}
58+
59+
symlink := filepath.Join(libDir, de.Name())
60+
path, err := m.EvalSymlinks(symlink)
61+
if err != nil {
62+
return nil, fmt.Errorf("eval symlink %s: %w", symlink, err)
63+
}
64+
65+
path = filepath.Base(path)
66+
if _, ok := libsSymlinks[path]; !ok {
67+
libsSymlinks[path] = make([]string, 0, 1)
68+
}
69+
70+
libsSymlinks[path] = append(libsSymlinks[path], de.Name())
71+
}
72+
73+
return libsSymlinks, nil
74+
}
75+
76+
// moveLibSymlinks moves a list of symlinks from source to destination directory.
77+
func moveLibSymlinks(m mounter, symlinks []string, srcDir, destDir string) error {
78+
for _, l := range symlinks {
79+
oldpath := filepath.Join(srcDir, l)
80+
newpath := filepath.Join(destDir, l)
81+
if err := m.Rename(oldpath, newpath); err != nil {
82+
return fmt.Errorf("move symlink %s => %s: %w", oldpath, newpath, err)
83+
}
84+
}
85+
return nil
86+
}

internal/ebutil/libs_amd64.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//go:build amd64
2+
3+
package ebutil
4+
5+
// Based on https://github.com/NVIDIA/libnvidia-container/blob/v1.15.0/src/common.h#L36
6+
7+
const usrLibMultiarchDir = "/usr/lib/x86_64-linux-gnu"

internal/ebutil/libs_arm64.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//go:build arm64
2+
3+
package ebutil
4+
5+
// Based on https://github.com/NVIDIA/libnvidia-container/blob/v1.15.0/src/common.h#L52
6+
7+
const usrLibMultiarchDir = "/usr/lib/aarch64-linux-gnu"

internal/ebutil/mock_mounter_test.go

+44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/ebutil/remount.go

+63-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ebutil
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"path/filepath"
@@ -44,15 +45,36 @@ func tempRemount(m mounter, logf func(notcodersdk.LogLevel, string, ...any), bas
4445
return func() error { return nil }, fmt.Errorf("get mounts: %w", err)
4546
}
4647

48+
libDir, err := libraryDirectoryPath(m)
49+
if err != nil {
50+
return func() error { return nil }, fmt.Errorf("get lib directory: %w", err)
51+
}
52+
53+
libsSymlinks, err := libraryDirectorySymlinks(m, libDir)
54+
if err != nil && !errors.Is(err, os.ErrNotExist) {
55+
return func() error { return nil }, fmt.Errorf("read lib symlinks: %w", err)
56+
}
57+
4758
// temp move of all ro mounts
4859
mounts := map[string]string{}
4960
var restoreOnce sync.Once
5061
var merr error
5162
// closer to attempt to restore original mount points
5263
restore = func() error {
5364
restoreOnce.Do(func() {
65+
if len(mounts) == 0 {
66+
return
67+
}
68+
69+
newLibDir, err := libraryDirectoryPath(m)
70+
if err != nil {
71+
merr = multierror.Append(merr, fmt.Errorf("get new lib directory: %w", err))
72+
return
73+
}
74+
5475
for orig, moved := range mounts {
55-
if err := remount(m, moved, orig); err != nil {
76+
logf(notcodersdk.LogLevelTrace, "restore mount %s", orig)
77+
if err := remount(m, moved, orig, newLibDir, libsSymlinks); err != nil {
5678
merr = multierror.Append(merr, fmt.Errorf("restore mount: %w", err))
5779
}
5880
}
@@ -77,7 +99,8 @@ outer:
7799

78100
src := mountInfo.MountPoint
79101
dest := filepath.Join(base, src)
80-
if err := remount(m, src, dest); err != nil {
102+
logf(notcodersdk.LogLevelTrace, "temp remount %s", src)
103+
if err := remount(m, src, dest, libDir, libsSymlinks); err != nil {
81104
return restore, fmt.Errorf("temp remount: %w", err)
82105
}
83106

@@ -87,30 +110,48 @@ outer:
87110
return restore, nil
88111
}
89112

90-
func remount(m mounter, src, dest string) error {
113+
func remount(m mounter, src, dest, libDir string, libsSymlinks map[string][]string) error {
91114
stat, err := m.Stat(src)
92115
if err != nil {
93116
return fmt.Errorf("stat %s: %w", src, err)
94117
}
118+
95119
var destDir string
96120
if stat.IsDir() {
97121
destDir = dest
98122
} else {
99123
destDir = filepath.Dir(dest)
124+
if destDir == usrLibDir || destDir == usrLibMultiarchDir {
125+
// Restore mount to libDir
126+
destDir = libDir
127+
dest = filepath.Join(destDir, stat.Name())
128+
}
100129
}
130+
101131
if err := m.MkdirAll(destDir, 0o750); err != nil {
102132
return fmt.Errorf("ensure path: %w", err)
103133
}
134+
104135
if !stat.IsDir() {
105136
f, err := m.OpenFile(dest, os.O_CREATE, 0o640)
106137
if err != nil {
107138
return fmt.Errorf("ensure file path: %w", err)
108139
}
109-
defer f.Close()
140+
// This ensure the file is created, it will not be used. It can be closed immediately.
141+
f.Close()
142+
143+
if symlinks, ok := libsSymlinks[stat.Name()]; ok {
144+
srcDir := filepath.Dir(src)
145+
if err := moveLibSymlinks(m, symlinks, srcDir, destDir); err != nil {
146+
return err
147+
}
148+
}
110149
}
150+
111151
if err := m.Mount(src, dest, "bind", syscall.MS_BIND, ""); err != nil {
112152
return fmt.Errorf("bind mount %s => %s: %w", src, dest, err)
113153
}
154+
114155
if err := m.Unmount(src, 0); err != nil {
115156
return fmt.Errorf("unmount orig src %s: %w", src, err)
116157
}
@@ -131,6 +172,12 @@ type mounter interface {
131172
Mount(string, string, string, uintptr, string) error
132173
// Unmount wraps syscall.Unmount
133174
Unmount(string, int) error
175+
// ReadDir wraps os.ReadDir
176+
ReadDir(string) ([]os.DirEntry, error)
177+
// EvalSymlinks wraps filepath.EvalSymlinks
178+
EvalSymlinks(string) (string, error)
179+
// Rename wraps os.Rename
180+
Rename(string, string) error
134181
}
135182

136183
// realMounter implements mounter and actually does the thing.
@@ -161,3 +208,15 @@ func (m *realMounter) OpenFile(name string, flag int, perm os.FileMode) (*os.Fil
161208
func (m *realMounter) Stat(path string) (os.FileInfo, error) {
162209
return os.Stat(path)
163210
}
211+
212+
func (m *realMounter) ReadDir(name string) ([]os.DirEntry, error) {
213+
return os.ReadDir(name)
214+
}
215+
216+
func (m *realMounter) EvalSymlinks(path string) (string, error) {
217+
return filepath.EvalSymlinks(path)
218+
}
219+
220+
func (m *realMounter) Rename(oldpath, newpath string) error {
221+
return os.Rename(oldpath, newpath)
222+
}

0 commit comments

Comments
 (0)