podman
304 строки · 10.3 Кб
1//go:build linux
2// +build linux
3
4package bind
5
6import (
7"errors"
8"fmt"
9"os"
10"path/filepath"
11"syscall"
12
13"github.com/containers/buildah/util"
14"github.com/containers/storage/pkg/idtools"
15"github.com/containers/storage/pkg/mount"
16"github.com/opencontainers/runtime-spec/specs-go"
17"github.com/sirupsen/logrus"
18"golang.org/x/exp/slices"
19"golang.org/x/sys/unix"
20)
21
22// SetupIntermediateMountNamespace creates a new mount namespace and bind
23// mounts all bind-mount sources into a subdirectory of bundlePath that can
24// only be reached by the root user of the container's user namespace, except
25// for Mounts which include the NoBindOption option in their options list. The
26// NoBindOption will then merely be removed.
27func SetupIntermediateMountNamespace(spec *specs.Spec, bundlePath string) (unmountAll func() error, err error) {
28defer stripNoBindOption(spec)
29
30// We expect a root directory to be defined.
31if spec.Root == nil {
32return nil, errors.New("configuration has no root filesystem?")
33}
34rootPath := spec.Root.Path
35
36// Create a new mount namespace in which to do the things we're doing.
37if err := unix.Unshare(unix.CLONE_NEWNS); err != nil {
38return nil, fmt.Errorf("creating new mount namespace for %v: %w", spec.Process.Args, err)
39}
40
41// Make all of our mounts private to our namespace.
42if err := mount.MakeRPrivate("/"); err != nil {
43return nil, fmt.Errorf("making mounts private to mount namespace for %v: %w", spec.Process.Args, err)
44}
45
46// Make sure the bundle directory is searchable. We created it with
47// TempDir(), so it should have started with permissions set to 0700.
48info, err := os.Stat(bundlePath)
49if err != nil {
50return nil, fmt.Errorf("checking permissions on %q: %w", bundlePath, err)
51}
52if err = os.Chmod(bundlePath, info.Mode()|0111); err != nil {
53return nil, fmt.Errorf("loosening permissions on %q: %w", bundlePath, err)
54}
55
56// Figure out who needs to be able to reach these bind mounts in order
57// for the container to be started.
58rootUID, rootGID, err := util.GetHostRootIDs(spec)
59if err != nil {
60return nil, err
61}
62
63// Hand back a callback that the caller can use to clean up everything
64// we're doing here.
65unmount := []string{}
66unmountAll = func() (err error) {
67for _, mountpoint := range unmount {
68// Unmount it and anything under it.
69if err2 := UnmountMountpoints(mountpoint, nil); err2 != nil {
70logrus.Warnf("pkg/bind: error unmounting %q: %v", mountpoint, err2)
71if err == nil {
72err = err2
73}
74}
75if err2 := unix.Unmount(mountpoint, unix.MNT_DETACH); err2 != nil {
76if errno, ok := err2.(syscall.Errno); !ok || errno != syscall.EINVAL {
77logrus.Warnf("pkg/bind: error detaching %q: %v", mountpoint, err2)
78if err == nil {
79err = err2
80}
81}
82}
83// Remove just the mountpoint.
84retry := 10
85remove := unix.Unlink
86err2 := remove(mountpoint)
87for err2 != nil && retry > 0 {
88if errno, ok := err2.(syscall.Errno); ok {
89switch errno {
90default:
91retry = 0
92continue
93case syscall.EISDIR:
94remove = unix.Rmdir
95err2 = remove(mountpoint)
96case syscall.EBUSY:
97if err3 := unix.Unmount(mountpoint, unix.MNT_DETACH); err3 == nil {
98err2 = remove(mountpoint)
99}
100}
101retry--
102}
103}
104if err2 != nil {
105logrus.Warnf("pkg/bind: error removing %q: %v", mountpoint, err2)
106if err == nil {
107err = err2
108}
109}
110}
111return err
112}
113
114// Create a top-level directory that the "root" user will be able to
115// access, that "root" from containers which use different mappings, or
116// other unprivileged users outside of containers, shouldn't be able to
117// access.
118mnt := filepath.Join(bundlePath, "mnt")
119if err = idtools.MkdirAndChown(mnt, 0100, idtools.IDPair{UID: int(rootUID), GID: int(rootGID)}); err != nil {
120return unmountAll, fmt.Errorf("creating %q owned by the container's root user: %w", mnt, err)
121}
122
123// Make that directory private, and add it to the list of locations we
124// unmount at cleanup time.
125if err = mount.MakeRPrivate(mnt); err != nil {
126return unmountAll, fmt.Errorf("marking filesystem at %q as private: %w", mnt, err)
127}
128unmount = append([]string{mnt}, unmount...)
129
130// Create a bind mount for the root filesystem and add it to the list.
131rootfs := filepath.Join(mnt, "rootfs")
132if err = os.Mkdir(rootfs, 0000); err != nil {
133return unmountAll, fmt.Errorf("creating directory %q: %w", rootfs, err)
134}
135if err = unix.Mount(rootPath, rootfs, "", unix.MS_BIND|unix.MS_REC|unix.MS_PRIVATE, ""); err != nil {
136return unmountAll, fmt.Errorf("bind mounting root filesystem from %q to %q: %w", rootPath, rootfs, err)
137}
138logrus.Debugf("bind mounted %q to %q", rootPath, rootfs)
139unmount = append([]string{rootfs}, unmount...)
140spec.Root.Path = rootfs
141
142// Do the same for everything we're binding in.
143mounts := make([]specs.Mount, 0, len(spec.Mounts))
144for i := range spec.Mounts {
145// If we're not using an intermediate, leave it in the list.
146if leaveBindMountAlone(spec.Mounts[i]) {
147mounts = append(mounts, spec.Mounts[i])
148continue
149}
150// Check if the source is a directory or something else.
151info, err := os.Stat(spec.Mounts[i].Source)
152if err != nil {
153if errors.Is(err, os.ErrNotExist) {
154logrus.Warnf("couldn't find %q on host to bind mount into container", spec.Mounts[i].Source)
155continue
156}
157return unmountAll, fmt.Errorf("checking if %q is a directory: %w", spec.Mounts[i].Source, err)
158}
159stage := filepath.Join(mnt, fmt.Sprintf("buildah-bind-target-%d", i))
160if info.IsDir() {
161// If the source is a directory, make one to use as the
162// mount target.
163if err = os.Mkdir(stage, 0000); err != nil {
164return unmountAll, fmt.Errorf("creating directory %q: %w", stage, err)
165}
166} else {
167// If the source is not a directory, create an empty
168// file to use as the mount target.
169file, err := os.OpenFile(stage, os.O_WRONLY|os.O_CREATE, 0000)
170if err != nil {
171return unmountAll, fmt.Errorf("creating file %q: %w", stage, err)
172}
173file.Close()
174}
175// Bind mount the source from wherever it is to a place where
176// we know the runtime helper will be able to get to it...
177if err = unix.Mount(spec.Mounts[i].Source, stage, "", unix.MS_BIND|unix.MS_REC|unix.MS_PRIVATE, ""); err != nil {
178return unmountAll, fmt.Errorf("bind mounting bind object from %q to %q: %w", spec.Mounts[i].Source, stage, err)
179}
180logrus.Debugf("bind mounted %q to %q", spec.Mounts[i].Source, stage)
181spec.Mounts[i].Source = stage
182// ... and update the source location that we'll pass to the
183// runtime to our intermediate location.
184mounts = append(mounts, spec.Mounts[i])
185unmount = append([]string{stage}, unmount...)
186}
187spec.Mounts = mounts
188
189return unmountAll, nil
190}
191
192// Decide if the mount should not be redirected to an intermediate location first.
193func leaveBindMountAlone(mount specs.Mount) bool {
194// If we know we shouldn't do a redirection for this mount, skip it.
195if slices.Contains(mount.Options, NoBindOption) {
196return true
197}
198// If we're not bind mounting it in, we don't need to do anything for it.
199if mount.Type != "bind" && !slices.Contains(mount.Options, "bind") && !slices.Contains(mount.Options, "rbind") {
200return true
201}
202return false
203}
204
205// UnmountMountpoints unmounts the given mountpoints and anything that's hanging
206// off of them, rather aggressively. If a mountpoint also appears in the
207// mountpointsToRemove slice, the mountpoints are removed after they are
208// unmounted.
209func UnmountMountpoints(mountpoint string, mountpointsToRemove []string) error {
210mounts, err := mount.GetMounts()
211if err != nil {
212return fmt.Errorf("retrieving list of mounts: %w", err)
213}
214// getChildren returns the list of mount IDs that hang off of the
215// specified ID.
216getChildren := func(id int) []int {
217var list []int
218for _, info := range mounts {
219if info.Parent == id {
220list = append(list, info.ID)
221}
222}
223return list
224}
225// getTree returns the list of mount IDs that hang off of the specified
226// ID, and off of those mount IDs, etc.
227getTree := func(id int) []int {
228mounts := []int{id}
229i := 0
230for i < len(mounts) {
231children := getChildren(mounts[i])
232mounts = append(mounts, children...)
233i++
234}
235return mounts
236}
237// getMountByID looks up the mount info with the specified ID
238getMountByID := func(id int) *mount.Info {
239for i := range mounts {
240if mounts[i].ID == id {
241return mounts[i]
242}
243}
244return nil
245}
246// getMountByPoint looks up the mount info with the specified mountpoint
247getMountByPoint := func(mountpoint string) *mount.Info {
248for i := range mounts {
249if mounts[i].Mountpoint == mountpoint {
250return mounts[i]
251}
252}
253return nil
254}
255// find the top of the tree we're unmounting
256top := getMountByPoint(mountpoint)
257if top == nil {
258if err != nil {
259return fmt.Errorf("%q is not mounted: %w", mountpoint, err)
260}
261return nil
262}
263// add all of the mounts that are hanging off of it
264tree := getTree(top.ID)
265// unmount each mountpoint, working from the end of the list (leaf nodes) to the top
266for i := range tree {
267var st unix.Stat_t
268id := tree[len(tree)-i-1]
269mount := getMountByID(id)
270// check if this mountpoint is mounted
271if err := unix.Lstat(mount.Mountpoint, &st); err != nil {
272if errors.Is(err, os.ErrNotExist) {
273logrus.Debugf("mountpoint %q is not present(?), skipping", mount.Mountpoint)
274continue
275}
276return fmt.Errorf("checking if %q is mounted: %w", mount.Mountpoint, err)
277}
278if uint64(mount.Major) != uint64(st.Dev) || uint64(mount.Minor) != uint64(st.Dev) { //nolint:unconvert // (required for some OS/arch combinations)
279logrus.Debugf("%q is apparently not really mounted, skipping", mount.Mountpoint)
280continue
281}
282// do the unmount
283if err := unix.Unmount(mount.Mountpoint, 0); err != nil {
284// if it was busy, detach it
285if errno, ok := err.(syscall.Errno); ok && errno == syscall.EBUSY {
286err = unix.Unmount(mount.Mountpoint, unix.MNT_DETACH)
287}
288if err != nil {
289// if it was invalid (not mounted), hide the error, else return it
290if errno, ok := err.(syscall.Errno); !ok || errno != syscall.EINVAL {
291logrus.Warnf("error unmounting %q: %v", mount.Mountpoint, err)
292continue
293}
294}
295}
296// if we're also supposed to remove this thing, do that, too
297if slices.Contains(mountpointsToRemove, mount.Mountpoint) {
298if err := os.Remove(mount.Mountpoint); err != nil {
299return fmt.Errorf("removing %q: %w", mount.Mountpoint, err)
300}
301}
302}
303return nil
304}
305