|
| 1 | +package ebutil |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + "path/filepath" |
| 7 | + "strings" |
| 8 | + "sync" |
| 9 | + "syscall" |
| 10 | + |
| 11 | + "github.com/coder/coder/v2/codersdk" |
| 12 | + "github.com/hashicorp/go-multierror" |
| 13 | + "github.com/prometheus/procfs" |
| 14 | +) |
| 15 | + |
| 16 | +// TempRemount iterates through all read-only mounted filesystems, bind-mounts them at dest, |
| 17 | +// and unmounts them from their original source. All mount points underneath ignorePrefixes |
| 18 | +// will not be touched. |
| 19 | +// |
| 20 | +// Some container runtimes such as sysbox-runc will mount in `/lib/modules` read-only. |
| 21 | +// See https://github.com/nestybox/sysbox/issues/564 |
| 22 | +// This trips us up because: |
| 23 | +// 1. We call a Kaniko library function `util.DeleteFilesystem` that does exactly what it says |
| 24 | +// on the tin. If this hits a read-only volume mounted in, unhappiness is the result. |
| 25 | +// 2. After deleting the filesystem and building the image, we extract it to the filesystem. |
| 26 | +// If some paths mounted in via volume are present at that time, unhappiness is also likely |
| 27 | +// to result -- especially in case of read-only mounts. |
| 28 | +// |
| 29 | +// To work around this we move the mounts out of the way temporarily by bind-mounting them |
| 30 | +// while we do our thing, and move them back when we're done. |
| 31 | +// |
| 32 | +// It is the responsibility of the caller to call the returned function |
| 33 | +// to restore the original mount points. If an error is encountered while attempting to perform |
| 34 | +// the operation, calling the returned function will make a best-effort attempt to restore |
| 35 | +// the original state. |
| 36 | +func TempRemount(logf func(codersdk.LogLevel, string, ...any), dest string, ignorePrefixes ...string) (restore func() error, err error, |
| 37 | +) { |
| 38 | + return tempRemount(&realMounter{}, logf, dest, ignorePrefixes...) |
| 39 | +} |
| 40 | + |
| 41 | +func tempRemount(m mounter, logf func(codersdk.LogLevel, string, ...any), base string, ignorePrefixes ...string) (restore func() error, err error) { |
| 42 | + mountInfos, err := m.GetMounts() |
| 43 | + if err != nil { |
| 44 | + return func() error { return nil }, fmt.Errorf("get mounts: %w", err) |
| 45 | + } |
| 46 | + |
| 47 | + // temp move of all ro mounts |
| 48 | + mounts := map[string]string{} |
| 49 | + var restoreOnce sync.Once |
| 50 | + var merr error |
| 51 | + // closer to attempt to restore original mount points |
| 52 | + restore = func() error { |
| 53 | + restoreOnce.Do(func() { |
| 54 | + for orig, moved := range mounts { |
| 55 | + if err := remount(m, moved, orig); err != nil { |
| 56 | + merr = multierror.Append(merr, fmt.Errorf("restore mount: %w", err)) |
| 57 | + } |
| 58 | + } |
| 59 | + }) |
| 60 | + return merr |
| 61 | + } |
| 62 | + |
| 63 | +outer: |
| 64 | + for _, mountInfo := range mountInfos { |
| 65 | + // TODO: do this for all mounts |
| 66 | + if _, ok := mountInfo.Options["ro"]; !ok { |
| 67 | + logf(codersdk.LogLevelTrace, "skip rw mount %s", mountInfo.MountPoint) |
| 68 | + continue |
| 69 | + } |
| 70 | + |
| 71 | + for _, prefix := range ignorePrefixes { |
| 72 | + if strings.HasPrefix(mountInfo.MountPoint, prefix) { |
| 73 | + logf(codersdk.LogLevelTrace, "skip mount %s under ignored prefix %s", mountInfo.MountPoint, prefix) |
| 74 | + continue outer |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + src := mountInfo.MountPoint |
| 79 | + dest := filepath.Join(base, src) |
| 80 | + if err := remount(m, src, dest); err != nil { |
| 81 | + return restore, fmt.Errorf("temp remount: %w", err) |
| 82 | + } |
| 83 | + |
| 84 | + mounts[src] = dest |
| 85 | + } |
| 86 | + |
| 87 | + return restore, nil |
| 88 | +} |
| 89 | + |
| 90 | +func remount(m mounter, src, dest string) error { |
| 91 | + if err := m.MkdirAll(dest, 0o750); err != nil { |
| 92 | + return fmt.Errorf("ensure path: %w", err) |
| 93 | + } |
| 94 | + if err := m.Mount(src, dest, "bind", syscall.MS_BIND, ""); err != nil { |
| 95 | + return fmt.Errorf("bind mount %s => %s: %w", src, dest, err) |
| 96 | + } |
| 97 | + if err := m.Unmount(src, 0); err != nil { |
| 98 | + return fmt.Errorf("unmount orig src %s: %w", src, err) |
| 99 | + } |
| 100 | + return nil |
| 101 | +} |
| 102 | + |
| 103 | +// mounter is an interface to system-level calls used by TempRemount. |
| 104 | +type mounter interface { |
| 105 | + // GetMounts wraps procfs.GetMounts |
| 106 | + GetMounts() ([]*procfs.MountInfo, error) |
| 107 | + // MkdirAll wraps os.MkdirAll |
| 108 | + MkdirAll(string, os.FileMode) error |
| 109 | + // Mount wraps syscall.Mount |
| 110 | + Mount(string, string, string, uintptr, string) error |
| 111 | + // Unmount wraps syscall.Unmount |
| 112 | + Unmount(string, int) error |
| 113 | +} |
| 114 | + |
| 115 | +// realMounter implements mounter and actually does the thing. |
| 116 | +type realMounter struct{} |
| 117 | + |
| 118 | +var _ mounter = &realMounter{} |
| 119 | + |
| 120 | +func (m *realMounter) Mount(src string, dest string, fstype string, flags uintptr, data string) error { |
| 121 | + return syscall.Mount(src, dest, fstype, flags, data) |
| 122 | +} |
| 123 | + |
| 124 | +func (m *realMounter) Unmount(tgt string, flags int) error { |
| 125 | + return syscall.Unmount(tgt, flags) |
| 126 | +} |
| 127 | + |
| 128 | +func (m *realMounter) GetMounts() ([]*procfs.MountInfo, error) { |
| 129 | + return procfs.GetMounts() |
| 130 | +} |
| 131 | + |
| 132 | +func (m *realMounter) MkdirAll(path string, perm os.FileMode) error { |
| 133 | + return os.MkdirAll(path, perm) |
| 134 | +} |
0 commit comments