116
core/mount/lookup_linux_test.go
Normal file
116
core/mount/lookup_linux_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
// containerd/pkg/testutil has circular dependency on this mount pkg.
|
||||
// so we use continuity/testutil instead.
|
||||
"github.com/containerd/continuity/testutil"
|
||||
"github.com/containerd/continuity/testutil/loopback"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func checkLookup(t *testing.T, fsType, mntPoint, dir string) {
|
||||
t.Helper()
|
||||
info, err := Lookup(dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fsType, info.FSType)
|
||||
assert.Equal(t, mntPoint, info.Mountpoint)
|
||||
}
|
||||
|
||||
func testLookup(t *testing.T, fsType string) {
|
||||
testutil.RequiresRoot(t)
|
||||
mnt := t.TempDir()
|
||||
|
||||
loop, err := loopback.New(100 << 20) // 100 MB
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out, err := exec.Command("mkfs", "-t", fsType, loop.Device).CombinedOutput(); err != nil {
|
||||
// not fatal
|
||||
loop.Close()
|
||||
t.Skipf("could not mkfs (%s) %s: %v (out: %q)", fsType, loop.Device, err, string(out))
|
||||
}
|
||||
if out, err := exec.Command("mount", loop.Device, mnt).CombinedOutput(); err != nil {
|
||||
// not fatal
|
||||
loop.Close()
|
||||
t.Skipf("could not mount %s: %v (out: %q)", loop.Device, err, string(out))
|
||||
}
|
||||
defer func() {
|
||||
testutil.Unmount(t, mnt)
|
||||
loop.Close()
|
||||
}()
|
||||
assert.True(t, strings.HasPrefix(loop.Device, "/dev/loop"))
|
||||
checkLookup(t, fsType, mnt, mnt)
|
||||
|
||||
newMnt := t.TempDir()
|
||||
|
||||
if out, err := exec.Command("mount", "--bind", mnt, newMnt).CombinedOutput(); err != nil {
|
||||
t.Fatalf("could not mount %s to %s: %v (out: %q)", mnt, newMnt, err, string(out))
|
||||
}
|
||||
defer func() {
|
||||
testutil.Unmount(t, newMnt)
|
||||
}()
|
||||
checkLookup(t, fsType, newMnt, newMnt)
|
||||
|
||||
subDir := filepath.Join(newMnt, "subDir")
|
||||
err = os.MkdirAll(subDir, 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkLookup(t, fsType, newMnt, subDir)
|
||||
}
|
||||
|
||||
func TestLookupWithExt4(t *testing.T) {
|
||||
testLookup(t, "ext4")
|
||||
}
|
||||
|
||||
func TestLookupWithXFS(t *testing.T) {
|
||||
testLookup(t, "xfs")
|
||||
}
|
||||
|
||||
func TestLookupWithOverlay(t *testing.T) {
|
||||
lower := t.TempDir()
|
||||
upper := t.TempDir()
|
||||
work := t.TempDir()
|
||||
overlay := t.TempDir()
|
||||
|
||||
if out, err := exec.Command("mount", "-t", "overlay", "overlay", "-o", fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s",
|
||||
lower, upper, work), overlay).CombinedOutput(); err != nil {
|
||||
// not fatal
|
||||
t.Skipf("could not mount overlay: %v (out: %q)", err, string(out))
|
||||
}
|
||||
defer testutil.Unmount(t, overlay)
|
||||
|
||||
testdir := filepath.Join(overlay, "testdir")
|
||||
err := os.Mkdir(testdir, 0777)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testfile := filepath.Join(overlay, "testfile")
|
||||
_, err = os.Create(testfile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
checkLookup(t, "overlay", overlay, testdir)
|
||||
checkLookup(t, "overlay", overlay, testfile)
|
||||
}
|
||||
51
core/mount/lookup_unix.go
Normal file
51
core/mount/lookup_unix.go
Normal file
@@ -0,0 +1,51 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
)
|
||||
|
||||
// Lookup returns the mount info corresponds to the path.
|
||||
func Lookup(dir string) (Info, error) {
|
||||
resolvedDir, err := CanonicalizePath(dir)
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
m, err := mountinfo.GetMounts(mountinfo.ParentsFilter(resolvedDir))
|
||||
if err != nil {
|
||||
return Info{}, fmt.Errorf("failed to find the mount info for %q: %w", resolvedDir, err)
|
||||
}
|
||||
if len(m) == 0 {
|
||||
return Info{}, fmt.Errorf("failed to find the mount info for %q", resolvedDir)
|
||||
}
|
||||
|
||||
// find the longest matching mount point
|
||||
var idx, maxlen int
|
||||
for i := range m {
|
||||
if len(m[i].Mountpoint) > maxlen {
|
||||
maxlen = len(m[i].Mountpoint)
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
return *m[idx], nil
|
||||
}
|
||||
29
core/mount/lookup_unsupported.go
Normal file
29
core/mount/lookup_unsupported.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Lookup returns the mount info corresponds to the path.
|
||||
func Lookup(dir string) (Info, error) {
|
||||
return Info{}, fmt.Errorf("mount.Lookup is not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
242
core/mount/losetup_linux.go
Normal file
242
core/mount/losetup_linux.go
Normal file
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
kernel "github.com/containerd/containerd/v2/contrib/seccomp/kernelversion"
|
||||
"github.com/containerd/containerd/v2/pkg/randutil"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
loopControlPath = "/dev/loop-control"
|
||||
loopDevFormat = "/dev/loop%d"
|
||||
|
||||
ebusyString = "device or resource busy"
|
||||
|
||||
loopConfigureIoctl = 0x4c0a
|
||||
)
|
||||
|
||||
type LoopConfig struct {
|
||||
Fd uint32
|
||||
BlockSize uint32
|
||||
Info unix.LoopInfo64
|
||||
Reserved [8]uint64
|
||||
}
|
||||
|
||||
func ioctlConfigure(fd int, value *LoopConfig) error {
|
||||
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(loopConfigureIoctl), uintptr(unsafe.Pointer(value)))
|
||||
if err == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// LoopParams parameters to control loop device setup
|
||||
type LoopParams struct {
|
||||
// Loop device should forbid write
|
||||
Readonly bool
|
||||
// Loop device is automatically cleared by kernel when the
|
||||
// last opener closes it
|
||||
Autoclear bool
|
||||
// Use direct IO to access the loop backing file
|
||||
Direct bool
|
||||
}
|
||||
|
||||
func getFreeLoopDev() (uint32, error) {
|
||||
ctrl, err := os.OpenFile(loopControlPath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not open %v: %v", loopControlPath, err)
|
||||
}
|
||||
defer ctrl.Close()
|
||||
num, err := unix.IoctlRetInt(int(ctrl.Fd()), unix.LOOP_CTL_GET_FREE)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not get free loop device: %w", err)
|
||||
}
|
||||
return uint32(num), nil
|
||||
}
|
||||
|
||||
// setupLoopDev attaches the backing file to the loop device and returns
|
||||
// the file handle for the loop device. The caller is responsible for
|
||||
// closing the file handle.
|
||||
func setupLoopDev(backingFile, loopDev string, param LoopParams) (_ *os.File, retErr error) {
|
||||
// 1. Open backing file and loop device
|
||||
flags := os.O_RDWR
|
||||
if param.Readonly {
|
||||
flags = os.O_RDONLY
|
||||
}
|
||||
|
||||
back, err := os.OpenFile(backingFile, flags, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open backing file: %s: %w", backingFile, err)
|
||||
}
|
||||
defer back.Close()
|
||||
|
||||
loop, err := os.OpenFile(loopDev, flags, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open loop device: %s: %w", loopDev, err)
|
||||
}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
loop.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
fiveDotEight := kernel.KernelVersion{Kernel: 5, Major: 8}
|
||||
if ok, err := kernel.GreaterEqualThan(fiveDotEight); err == nil && ok {
|
||||
config := LoopConfig{
|
||||
Fd: uint32(back.Fd()),
|
||||
}
|
||||
|
||||
copy(config.Info.File_name[:], backingFile)
|
||||
if param.Readonly {
|
||||
config.Info.Flags |= unix.LO_FLAGS_READ_ONLY
|
||||
}
|
||||
|
||||
if param.Autoclear {
|
||||
config.Info.Flags |= unix.LO_FLAGS_AUTOCLEAR
|
||||
}
|
||||
|
||||
if param.Direct {
|
||||
config.Info.Flags |= unix.LO_FLAGS_DIRECT_IO
|
||||
}
|
||||
|
||||
if err := ioctlConfigure(int(loop.Fd()), &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to configure loop device: %s: %w", loopDev, err)
|
||||
}
|
||||
|
||||
return loop, nil
|
||||
}
|
||||
|
||||
// 2. Set FD
|
||||
if err := unix.IoctlSetInt(int(loop.Fd()), unix.LOOP_SET_FD, int(back.Fd())); err != nil {
|
||||
return nil, fmt.Errorf("could not set loop fd for device: %s: %w", loopDev, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
_ = unix.IoctlSetInt(int(loop.Fd()), unix.LOOP_CLR_FD, 0)
|
||||
}
|
||||
}()
|
||||
|
||||
// 3. Set Info
|
||||
info := unix.LoopInfo64{}
|
||||
copy(info.File_name[:], backingFile)
|
||||
if param.Readonly {
|
||||
info.Flags |= unix.LO_FLAGS_READ_ONLY
|
||||
}
|
||||
|
||||
if param.Autoclear {
|
||||
info.Flags |= unix.LO_FLAGS_AUTOCLEAR
|
||||
}
|
||||
|
||||
err = unix.IoctlLoopSetStatus64(int(loop.Fd()), &info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set loop device info: %w", err)
|
||||
}
|
||||
|
||||
// 4. Set Direct IO
|
||||
if param.Direct {
|
||||
err = unix.IoctlSetInt(int(loop.Fd()), unix.LOOP_SET_DIRECT_IO, 1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup loop with direct: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return loop, nil
|
||||
}
|
||||
|
||||
// setupLoop looks for (and possibly creates) a free loop device, and
|
||||
// then attaches backingFile to it.
|
||||
//
|
||||
// When autoclear is true, caller should take care to close it when
|
||||
// done with the loop device. The loop device file handle keeps
|
||||
// loFlagsAutoclear in effect and we rely on it to clean up the loop
|
||||
// device. If caller closes the file handle after mounting the device,
|
||||
// kernel will clear the loop device after it is umounted. Otherwise
|
||||
// the loop device is cleared when the file handle is closed.
|
||||
//
|
||||
// When autoclear is false, caller should be responsible to remove
|
||||
// the loop device when done with it.
|
||||
//
|
||||
// Upon success, the file handle to the loop device is returned.
|
||||
func setupLoop(backingFile string, param LoopParams) (*os.File, error) {
|
||||
for retry := 1; retry < 100; retry++ {
|
||||
num, err := getFreeLoopDev()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loopDev := fmt.Sprintf(loopDevFormat, num)
|
||||
file, err := setupLoopDev(backingFile, loopDev, param)
|
||||
if err != nil {
|
||||
// Per util-linux/sys-utils/losetup.c:create_loop(),
|
||||
// free loop device can race and we end up failing
|
||||
// with EBUSY when trying to set it up.
|
||||
if strings.Contains(err.Error(), ebusyString) {
|
||||
// Fallback a bit to avoid live lock
|
||||
time.Sleep(time.Millisecond * time.Duration(randutil.Intn(retry*10)))
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("timeout creating new loopback device")
|
||||
}
|
||||
|
||||
func removeLoop(loopdev string) error {
|
||||
file, err := os.Open(loopdev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return unix.IoctlSetInt(int(file.Fd()), unix.LOOP_CLR_FD, 0)
|
||||
}
|
||||
|
||||
// AttachLoopDevice attaches a specified backing file to a loop device
|
||||
func AttachLoopDevice(backingFile string) (string, error) {
|
||||
file, err := setupLoop(backingFile, LoopParams{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
return file.Name(), nil
|
||||
}
|
||||
|
||||
// DetachLoopDevice detaches the provided loop devices
|
||||
func DetachLoopDevice(devices ...string) error {
|
||||
for _, dev := range devices {
|
||||
if err := removeLoop(dev); err != nil {
|
||||
return fmt.Errorf("failed to remove loop device: %s: %w", dev, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
165
core/mount/losetup_linux_test.go
Normal file
165
core/mount/losetup_linux_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/continuity/testutil"
|
||||
)
|
||||
|
||||
var randomData = []byte("randomdata")
|
||||
|
||||
func createTempFile(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
f, err := os.CreateTemp("", "losetup")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = f.Truncate(512); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func TestNonExistingLoop(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
backingFile := "setup-loop-test-no-such-file"
|
||||
_, err := setupLoop(backingFile, LoopParams{})
|
||||
if err == nil {
|
||||
t.Fatalf("setupLoop with non-existing file should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoLoop(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
backingFile := createTempFile(t)
|
||||
defer func() {
|
||||
if err := os.Remove(backingFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
file, err := setupLoop(backingFile, LoopParams{Readonly: true, Autoclear: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.Write(randomData); err == nil {
|
||||
t.Fatalf("writing to readonly loop device should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRwLoop(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
backingFile := createTempFile(t)
|
||||
defer func() {
|
||||
if err := os.Remove(backingFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
file, err := setupLoop(backingFile, LoopParams{Autoclear: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.Write(randomData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachDetachLoopDevice(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
path := createTempFile(t)
|
||||
defer func() {
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
dev, err := AttachLoopDevice(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = DetachLoopDevice(dev); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoclearTrueLoop(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
dev := func() string {
|
||||
backingFile := createTempFile(t)
|
||||
defer func() {
|
||||
if err := os.Remove(backingFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
file, err := setupLoop(backingFile, LoopParams{Autoclear: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dev := file.Name()
|
||||
file.Close()
|
||||
return dev
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if err := removeLoop(dev); err == nil {
|
||||
t.Fatalf("removeLoop should fail if Autoclear is true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoclearFalseLoop(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
dev := func() string {
|
||||
backingFile := createTempFile(t)
|
||||
defer func() {
|
||||
if err := os.Remove(backingFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
file, err := setupLoop(backingFile, LoopParams{Autoclear: false})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dev := file.Name()
|
||||
file.Close()
|
||||
return dev
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if err := removeLoop(dev); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
176
core/mount/mount.go
Normal file
176
core/mount/mount.go
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/v2/api/types"
|
||||
"github.com/containerd/continuity/fs"
|
||||
)
|
||||
|
||||
// Mount is the lingua franca of containerd. A mount represents a
|
||||
// serialized mount syscall. Components either emit or consume mounts.
|
||||
type Mount struct {
|
||||
// Type specifies the host-specific of the mount.
|
||||
Type string
|
||||
// Source specifies where to mount from. Depending on the host system, this
|
||||
// can be a source path or device.
|
||||
Source string
|
||||
// Target specifies an optional subdirectory as a mountpoint. It assumes that
|
||||
// the subdirectory exists in a parent mount.
|
||||
Target string
|
||||
// Options contains zero or more fstab-style mount options. Typically,
|
||||
// these are platform specific.
|
||||
Options []string
|
||||
}
|
||||
|
||||
// All mounts all the provided mounts to the provided target. If submounts are
|
||||
// present, it assumes that parent mounts come before child mounts.
|
||||
func All(mounts []Mount, target string) error {
|
||||
for _, m := range mounts {
|
||||
if err := m.Mount(target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmountMounts unmounts all the mounts under a target in the reverse order of
|
||||
// the mounts array provided.
|
||||
func UnmountMounts(mounts []Mount, target string, flags int) error {
|
||||
for i := len(mounts) - 1; i >= 0; i-- {
|
||||
mountpoint, err := fs.RootPath(target, mounts[i].Target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := UnmountAll(mountpoint, flags); err != nil {
|
||||
if i == len(mounts)-1 { // last mount
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanonicalizePath makes path absolute and resolves symlinks in it.
|
||||
// Path must exist.
|
||||
func CanonicalizePath(path string) (string, error) {
|
||||
// Abs also does Clean, so we do not need to call it separately
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.EvalSymlinks(path)
|
||||
}
|
||||
|
||||
// ReadOnly returns a boolean value indicating whether this mount has the "ro"
|
||||
// option set.
|
||||
func (m *Mount) ReadOnly() bool {
|
||||
for _, option := range m.Options {
|
||||
if option == "ro" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Mount to the provided target path.
|
||||
func (m *Mount) Mount(target string) error {
|
||||
target, err := fs.RootPath(target, m.Target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to join path %q with root %q: %w", m.Target, target, err)
|
||||
}
|
||||
return m.mount(target)
|
||||
}
|
||||
|
||||
// readonlyMounts modifies the received mount options
|
||||
// to make them readonly
|
||||
func readonlyMounts(mounts []Mount) []Mount {
|
||||
for i, m := range mounts {
|
||||
if m.Type == "overlay" {
|
||||
mounts[i].Options = readonlyOverlay(m.Options)
|
||||
continue
|
||||
}
|
||||
opts := make([]string, 0, len(m.Options))
|
||||
for _, opt := range m.Options {
|
||||
if opt != "rw" && opt != "ro" { // skip `ro` too so we don't append it twice
|
||||
opts = append(opts, opt)
|
||||
}
|
||||
}
|
||||
opts = append(opts, "ro")
|
||||
mounts[i].Options = opts
|
||||
}
|
||||
return mounts
|
||||
}
|
||||
|
||||
// readonlyOverlay takes mount options for overlay mounts and makes them readonly by
|
||||
// removing workdir and upperdir (and appending the upperdir layer to lowerdir) - see:
|
||||
// https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#multiple-lower-layers
|
||||
func readonlyOverlay(opt []string) []string {
|
||||
out := make([]string, 0, len(opt))
|
||||
upper := ""
|
||||
for _, o := range opt {
|
||||
if strings.HasPrefix(o, "upperdir=") {
|
||||
upper = strings.TrimPrefix(o, "upperdir=")
|
||||
} else if !strings.HasPrefix(o, "workdir=") {
|
||||
out = append(out, o)
|
||||
}
|
||||
}
|
||||
if upper != "" {
|
||||
for i, o := range out {
|
||||
if strings.HasPrefix(o, "lowerdir=") {
|
||||
out[i] = "lowerdir=" + upper + ":" + strings.TrimPrefix(o, "lowerdir=")
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ToProto converts from [Mount] to the containerd
|
||||
// APIs protobuf definition of a Mount.
|
||||
func ToProto(mounts []Mount) []*types.Mount {
|
||||
apiMounts := make([]*types.Mount, len(mounts))
|
||||
for i, m := range mounts {
|
||||
apiMounts[i] = &types.Mount{
|
||||
Type: m.Type,
|
||||
Source: m.Source,
|
||||
Target: m.Target,
|
||||
Options: m.Options,
|
||||
}
|
||||
}
|
||||
return apiMounts
|
||||
}
|
||||
|
||||
// FromProto converts from the protobuf definition [types.Mount] to
|
||||
// [Mount].
|
||||
func FromProto(mm []*types.Mount) []Mount {
|
||||
mounts := make([]Mount, len(mm))
|
||||
for i, m := range mm {
|
||||
mounts[i] = Mount{
|
||||
Type: m.Type,
|
||||
Source: m.Source,
|
||||
Target: m.Target,
|
||||
Options: m.Options,
|
||||
}
|
||||
}
|
||||
return mounts
|
||||
}
|
||||
133
core/mount/mount_freebsd.go
Normal file
133
core/mount/mount_freebsd.go
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotImplementOnUnix is returned for methods that are not implemented
|
||||
ErrNotImplementOnUnix = errors.New("not implemented under unix")
|
||||
)
|
||||
|
||||
// Mount to the provided target.
|
||||
//
|
||||
// The "syscall" and "golang.org/x/sys/unix" packages do not define a Mount
|
||||
// function for FreeBSD, so instead we execute mount(8) and trust it to do
|
||||
// the right thing
|
||||
func (m *Mount) mount(target string) error {
|
||||
// target: "/foo/target"
|
||||
// command: "mount -o ro -t nullfs /foo/source /foo/merged"
|
||||
// Note: FreeBSD mount(8) is particular about the order of flags and arguments
|
||||
var args []string
|
||||
for _, o := range m.Options {
|
||||
args = append(args, "-o", o)
|
||||
}
|
||||
args = append(args, "-t", m.Type)
|
||||
args = append(args, m.Source, target)
|
||||
|
||||
infoBeforeMount, err := Lookup(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// cmd.CombinedOutput() may intermittently return ECHILD because of our signal handling in shim.
|
||||
// See #4387 and wait(2).
|
||||
const retriesOnECHILD = 10
|
||||
for i := 0; i < retriesOnECHILD; i++ {
|
||||
cmd := exec.Command("mount", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, unix.ECHILD) {
|
||||
return fmt.Errorf("mount [%v] failed: %q: %w", args, string(out), err)
|
||||
}
|
||||
// We got ECHILD, we are not sure whether the mount was successful.
|
||||
// If the mount ID has changed, we are sure we got some new mount, but still not sure it is fully completed.
|
||||
// So we attempt to unmount the new mount before retrying.
|
||||
infoAfterMount, err := Lookup(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if infoAfterMount.ID != infoBeforeMount.ID {
|
||||
_ = unmount(target, 0)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("mount [%v] failed with ECHILD (retried %d times)", args, retriesOnECHILD)
|
||||
}
|
||||
|
||||
// Unmount the provided mount path with the flags
|
||||
func Unmount(target string, flags int) error {
|
||||
if err := unmount(target, flags); err != nil && err != unix.EINVAL {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmount(target string, flags int) error {
|
||||
for i := 0; i < 50; i++ {
|
||||
if err := unix.Unmount(target, flags); err != nil {
|
||||
switch err {
|
||||
case unix.EBUSY:
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to unmount target %s: %w", target, unix.EBUSY)
|
||||
}
|
||||
|
||||
// UnmountAll repeatedly unmounts the given mount point until there
|
||||
// are no mounts remaining (EINVAL is returned by mount), which is
|
||||
// useful for undoing a stack of mounts on the same mount point.
|
||||
// UnmountAll all is noop when the first argument is an empty string.
|
||||
// This is done when the containerd client did not specify any rootfs
|
||||
// mounts (e.g. because the rootfs is managed outside containerd)
|
||||
// UnmountAll is noop when the mount path does not exist.
|
||||
func UnmountAll(mount string, flags int) error {
|
||||
if mount == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := os.Stat(mount); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
if err := unmount(mount, flags); err != nil {
|
||||
// EINVAL is returned if the target is not a
|
||||
// mount point, indicating that we are
|
||||
// done. It can also indicate a few other
|
||||
// things (such as invalid flags) which we
|
||||
// unfortunately end up squelching here too.
|
||||
if err == unix.EINVAL {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
212
core/mount/mount_idmapped_linux.go
Normal file
212
core/mount/mount_idmapped_linux.go
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/containerd/containerd/v2/sys"
|
||||
)
|
||||
|
||||
// TODO: Support multiple mappings in future
|
||||
func parseIDMapping(mapping string) ([]syscall.SysProcIDMap, error) {
|
||||
parts := strings.Split(mapping, ":")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("user namespace mappings require the format `container-id:host-id:size`")
|
||||
}
|
||||
|
||||
cID, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid container id for user namespace remapping, %w", err)
|
||||
}
|
||||
|
||||
hID, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid host id for user namespace remapping, %w", err)
|
||||
}
|
||||
|
||||
size, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid size for user namespace remapping, %w", err)
|
||||
}
|
||||
|
||||
if cID < 0 || hID < 0 || size < 0 {
|
||||
return nil, fmt.Errorf("invalid mapping %s, all IDs and size must be positive integers", mapping)
|
||||
}
|
||||
|
||||
return []syscall.SysProcIDMap{
|
||||
{
|
||||
ContainerID: cID,
|
||||
HostID: hID,
|
||||
Size: size,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IDMapMount applies GID/UID shift according to gidmap/uidmap for target path
|
||||
func IDMapMount(source, target string, usernsFd int) (err error) {
|
||||
var (
|
||||
attr unix.MountAttr
|
||||
)
|
||||
|
||||
attr.Attr_set = unix.MOUNT_ATTR_IDMAP
|
||||
attr.Attr_clr = 0
|
||||
attr.Propagation = 0
|
||||
attr.Userns_fd = uint64(usernsFd)
|
||||
|
||||
dFd, err := unix.OpenTree(-int(unix.EBADF), source, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC|unix.AT_EMPTY_PATH))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to open tree for %s: %w", target, err)
|
||||
}
|
||||
|
||||
defer unix.Close(dFd)
|
||||
if err = unix.MountSetattr(dFd, "", unix.AT_EMPTY_PATH, &attr); err != nil {
|
||||
return fmt.Errorf("Unable to shift GID/UID for %s: %w", target, err)
|
||||
}
|
||||
|
||||
if err = unix.MoveMount(dFd, "", -int(unix.EBADF), target, unix.MOVE_MOUNT_F_EMPTY_PATH); err != nil {
|
||||
return fmt.Errorf("Unable to attach mount tree to %s: %w", target, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUsernsFD forks the current process and creates a user namespace using
|
||||
// the specified mappings.
|
||||
func GetUsernsFD(uidmap, gidmap string) (_usernsFD *os.File, _ error) {
|
||||
uidMaps, err := parseIDMapping(uidmap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gidMaps, err := parseIDMapping(gidmap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getUsernsFD(uidMaps, gidMaps)
|
||||
}
|
||||
|
||||
func getUsernsFD(uidMaps, gidMaps []syscall.SysProcIDMap) (_usernsFD *os.File, retErr error) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
pid, pidfd, errno := sys.ForkUserns()
|
||||
if errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
pidFD := os.NewFile(pidfd, "pidfd")
|
||||
defer func() {
|
||||
unix.PidfdSendSignal(int(pidFD.Fd()), unix.SIGKILL, nil, 0)
|
||||
|
||||
pidfdWaitid(pidFD)
|
||||
|
||||
pidFD.Close()
|
||||
}()
|
||||
|
||||
// NOTE:
|
||||
//
|
||||
// The usernsFD will hold the userns reference in kernel. Even if the
|
||||
// child process is reaped, the usernsFD is still valid.
|
||||
usernsFD, err := os.Open(fmt.Sprintf("/proc/%d/ns/user", pid))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get userns file descriptor for /proc/%d/user/ns: %w", pid, err)
|
||||
}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
usernsFD.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
uidmapFile, err := os.OpenFile(fmt.Sprintf("/proc/%d/%s", pid, "uid_map"), os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open /proc/%d/uid_map: %w", pid, err)
|
||||
}
|
||||
defer uidmapFile.Close()
|
||||
|
||||
gidmapFile, err := os.OpenFile(fmt.Sprintf("/proc/%d/%s", pid, "gid_map"), os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open /proc/%d/gid_map: %w", pid, err)
|
||||
}
|
||||
defer gidmapFile.Close()
|
||||
|
||||
testHookKillChildBeforePidfdSendSignal(pid, pidFD)
|
||||
|
||||
// Ensure the child process is still alive. If the err is ESRCH, we
|
||||
// should return error because we can't guarantee the usernsFD and
|
||||
// u[g]idmapFile are valid. It's safe to return error and retry.
|
||||
if err := unix.PidfdSendSignal(int(pidFD.Fd()), 0, nil, 0); err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure child process is alive: %w", err)
|
||||
}
|
||||
|
||||
testHookKillChildAfterPidfdSendSignal(pid, pidFD)
|
||||
|
||||
// NOTE:
|
||||
//
|
||||
// The u[g]id_map file descriptor is still valid if the child process
|
||||
// is reaped.
|
||||
writeMappings := func(f *os.File, idmap []syscall.SysProcIDMap) error {
|
||||
mappings := ""
|
||||
for _, m := range idmap {
|
||||
mappings = fmt.Sprintf("%s%d %d %d\n", mappings, m.ContainerID, m.HostID, m.Size)
|
||||
}
|
||||
|
||||
_, err := f.Write([]byte(mappings))
|
||||
if err1 := f.Close(); err1 != nil && err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeMappings(uidmapFile, uidMaps); err != nil {
|
||||
return nil, fmt.Errorf("failed to write uid_map: %w", err)
|
||||
}
|
||||
|
||||
if err := writeMappings(gidmapFile, gidMaps); err != nil {
|
||||
return nil, fmt.Errorf("failed to write gid_map: %w", err)
|
||||
}
|
||||
return usernsFD, nil
|
||||
}
|
||||
|
||||
func pidfdWaitid(pidFD *os.File) error {
|
||||
// https://elixir.bootlin.com/linux/v5.4.258/source/include/uapi/linux/wait.h#L20
|
||||
const PPidFD = 3
|
||||
|
||||
var e syscall.Errno
|
||||
for {
|
||||
_, _, e = syscall.Syscall6(syscall.SYS_WAITID, PPidFD, pidFD.Fd(), 0, syscall.WEXITED, 0, 0)
|
||||
if e != syscall.EINTR {
|
||||
break
|
||||
}
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
testHookLock sync.Mutex
|
||||
|
||||
testHookKillChildBeforePidfdSendSignal = func(_pid uintptr, _pidFD *os.File) {}
|
||||
|
||||
testHookKillChildAfterPidfdSendSignal = func(_pid uintptr, _pidFD *os.File) {}
|
||||
)
|
||||
243
core/mount/mount_idmapped_linux_test.go
Normal file
243
core/mount/mount_idmapped_linux_test.go
Normal file
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
kernel "github.com/containerd/containerd/v2/contrib/seccomp/kernelversion"
|
||||
"github.com/containerd/continuity/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var (
|
||||
testUIDMaps = []syscall.SysProcIDMap{
|
||||
{ContainerID: 1000, HostID: 0, Size: 100},
|
||||
{ContainerID: 5000, HostID: 2000, Size: 100},
|
||||
{ContainerID: 10000, HostID: 3000, Size: 100},
|
||||
}
|
||||
|
||||
testGIDMaps = []syscall.SysProcIDMap{
|
||||
{ContainerID: 1000, HostID: 0, Size: 100},
|
||||
{ContainerID: 5000, HostID: 2000, Size: 100},
|
||||
{ContainerID: 10000, HostID: 3000, Size: 100},
|
||||
}
|
||||
)
|
||||
|
||||
func TestGetUsernsFD(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
k512 := kernel.KernelVersion{Kernel: 5, Major: 12}
|
||||
ok, err := kernel.GreaterEqualThan(k512)
|
||||
require.NoError(t, err)
|
||||
if !ok {
|
||||
t.Skip("GetUsernsFD requires kernel >= 5.12")
|
||||
}
|
||||
|
||||
t.Run("basic", testGetUsernsFDBasic)
|
||||
|
||||
t.Run("when kill child process before write u[g]id maps", testGetUsernsFDKillChildWhenWriteUGIDMaps)
|
||||
|
||||
t.Run("when kill child process after open u[g]id_map file", testGetUsernsFDKillChildAfterOpenUGIDMapFiles)
|
||||
|
||||
}
|
||||
|
||||
func testGetUsernsFDBasic(t *testing.T) {
|
||||
for idx, tc := range []struct {
|
||||
uidMaps string
|
||||
gidMaps string
|
||||
hasErr bool
|
||||
}{
|
||||
{
|
||||
uidMaps: "0:1000:100",
|
||||
gidMaps: "0:1000:100",
|
||||
hasErr: false,
|
||||
},
|
||||
{
|
||||
uidMaps: "100:1000:100",
|
||||
gidMaps: "0:-1:100",
|
||||
hasErr: true,
|
||||
},
|
||||
{
|
||||
uidMaps: "100:1000:100",
|
||||
gidMaps: "-1:1000:100",
|
||||
hasErr: true,
|
||||
},
|
||||
{
|
||||
uidMaps: "100:1000:100",
|
||||
gidMaps: "0:1000:-1",
|
||||
hasErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("#%v", idx), func(t *testing.T) {
|
||||
_, err := GetUsernsFD(tc.uidMaps, tc.gidMaps)
|
||||
if tc.hasErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testGetUsernsFDKillChildWhenWriteUGIDMaps(t *testing.T) {
|
||||
hookFunc := func(reap bool) func(uintptr, *os.File) {
|
||||
return func(_pid uintptr, pidFD *os.File) {
|
||||
err := unix.PidfdSendSignal(int(pidFD.Fd()), unix.SIGKILL, nil, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
if reap {
|
||||
pidfdWaitid(pidFD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tcReap := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("#reap=%v", tcReap), func(t *testing.T) {
|
||||
updateTestHookKillForGetUsernsFD(t, nil, hookFunc(tcReap))
|
||||
|
||||
usernsFD, err := getUsernsFD(testUIDMaps, testGIDMaps)
|
||||
require.NoError(t, err)
|
||||
defer usernsFD.Close()
|
||||
|
||||
srcDir, checkFunc := initIDMappedChecker(t, testUIDMaps, testGIDMaps)
|
||||
destDir := t.TempDir()
|
||||
defer func() {
|
||||
require.NoError(t, UnmountAll(destDir, 0))
|
||||
}()
|
||||
|
||||
err = IDMapMount(srcDir, destDir, int(usernsFD.Fd()))
|
||||
usernsFD.Close()
|
||||
require.NoError(t, err)
|
||||
checkFunc(destDir)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testGetUsernsFDKillChildAfterOpenUGIDMapFiles(t *testing.T) {
|
||||
hookFunc := func(reap bool) func(uintptr, *os.File) {
|
||||
return func(_pid uintptr, pidFD *os.File) {
|
||||
err := unix.PidfdSendSignal(int(pidFD.Fd()), unix.SIGKILL, nil, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
if reap {
|
||||
pidfdWaitid(pidFD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
reap bool
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
reap: false,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
reap: true,
|
||||
expected: syscall.ESRCH,
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("#reap=%v", tc.reap), func(t *testing.T) {
|
||||
updateTestHookKillForGetUsernsFD(t, hookFunc(tc.reap), nil)
|
||||
|
||||
usernsFD, err := getUsernsFD(testUIDMaps, testGIDMaps)
|
||||
if tc.expected != nil {
|
||||
require.Error(t, tc.expected, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
defer usernsFD.Close()
|
||||
|
||||
srcDir, checkFunc := initIDMappedChecker(t, testUIDMaps, testGIDMaps)
|
||||
destDir := t.TempDir()
|
||||
defer func() {
|
||||
require.NoError(t, UnmountAll(destDir, 0))
|
||||
}()
|
||||
|
||||
err = IDMapMount(srcDir, destDir, int(usernsFD.Fd()))
|
||||
usernsFD.Close()
|
||||
require.NoError(t, err)
|
||||
checkFunc(destDir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateTestHookKillForGetUsernsFD(t *testing.T, newBeforeFunc, newAfterFunc func(uintptr, *os.File)) {
|
||||
testHookLock.Lock()
|
||||
|
||||
oldBefore := testHookKillChildBeforePidfdSendSignal
|
||||
oldAfter := testHookKillChildAfterPidfdSendSignal
|
||||
t.Cleanup(func() {
|
||||
testHookKillChildBeforePidfdSendSignal = oldBefore
|
||||
testHookKillChildAfterPidfdSendSignal = oldAfter
|
||||
testHookLock.Unlock()
|
||||
})
|
||||
if newBeforeFunc != nil {
|
||||
testHookKillChildBeforePidfdSendSignal = newBeforeFunc
|
||||
}
|
||||
if newAfterFunc != nil {
|
||||
testHookKillChildAfterPidfdSendSignal = newAfterFunc
|
||||
}
|
||||
}
|
||||
|
||||
func initIDMappedChecker(t *testing.T, uidMaps, gidMaps []syscall.SysProcIDMap) (_srcDir string, _verifyFunc func(destDir string)) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
srcDir := t.TempDir()
|
||||
|
||||
require.Equal(t, len(uidMaps), len(gidMaps))
|
||||
for idx := range uidMaps {
|
||||
file := filepath.Join(srcDir, fmt.Sprintf("%v", idx))
|
||||
|
||||
f, err := os.Create(file)
|
||||
require.NoError(t, err, fmt.Sprintf("create file %s", file))
|
||||
defer f.Close()
|
||||
|
||||
uid, gid := uidMaps[idx].ContainerID, gidMaps[idx].ContainerID
|
||||
err = f.Chown(uid, gid)
|
||||
require.NoError(t, err, fmt.Sprintf("chown %v:%v for file %s", uid, gid, file))
|
||||
}
|
||||
|
||||
return srcDir, func(destDir string) {
|
||||
for idx := range uidMaps {
|
||||
file := filepath.Join(destDir, fmt.Sprintf("%v", idx))
|
||||
|
||||
f, err := os.Open(file)
|
||||
require.NoError(t, err, fmt.Sprintf("open file %s", file))
|
||||
defer f.Close()
|
||||
|
||||
stat, err := f.Stat()
|
||||
require.NoError(t, err, fmt.Sprintf("stat file %s", file))
|
||||
|
||||
sysStat := stat.Sys().(*syscall.Stat_t)
|
||||
|
||||
uid, gid := uidMaps[idx].HostID, gidMaps[idx].HostID
|
||||
require.Equal(t, uint32(uid), sysStat.Uid, fmt.Sprintf("check file %s uid", file))
|
||||
require.Equal(t, uint32(gid), sysStat.Gid, fmt.Sprintf("check file %s gid", file))
|
||||
t.Logf("IDMapped File %s uid=%v, gid=%v", file, uid, gid)
|
||||
}
|
||||
}
|
||||
}
|
||||
560
core/mount/mount_linux.go
Normal file
560
core/mount/mount_linux.go
Normal file
@@ -0,0 +1,560 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type mountOpt struct {
|
||||
flags int
|
||||
data []string
|
||||
losetup bool
|
||||
uidmap string
|
||||
gidmap string
|
||||
}
|
||||
|
||||
var (
|
||||
pagesize = 4096
|
||||
allowedHelperBinaries = []string{"mount.fuse", "mount.fuse3"}
|
||||
)
|
||||
|
||||
func init() {
|
||||
pagesize = os.Getpagesize()
|
||||
}
|
||||
|
||||
// prepareIDMappedOverlay is a helper function to obtain
|
||||
// actual "lowerdir=..." mount options. It creates and
|
||||
// applies id mapping for each lowerdir.
|
||||
//
|
||||
// It returns:
|
||||
// 1. New options that include new "lowedir=..." mount option.
|
||||
// 2. "Clean up" function -- it should be called as a defer one before
|
||||
// checking for error, because if do the second and avoid calling "clean up",
|
||||
// you're going to have "dirty" setup -- there's no guarantee that those
|
||||
// temporary mount points for lowedirs will be cleaned properly.
|
||||
// 3. Error -- nil if everything's fine, otherwise an error.
|
||||
func prepareIDMappedOverlay(usernsFd int, options []string) ([]string, func(), error) {
|
||||
lowerIdx, lowerDirs := findOverlayLowerdirs(options)
|
||||
if lowerIdx == -1 {
|
||||
return options, nil, fmt.Errorf("failed to parse overlay lowerdir's from given options")
|
||||
}
|
||||
|
||||
tmpLowerdirs, idMapCleanUp, err := doPrepareIDMappedOverlay(lowerDirs, usernsFd)
|
||||
if err != nil {
|
||||
return options, idMapCleanUp, fmt.Errorf("failed to create idmapped mount: %w", err)
|
||||
}
|
||||
|
||||
options = append(options[:lowerIdx], options[lowerIdx+1:]...)
|
||||
options = append(options, fmt.Sprintf("lowerdir=%s", strings.Join(tmpLowerdirs, ":")))
|
||||
|
||||
return options, idMapCleanUp, nil
|
||||
}
|
||||
|
||||
// Mount to the provided target path.
|
||||
//
|
||||
// If m.Type starts with "fuse." or "fuse3.", "mount.fuse" or "mount.fuse3"
|
||||
// helper binary is called.
|
||||
func (m *Mount) mount(target string) (err error) {
|
||||
for _, helperBinary := range allowedHelperBinaries {
|
||||
// helperBinary = "mount.fuse", typePrefix = "fuse."
|
||||
typePrefix := strings.TrimPrefix(helperBinary, "mount.") + "."
|
||||
if strings.HasPrefix(m.Type, typePrefix) {
|
||||
return m.mountWithHelper(helperBinary, typePrefix, target)
|
||||
}
|
||||
}
|
||||
var (
|
||||
chdir string
|
||||
recalcOpt bool
|
||||
usernsFd *os.File
|
||||
options = m.Options
|
||||
)
|
||||
opt := parseMountOptions(options)
|
||||
// The only remapping of both GID and UID is supported
|
||||
if opt.uidmap != "" && opt.gidmap != "" {
|
||||
if usernsFd, err = GetUsernsFD(opt.uidmap, opt.gidmap); err != nil {
|
||||
return err
|
||||
}
|
||||
defer usernsFd.Close()
|
||||
|
||||
// overlay expects lowerdir's to be remapped instead
|
||||
if m.Type == "overlay" {
|
||||
var (
|
||||
userNsCleanUp func()
|
||||
)
|
||||
options, userNsCleanUp, err = prepareIDMappedOverlay(int(usernsFd.Fd()), options)
|
||||
defer userNsCleanUp()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare idmapped overlay: %w", err)
|
||||
}
|
||||
// To not meet concurrency issues while using the same lowedirs
|
||||
// for different containers, replace them by temporary directories,
|
||||
if optionsSize(options) >= pagesize-512 {
|
||||
recalcOpt = true
|
||||
} else {
|
||||
opt = parseMountOptions(options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// avoid hitting one page limit of mount argument buffer
|
||||
//
|
||||
// NOTE: 512 is a buffer during pagesize check.
|
||||
if m.Type == "overlay" && optionsSize(options) >= pagesize-512 {
|
||||
chdir, options = compactLowerdirOption(options)
|
||||
// recalculate opt in case of lowerdirs have been replaced
|
||||
// by idmapped ones OR idmapped mounts' not used/supported.
|
||||
if recalcOpt || (opt.uidmap == "" || opt.gidmap == "") {
|
||||
opt = parseMountOptions(options)
|
||||
}
|
||||
}
|
||||
|
||||
// propagation types.
|
||||
const ptypes = unix.MS_SHARED | unix.MS_PRIVATE | unix.MS_SLAVE | unix.MS_UNBINDABLE
|
||||
|
||||
// Ensure propagation type change flags aren't included in other calls.
|
||||
oflags := opt.flags &^ ptypes
|
||||
|
||||
var loopParams LoopParams
|
||||
if opt.losetup {
|
||||
loopParams = LoopParams{
|
||||
Readonly: oflags&unix.MS_RDONLY == unix.MS_RDONLY,
|
||||
Autoclear: true,
|
||||
}
|
||||
loopParams.Direct, opt.data = hasDirectIO(opt.data)
|
||||
}
|
||||
|
||||
dataInStr := strings.Join(opt.data, ",")
|
||||
if len(dataInStr) > pagesize {
|
||||
return errors.New("mount options is too long")
|
||||
}
|
||||
|
||||
// In the case of remounting with changed data (dataInStr != ""), need to call mount (moby/moby#34077).
|
||||
if opt.flags&unix.MS_REMOUNT == 0 || dataInStr != "" {
|
||||
// Initial call applying all non-propagation flags for mount
|
||||
// or remount with changed data
|
||||
source := m.Source
|
||||
if opt.losetup {
|
||||
loFile, err := setupLoop(m.Source, loopParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer loFile.Close()
|
||||
|
||||
// Mount the loop device instead
|
||||
source = loFile.Name()
|
||||
}
|
||||
if err := mountAt(chdir, source, target, m.Type, uintptr(oflags), dataInStr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opt.flags&ptypes != 0 {
|
||||
// Change the propagation type.
|
||||
const pflags = ptypes | unix.MS_REC | unix.MS_SILENT
|
||||
if err := unix.Mount("", target, "", uintptr(opt.flags&pflags), ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
const broflags = unix.MS_BIND | unix.MS_RDONLY
|
||||
if oflags&broflags == broflags {
|
||||
// Remount the bind to apply read only.
|
||||
return unix.Mount("", target, "", uintptr(oflags|unix.MS_REMOUNT), "")
|
||||
}
|
||||
|
||||
// remap non-overlay mount point
|
||||
if opt.uidmap != "" && opt.gidmap != "" && m.Type != "overlay" {
|
||||
if err := IDMapMount(target, target, int(usernsFd.Fd())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doPrepareIDMappedOverlay(lowerDirs []string, usernsFd int) (tmpLowerDirs []string, _ func(), _ error) {
|
||||
td, err := os.MkdirTemp(tempMountLocation, "ovl-idmapped")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cleanUp := func() {
|
||||
for _, lowerDir := range tmpLowerDirs {
|
||||
if err := unix.Unmount(lowerDir, 0); err != nil {
|
||||
logrus.WithError(err).Warnf("failed to unmount temp lowerdir %s", lowerDir)
|
||||
}
|
||||
}
|
||||
if terr := os.RemoveAll(filepath.Clean(filepath.Join(tmpLowerDirs[0], ".."))); terr != nil {
|
||||
logrus.WithError(terr).Warnf("failed to remove temporary overlay lowerdir's")
|
||||
}
|
||||
}
|
||||
for i, lowerDir := range lowerDirs {
|
||||
tmpLowerDir := filepath.Join(td, strconv.Itoa(i))
|
||||
tmpLowerDirs = append(tmpLowerDirs, tmpLowerDir)
|
||||
|
||||
if err = os.MkdirAll(tmpLowerDir, 0700); err != nil {
|
||||
return nil, cleanUp, fmt.Errorf("failed to create temporary dir: %w", err)
|
||||
}
|
||||
if err = IDMapMount(lowerDir, tmpLowerDir, usernsFd); err != nil {
|
||||
return nil, cleanUp, err
|
||||
}
|
||||
}
|
||||
return tmpLowerDirs, cleanUp, nil
|
||||
}
|
||||
|
||||
// Unmount the provided mount path with the flags
|
||||
func Unmount(target string, flags int) error {
|
||||
if err := unmount(target, flags); err != nil && err != unix.EINVAL {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fuseSuperMagic is defined in statfs(2)
|
||||
const fuseSuperMagic = 0x65735546
|
||||
|
||||
func isFUSE(dir string) bool {
|
||||
var st unix.Statfs_t
|
||||
if err := unix.Statfs(dir, &st); err != nil {
|
||||
return false
|
||||
}
|
||||
return st.Type == fuseSuperMagic
|
||||
}
|
||||
|
||||
// unmountFUSE attempts to unmount using fusermount/fusermount3 helper binary.
|
||||
//
|
||||
// For FUSE mounts, using these helper binaries is preferred, see:
|
||||
// https://github.com/containerd/containerd/pull/3765#discussion_r342083514
|
||||
func unmountFUSE(target string) error {
|
||||
var err error
|
||||
for _, helperBinary := range []string{"fusermount3", "fusermount"} {
|
||||
cmd := exec.Command(helperBinary, "-u", target)
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func unmount(target string, flags int) error {
|
||||
if isFUSE(target) {
|
||||
if err := unmountFUSE(target); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
if err := unix.Unmount(target, flags); err != nil {
|
||||
switch err {
|
||||
case unix.EBUSY:
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to unmount target %s: %w", target, unix.EBUSY)
|
||||
}
|
||||
|
||||
// UnmountAll repeatedly unmounts the given mount point until there
|
||||
// are no mounts remaining (EINVAL is returned by mount), which is
|
||||
// useful for undoing a stack of mounts on the same mount point.
|
||||
// UnmountAll all is noop when the first argument is an empty string.
|
||||
// This is done when the containerd client did not specify any rootfs
|
||||
// mounts (e.g. because the rootfs is managed outside containerd)
|
||||
// UnmountAll is noop when the mount path does not exist.
|
||||
func UnmountAll(mount string, flags int) error {
|
||||
if mount == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := os.Stat(mount); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
if err := unmount(mount, flags); err != nil {
|
||||
// EINVAL is returned if the target is not a
|
||||
// mount point, indicating that we are
|
||||
// done. It can also indicate a few other
|
||||
// things (such as invalid flags) which we
|
||||
// unfortunately end up squelching here too.
|
||||
if err == unix.EINVAL {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseMountOptions takes fstab style mount options and parses them for
|
||||
// use with a standard mount() syscall
|
||||
func parseMountOptions(options []string) (opt mountOpt) {
|
||||
loopOpt := "loop"
|
||||
flagsMap := map[string]struct {
|
||||
clear bool
|
||||
flag int
|
||||
}{
|
||||
"async": {true, unix.MS_SYNCHRONOUS},
|
||||
"atime": {true, unix.MS_NOATIME},
|
||||
"bind": {false, unix.MS_BIND},
|
||||
"defaults": {false, 0},
|
||||
"dev": {true, unix.MS_NODEV},
|
||||
"diratime": {true, unix.MS_NODIRATIME},
|
||||
"dirsync": {false, unix.MS_DIRSYNC},
|
||||
"exec": {true, unix.MS_NOEXEC},
|
||||
"mand": {false, unix.MS_MANDLOCK},
|
||||
"noatime": {false, unix.MS_NOATIME},
|
||||
"nodev": {false, unix.MS_NODEV},
|
||||
"nodiratime": {false, unix.MS_NODIRATIME},
|
||||
"noexec": {false, unix.MS_NOEXEC},
|
||||
"nomand": {true, unix.MS_MANDLOCK},
|
||||
"norelatime": {true, unix.MS_RELATIME},
|
||||
"nostrictatime": {true, unix.MS_STRICTATIME},
|
||||
"nosuid": {false, unix.MS_NOSUID},
|
||||
"rbind": {false, unix.MS_BIND | unix.MS_REC},
|
||||
"relatime": {false, unix.MS_RELATIME},
|
||||
"remount": {false, unix.MS_REMOUNT},
|
||||
"ro": {false, unix.MS_RDONLY},
|
||||
"rw": {true, unix.MS_RDONLY},
|
||||
"strictatime": {false, unix.MS_STRICTATIME},
|
||||
"suid": {true, unix.MS_NOSUID},
|
||||
"sync": {false, unix.MS_SYNCHRONOUS},
|
||||
}
|
||||
for _, o := range options {
|
||||
// If the option does not exist in the flags table or the flag
|
||||
// is not supported on the platform,
|
||||
// then it is a data value for a specific fs type
|
||||
if f, exists := flagsMap[o]; exists && f.flag != 0 {
|
||||
if f.clear {
|
||||
opt.flags &^= f.flag
|
||||
} else {
|
||||
opt.flags |= f.flag
|
||||
}
|
||||
} else if o == loopOpt {
|
||||
opt.losetup = true
|
||||
} else if strings.HasPrefix(o, "uidmap=") {
|
||||
opt.uidmap = strings.TrimPrefix(o, "uidmap=")
|
||||
} else if strings.HasPrefix(o, "gidmap=") {
|
||||
opt.gidmap = strings.TrimPrefix(o, "gidmap=")
|
||||
} else {
|
||||
opt.data = append(opt.data, o)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func hasDirectIO(opts []string) (bool, []string) {
|
||||
for idx, opt := range opts {
|
||||
if opt == "direct-io" {
|
||||
return true, append(opts[:idx], opts[idx+1:]...)
|
||||
}
|
||||
}
|
||||
return false, opts
|
||||
}
|
||||
|
||||
// compactLowerdirOption updates overlay lowdir option and returns the common
|
||||
// dir among all the lowdirs.
|
||||
func compactLowerdirOption(opts []string) (string, []string) {
|
||||
idx, dirs := findOverlayLowerdirs(opts)
|
||||
if idx == -1 || len(dirs) == 1 {
|
||||
// no need to compact if there is only one lowerdir
|
||||
return "", opts
|
||||
}
|
||||
|
||||
// find out common dir
|
||||
commondir := longestCommonPrefix(dirs)
|
||||
if commondir == "" {
|
||||
return "", opts
|
||||
}
|
||||
|
||||
// NOTE: the snapshot id is based on digits.
|
||||
// in order to avoid to get snapshots/x, should be back to parent dir.
|
||||
// however, there is assumption that the common dir is ${root}/io.containerd.v1.overlayfs/snapshots.
|
||||
commondir = path.Dir(commondir)
|
||||
if commondir == "/" || commondir == "." {
|
||||
return "", opts
|
||||
}
|
||||
commondir = commondir + "/"
|
||||
|
||||
newdirs := make([]string, 0, len(dirs))
|
||||
for _, dir := range dirs {
|
||||
if len(dir) <= len(commondir) {
|
||||
return "", opts
|
||||
}
|
||||
newdirs = append(newdirs, dir[len(commondir):])
|
||||
}
|
||||
|
||||
newopts := copyOptions(opts)
|
||||
newopts = append(newopts[:idx], newopts[idx+1:]...)
|
||||
newopts = append(newopts, fmt.Sprintf("lowerdir=%s", strings.Join(newdirs, ":")))
|
||||
return commondir, newopts
|
||||
}
|
||||
|
||||
// findOverlayLowerdirs returns the index of lowerdir in mount's options and
|
||||
// all the lowerdir target.
|
||||
func findOverlayLowerdirs(opts []string) (int, []string) {
|
||||
var (
|
||||
idx = -1
|
||||
prefix = "lowerdir="
|
||||
)
|
||||
|
||||
for i, opt := range opts {
|
||||
if strings.HasPrefix(opt, prefix) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
return -1, nil
|
||||
}
|
||||
return idx, strings.Split(opts[idx][len(prefix):], ":")
|
||||
}
|
||||
|
||||
// longestCommonPrefix finds the longest common prefix in the string slice.
|
||||
func longestCommonPrefix(strs []string) string {
|
||||
if len(strs) == 0 {
|
||||
return ""
|
||||
} else if len(strs) == 1 {
|
||||
return strs[0]
|
||||
}
|
||||
|
||||
// find out the min/max value by alphabetical order
|
||||
min, max := strs[0], strs[0]
|
||||
for _, str := range strs[1:] {
|
||||
if min > str {
|
||||
min = str
|
||||
}
|
||||
if max < str {
|
||||
max = str
|
||||
}
|
||||
}
|
||||
|
||||
// find out the common part between min and max
|
||||
for i := 0; i < len(min) && i < len(max); i++ {
|
||||
if min[i] != max[i] {
|
||||
return min[:i]
|
||||
}
|
||||
}
|
||||
return min
|
||||
}
|
||||
|
||||
// copyOptions copies the options.
|
||||
func copyOptions(opts []string) []string {
|
||||
if len(opts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
acopy := make([]string, len(opts))
|
||||
copy(acopy, opts)
|
||||
return acopy
|
||||
}
|
||||
|
||||
// optionsSize returns the byte size of options of mount.
|
||||
func optionsSize(opts []string) int {
|
||||
size := 0
|
||||
for _, opt := range opts {
|
||||
size += len(opt)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func mountAt(chdir string, source, target, fstype string, flags uintptr, data string) error {
|
||||
if chdir == "" {
|
||||
return unix.Mount(source, target, fstype, flags, data)
|
||||
}
|
||||
|
||||
ch := make(chan error, 1)
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
|
||||
// Do not unlock this thread.
|
||||
// If the thread is unlocked go will try to use it for other goroutines.
|
||||
// However it is not possible to restore the thread state after CLONE_FS.
|
||||
//
|
||||
// Once the goroutine exits the thread should eventually be terminated by go.
|
||||
|
||||
if err := unix.Unshare(unix.CLONE_FS); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
if err := unix.Chdir(chdir); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
ch <- unix.Mount(source, target, fstype, flags, data)
|
||||
}()
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func (m *Mount) mountWithHelper(helperBinary, typePrefix, target string) error {
|
||||
// helperBinary: "mount.fuse3"
|
||||
// target: "/foo/merged"
|
||||
// m.Type: "fuse3.fuse-overlayfs"
|
||||
// command: "mount.fuse3 overlay /foo/merged -o lowerdir=/foo/lower2:/foo/lower1,upperdir=/foo/upper,workdir=/foo/work -t fuse-overlayfs"
|
||||
args := []string{m.Source, target}
|
||||
for _, o := range m.Options {
|
||||
args = append(args, "-o", o)
|
||||
}
|
||||
args = append(args, "-t", strings.TrimPrefix(m.Type, typePrefix))
|
||||
|
||||
infoBeforeMount, err := Lookup(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// cmd.CombinedOutput() may intermittently return ECHILD because of our signal handling in shim.
|
||||
// See #4387 and wait(2).
|
||||
const retriesOnECHILD = 10
|
||||
for i := 0; i < retriesOnECHILD; i++ {
|
||||
cmd := exec.Command(helperBinary, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, unix.ECHILD) {
|
||||
return fmt.Errorf("mount helper [%s %v] failed: %q: %w", helperBinary, args, string(out), err)
|
||||
}
|
||||
// We got ECHILD, we are not sure whether the mount was successful.
|
||||
// If the mount ID has changed, we are sure we got some new mount, but still not sure it is fully completed.
|
||||
// So we attempt to unmount the new mount before retrying.
|
||||
infoAfterMount, err := Lookup(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if infoAfterMount.ID != infoBeforeMount.ID {
|
||||
_ = unmount(target, 0)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("mount helper [%s %v] failed with ECHILD (retried %d times)", helperBinary, args, retriesOnECHILD)
|
||||
}
|
||||
245
core/mount/mount_linux_test.go
Normal file
245
core/mount/mount_linux_test.go
Normal file
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/continuity/testutil"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestLongestCommonPrefix(t *testing.T) {
|
||||
tcases := []struct {
|
||||
in []string
|
||||
expected string
|
||||
}{
|
||||
{[]string{}, ""},
|
||||
{[]string{"foo"}, "foo"},
|
||||
{[]string{"foo", "bar"}, ""},
|
||||
{[]string{"foo", "foo"}, "foo"},
|
||||
{[]string{"foo", "foobar"}, "foo"},
|
||||
{[]string{"foo", "", "foobar"}, ""},
|
||||
}
|
||||
|
||||
for i, tc := range tcases {
|
||||
if got := longestCommonPrefix(tc.in); got != tc.expected {
|
||||
t.Fatalf("[%d case] expected (%s), but got (%s)", i+1, tc.expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactLowerdirOption(t *testing.T) {
|
||||
tcases := []struct {
|
||||
opts []string
|
||||
commondir string
|
||||
newopts []string
|
||||
}{
|
||||
// no lowerdir or only one
|
||||
{
|
||||
[]string{"workdir=a"},
|
||||
"",
|
||||
[]string{"workdir=a"},
|
||||
},
|
||||
{
|
||||
[]string{"workdir=a", "lowerdir=b"},
|
||||
"",
|
||||
[]string{"workdir=a", "lowerdir=b"},
|
||||
},
|
||||
|
||||
// >= 2 lowerdir
|
||||
{
|
||||
[]string{"lowerdir=/snapshots/1/fs:/snapshots/10/fs"},
|
||||
"/snapshots/",
|
||||
[]string{"lowerdir=1/fs:10/fs"},
|
||||
},
|
||||
{
|
||||
[]string{"lowerdir=/snapshots/1/fs:/snapshots/10/fs:/snapshots/2/fs"},
|
||||
"/snapshots/",
|
||||
[]string{"lowerdir=1/fs:10/fs:2/fs"},
|
||||
},
|
||||
|
||||
// if common dir is /
|
||||
{
|
||||
[]string{"lowerdir=/snapshots/1/fs:/other_snapshots/1/fs"},
|
||||
"",
|
||||
[]string{"lowerdir=/snapshots/1/fs:/other_snapshots/1/fs"},
|
||||
},
|
||||
|
||||
// if common dir is .
|
||||
{
|
||||
[]string{"lowerdir=a:aaa"},
|
||||
"",
|
||||
[]string{"lowerdir=a:aaa"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tcases {
|
||||
dir, opts := compactLowerdirOption(tc.opts)
|
||||
if dir != tc.commondir {
|
||||
t.Fatalf("[%d case] expected common dir (%s), but got (%s)", i+1, tc.commondir, dir)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(opts, tc.newopts) {
|
||||
t.Fatalf("[%d case] expected options (%v), but got (%v)", i+1, tc.newopts, opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFUSEHelper(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
const fuseoverlayfsBinary = "fuse-overlayfs"
|
||||
_, err := exec.LookPath(fuseoverlayfsBinary)
|
||||
if err != nil {
|
||||
t.Skip("fuse-overlayfs not installed")
|
||||
}
|
||||
td := t.TempDir()
|
||||
|
||||
for _, dir := range []string{"lower1", "lower2", "upper", "work", "merged"} {
|
||||
if err := os.Mkdir(filepath.Join(td, dir), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
opts := fmt.Sprintf("lowerdir=%s:%s,upperdir=%s,workdir=%s", filepath.Join(td, "lower2"), filepath.Join(td, "lower1"), filepath.Join(td, "upper"), filepath.Join(td, "work"))
|
||||
m := Mount{
|
||||
Type: "fuse3." + fuseoverlayfsBinary,
|
||||
Source: "overlay",
|
||||
Options: []string{opts},
|
||||
}
|
||||
dest := filepath.Join(td, "merged")
|
||||
if err := m.Mount(dest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := UnmountAll(dest, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountAt(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
dir1 := t.TempDir()
|
||||
dir2 := t.TempDir()
|
||||
|
||||
defer unix.Unmount(filepath.Join(dir2, "bar"), unix.MNT_DETACH)
|
||||
|
||||
if err := os.WriteFile(filepath.Join(dir1, "foo"), []byte("foo"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(dir2, "bar"), []byte{}, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// mount ${dir1}/foo at ${dir2}/bar
|
||||
// But since we are using `mountAt` we only need to specify the relative path to dir2 as the target mountAt will chdir to there.
|
||||
if err := mountAt(dir2, filepath.Join(dir1, "foo"), "bar", "none", unix.MS_BIND, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(dir2, "bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != "foo" {
|
||||
t.Fatalf("unexpected file content: %s", b)
|
||||
}
|
||||
|
||||
newWD, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wd != newWD {
|
||||
t.Fatalf("unexpected working directory: %s", newWD)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmountMounts(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
target, mounts := setupMounts(t)
|
||||
if err := UnmountMounts(mounts, target, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmountRecursive(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
target, _ := setupMounts(t)
|
||||
if err := UnmountRecursive(target, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupMounts(t *testing.T) (target string, mounts []Mount) {
|
||||
dir1 := t.TempDir()
|
||||
dir2 := t.TempDir()
|
||||
|
||||
if err := os.Mkdir(filepath.Join(dir1, "foo"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mounts = append(mounts, Mount{
|
||||
Type: "bind",
|
||||
Source: dir1,
|
||||
Options: []string{
|
||||
"ro",
|
||||
"rbind",
|
||||
},
|
||||
})
|
||||
|
||||
if err := os.WriteFile(filepath.Join(dir2, "bar"), []byte("bar"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mounts = append(mounts, Mount{
|
||||
Type: "bind",
|
||||
Source: dir2,
|
||||
Target: "foo",
|
||||
Options: []string{
|
||||
"ro",
|
||||
"rbind",
|
||||
},
|
||||
})
|
||||
|
||||
target = t.TempDir()
|
||||
if err := All(mounts, target); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(target, "foo/bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != "bar" {
|
||||
t.Fatalf("unexpected file content: %s", b)
|
||||
}
|
||||
|
||||
return target, mounts
|
||||
}
|
||||
150
core/mount/mount_test.go
Normal file
150
core/mount/mount_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
// required for `-test.root` flag not to fail
|
||||
_ "github.com/containerd/continuity/testutil"
|
||||
)
|
||||
|
||||
func TestReadonlyMounts(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
input []Mount
|
||||
expected []Mount
|
||||
}{
|
||||
{
|
||||
desc: "empty slice",
|
||||
input: []Mount{},
|
||||
expected: []Mount{},
|
||||
},
|
||||
{
|
||||
desc: "removes `upperdir` and `workdir` from overlay mounts, appends upper layer to lower",
|
||||
input: []Mount{
|
||||
{
|
||||
Type: "overlay",
|
||||
Source: "overlay",
|
||||
Options: []string{
|
||||
"index=off",
|
||||
"workdir=/path/to/snapshots/4/work",
|
||||
"upperdir=/path/to/snapshots/4/fs",
|
||||
"lowerdir=/path/to/snapshots/1/fs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "overlay",
|
||||
Source: "overlay",
|
||||
Options: []string{
|
||||
"index=on",
|
||||
"lowerdir=/another/path/to/snapshots/2/fs",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []Mount{
|
||||
{
|
||||
Type: "overlay",
|
||||
Source: "overlay",
|
||||
Options: []string{
|
||||
"index=off",
|
||||
"lowerdir=/path/to/snapshots/4/fs:/path/to/snapshots/1/fs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "overlay",
|
||||
Source: "overlay",
|
||||
Options: []string{
|
||||
"index=on",
|
||||
"lowerdir=/another/path/to/snapshots/2/fs",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "removes `rw` and appends `ro` (once) to other mount types",
|
||||
input: []Mount{
|
||||
{
|
||||
Type: "mount-without-rw",
|
||||
Source: "",
|
||||
Options: []string{
|
||||
"index=off",
|
||||
"workdir=/path/to/other/snapshots/work",
|
||||
"upperdir=/path/to/other/snapshots/2",
|
||||
"lowerdir=/path/to/other/snapshots/1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "mount-with-rw",
|
||||
Source: "",
|
||||
Options: []string{
|
||||
"an-option=a-value",
|
||||
"another_opt=/another/value",
|
||||
"rw",
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "mount-with-ro",
|
||||
Source: "",
|
||||
Options: []string{
|
||||
"an-option=a-value",
|
||||
"another_opt=/another/value",
|
||||
"ro",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []Mount{
|
||||
{
|
||||
Type: "mount-without-rw",
|
||||
Source: "",
|
||||
Options: []string{
|
||||
"index=off",
|
||||
"workdir=/path/to/other/snapshots/work",
|
||||
"upperdir=/path/to/other/snapshots/2",
|
||||
"lowerdir=/path/to/other/snapshots/1",
|
||||
"ro",
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "mount-with-rw",
|
||||
Source: "",
|
||||
Options: []string{
|
||||
"an-option=a-value",
|
||||
"another_opt=/another/value",
|
||||
"ro",
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "mount-with-ro",
|
||||
Source: "",
|
||||
Options: []string{
|
||||
"an-option=a-value",
|
||||
"another_opt=/another/value",
|
||||
"ro",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if !reflect.DeepEqual(readonlyMounts(tc.input), tc.expected) {
|
||||
t.Fatalf("incorrectly modified mounts: %s", tc.desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
71
core/mount/mount_unix.go
Normal file
71
core/mount/mount_unix.go
Normal file
@@ -0,0 +1,71 @@
|
||||
//go:build !windows && !darwin && !openbsd
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
)
|
||||
|
||||
// UnmountRecursive unmounts the target and all mounts underneath, starting
|
||||
// with the deepest mount first.
|
||||
func UnmountRecursive(target string, flags int) error {
|
||||
if target == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
target, err := CanonicalizePath(target)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
mounts, err := mountinfo.GetMounts(mountinfo.PrefixFilter(target))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetSet := make(map[string]struct{})
|
||||
for _, m := range mounts {
|
||||
targetSet[m.Mountpoint] = struct{}{}
|
||||
}
|
||||
|
||||
var targets []string
|
||||
for m := range targetSet {
|
||||
targets = append(targets, m)
|
||||
}
|
||||
|
||||
// Make the deepest mount be first
|
||||
sort.SliceStable(targets, func(i, j int) bool {
|
||||
return len(targets[i]) > len(targets[j])
|
||||
})
|
||||
|
||||
for i, target := range targets {
|
||||
if err := UnmountAll(target, flags); err != nil {
|
||||
if i == len(targets)-1 { // last mount
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
46
core/mount/mount_unsupported.go
Normal file
46
core/mount/mount_unsupported.go
Normal file
@@ -0,0 +1,46 @@
|
||||
//go:build darwin || openbsd
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrNotImplementOnUnix is returned for methods that are not implemented
|
||||
ErrNotImplementOnUnix = errors.New("not implemented under unix")
|
||||
)
|
||||
|
||||
// Mount is not implemented on this platform
|
||||
func (m *Mount) mount(target string) error {
|
||||
return ErrNotImplementOnUnix
|
||||
}
|
||||
|
||||
// Unmount is not implemented on this platform
|
||||
func Unmount(mount string, flags int) error {
|
||||
return ErrNotImplementOnUnix
|
||||
}
|
||||
|
||||
// UnmountAll is not implemented on this platform
|
||||
func UnmountAll(mount string, flags int) error {
|
||||
return ErrNotImplementOnUnix
|
||||
}
|
||||
|
||||
// UnmountRecursive is not implemented on this platform
|
||||
func UnmountRecursive(mount string, flags int) error {
|
||||
return ErrNotImplementOnUnix
|
||||
}
|
||||
190
core/mount/mount_windows.go
Normal file
190
core/mount/mount_windows.go
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Microsoft/go-winio/pkg/bindfilter"
|
||||
"github.com/Microsoft/hcsshim"
|
||||
"github.com/containerd/log"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const sourceStreamName = "containerd.io-source"
|
||||
|
||||
var (
|
||||
// ErrNotImplementOnWindows is returned when an action is not implemented for windows
|
||||
ErrNotImplementOnWindows = errors.New("not implemented under windows")
|
||||
)
|
||||
|
||||
// Mount to the provided target.
|
||||
func (m *Mount) mount(target string) (retErr error) {
|
||||
if m.Type != "windows-layer" {
|
||||
return fmt.Errorf("invalid windows mount type: '%s'", m.Type)
|
||||
}
|
||||
|
||||
home, layerID := filepath.Split(m.Source)
|
||||
|
||||
parentLayerPaths, err := m.GetParentPaths()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var di = hcsshim.DriverInfo{
|
||||
HomeDir: home,
|
||||
}
|
||||
|
||||
if err := hcsshim.ActivateLayer(di, layerID); err != nil {
|
||||
return fmt.Errorf("failed to activate layer %s: %w", m.Source, err)
|
||||
}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
if layerErr := hcsshim.DeactivateLayer(di, layerID); layerErr != nil {
|
||||
log.G(context.TODO()).WithError(layerErr).Error("failed to deactivate layer during mount failure cleanup")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := hcsshim.PrepareLayer(di, layerID, parentLayerPaths); err != nil {
|
||||
return fmt.Errorf("failed to prepare layer %s: %w", m.Source, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
if layerErr := hcsshim.UnprepareLayer(di, layerID); layerErr != nil {
|
||||
log.G(context.TODO()).WithError(layerErr).Error("failed to unprepare layer during mount failure cleanup")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
volume, err := hcsshim.GetLayerMountPath(di, layerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get volume path for layer %s: %w", m.Source, err)
|
||||
}
|
||||
|
||||
if len(parentLayerPaths) == 0 {
|
||||
// this is a base layer. It gets mounted without going through WCIFS. We need to mount the Files
|
||||
// folder, not the actual source, or the client may inadvertently remove metadata files.
|
||||
volume = filepath.Join(volume, "Files")
|
||||
if _, err := os.Stat(volume); err != nil {
|
||||
return fmt.Errorf("no Files folder in layer %s", layerID)
|
||||
}
|
||||
}
|
||||
if err := bindfilter.ApplyFileBinding(target, volume, m.ReadOnly()); err != nil {
|
||||
return fmt.Errorf("failed to set volume mount path for layer %s: %w", m.Source, err)
|
||||
}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
if bindErr := bindfilter.RemoveFileBinding(target); bindErr != nil {
|
||||
log.G(context.TODO()).WithError(bindErr).Error("failed to remove binding during mount failure cleanup")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Add an Alternate Data Stream to record the layer source.
|
||||
// See https://docs.microsoft.com/en-au/archive/blogs/askcore/alternate-data-streams-in-ntfs
|
||||
// for details on Alternate Data Streams.
|
||||
if err := os.WriteFile(filepath.Clean(target)+":"+sourceStreamName, []byte(m.Source), 0666); err != nil {
|
||||
return fmt.Errorf("failed to record source for layer %s: %w", m.Source, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParentLayerPathsFlag is the options flag used to represent the JSON encoded
|
||||
// list of parent layers required to use the layer
|
||||
const ParentLayerPathsFlag = "parentLayerPaths="
|
||||
|
||||
// GetParentPaths of the mount
|
||||
func (m *Mount) GetParentPaths() ([]string, error) {
|
||||
var parentLayerPaths []string
|
||||
for _, option := range m.Options {
|
||||
if strings.HasPrefix(option, ParentLayerPathsFlag) {
|
||||
err := json.Unmarshal([]byte(option[len(ParentLayerPathsFlag):]), &parentLayerPaths)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal parent layer paths from mount: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return parentLayerPaths, nil
|
||||
}
|
||||
|
||||
// Unmount the mount at the provided path
|
||||
func Unmount(mount string, flags int) error {
|
||||
mount = filepath.Clean(mount)
|
||||
adsFile := mount + ":" + sourceStreamName
|
||||
var layerPath string
|
||||
|
||||
if _, err := os.Lstat(adsFile); err == nil {
|
||||
layerPathb, err := os.ReadFile(mount + ":" + sourceStreamName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve source for layer %s: %w", mount, err)
|
||||
}
|
||||
layerPath = string(layerPathb)
|
||||
}
|
||||
|
||||
if err := bindfilter.RemoveFileBinding(mount); err != nil {
|
||||
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) || errors.Is(err, windows.ERROR_NOT_FOUND) {
|
||||
// not a mount point
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("removing mount: %w", err)
|
||||
}
|
||||
|
||||
if layerPath != "" {
|
||||
var (
|
||||
home, layerID = filepath.Split(layerPath)
|
||||
di = hcsshim.DriverInfo{
|
||||
HomeDir: home,
|
||||
}
|
||||
)
|
||||
|
||||
if err := hcsshim.UnprepareLayer(di, layerID); err != nil {
|
||||
return fmt.Errorf("failed to unprepare layer %s: %w", mount, err)
|
||||
}
|
||||
|
||||
if err := hcsshim.DeactivateLayer(di, layerID); err != nil {
|
||||
return fmt.Errorf("failed to deactivate layer %s: %w", mount, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmountAll unmounts from the provided path
|
||||
func UnmountAll(mount string, flags int) error {
|
||||
if mount == "" {
|
||||
// This isn't an error, per the EINVAL handling in the Linux version
|
||||
return nil
|
||||
}
|
||||
if _, err := os.Stat(mount); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Unmount(mount, flags)
|
||||
}
|
||||
|
||||
// UnmountRecursive unmounts from the provided path
|
||||
func UnmountRecursive(mount string, flags int) error {
|
||||
return UnmountAll(mount, flags)
|
||||
}
|
||||
23
core/mount/mountinfo.go
Normal file
23
core/mount/mountinfo.go
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import "github.com/moby/sys/mountinfo"
|
||||
|
||||
// Info reveals information about a particular mounted filesystem. This
|
||||
// struct is populated from the content in the /proc/<pid>/mountinfo file.
|
||||
type Info = mountinfo.Info
|
||||
82
core/mount/temp.go
Normal file
82
core/mount/temp.go
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containerd/log"
|
||||
)
|
||||
|
||||
var tempMountLocation = getTempDir()
|
||||
|
||||
// WithTempMount mounts the provided mounts to a temp dir, and pass the temp dir to f.
|
||||
// The mounts are valid during the call to the f.
|
||||
// Finally we will unmount and remove the temp dir regardless of the result of f.
|
||||
func WithTempMount(ctx context.Context, mounts []Mount, f func(root string) error) (err error) {
|
||||
root, uerr := os.MkdirTemp(tempMountLocation, "containerd-mount")
|
||||
if uerr != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %w", uerr)
|
||||
}
|
||||
// We use Remove here instead of RemoveAll.
|
||||
// The RemoveAll will delete the temp dir and all children it contains.
|
||||
// When the Unmount fails, RemoveAll will incorrectly delete data from
|
||||
// the mounted dir. However, if we use Remove, even though we won't
|
||||
// successfully delete the temp dir and it may leak, we won't loss data
|
||||
// from the mounted dir.
|
||||
// For details, please refer to #1868 #1785.
|
||||
defer func() {
|
||||
if uerr = os.Remove(root); uerr != nil {
|
||||
log.G(ctx).WithError(uerr).WithField("dir", root).Error("failed to remove mount temp dir")
|
||||
}
|
||||
}()
|
||||
|
||||
// We should do defer first, if not we will not do Unmount when only a part of Mounts are failed.
|
||||
defer func() {
|
||||
if uerr = UnmountMounts(mounts, root, 0); uerr != nil {
|
||||
uerr = fmt.Errorf("failed to unmount %s: %w", root, uerr)
|
||||
if err == nil {
|
||||
err = uerr
|
||||
} else {
|
||||
err = fmt.Errorf("%s: %w", uerr.Error(), err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if uerr = All(mounts, root); uerr != nil {
|
||||
return fmt.Errorf("failed to mount %s: %w", root, uerr)
|
||||
}
|
||||
if err := f(root); err != nil {
|
||||
return fmt.Errorf("mount callback failed on %s: %w", root, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithReadonlyTempMount mounts the provided mounts to a temp dir as readonly,
|
||||
// and pass the temp dir to f. The mounts are valid during the call to the f.
|
||||
// Finally we will unmount and remove the temp dir regardless of the result of f.
|
||||
func WithReadonlyTempMount(ctx context.Context, mounts []Mount, f func(root string) error) (err error) {
|
||||
return WithTempMount(ctx, readonlyMounts(mounts), f)
|
||||
}
|
||||
|
||||
func getTempDir() string {
|
||||
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return os.TempDir()
|
||||
}
|
||||
60
core/mount/temp_unix.go
Normal file
60
core/mount/temp_unix.go
Normal file
@@ -0,0 +1,60 @@
|
||||
//go:build !windows && !darwin
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
)
|
||||
|
||||
// SetTempMountLocation sets the temporary mount location
|
||||
func SetTempMountLocation(root string) error {
|
||||
err := os.MkdirAll(root, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We need to pass canonicalized path to mountinfo.PrefixFilter in CleanupTempMounts
|
||||
tempMountLocation, err = CanonicalizePath(root)
|
||||
return err
|
||||
}
|
||||
|
||||
// CleanupTempMounts all temp mounts and remove the directories
|
||||
func CleanupTempMounts(flags int) (warnings []error, err error) {
|
||||
mounts, err := mountinfo.GetMounts(mountinfo.PrefixFilter(tempMountLocation))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make the deepest mount be first
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return len(mounts[i].Mountpoint) > len(mounts[j].Mountpoint)
|
||||
})
|
||||
for _, mount := range mounts {
|
||||
if err := UnmountAll(mount.Mountpoint, flags); err != nil {
|
||||
warnings = append(warnings, err)
|
||||
continue
|
||||
}
|
||||
if err := os.Remove(mount.Mountpoint); err != nil {
|
||||
warnings = append(warnings, err)
|
||||
}
|
||||
}
|
||||
return warnings, nil
|
||||
}
|
||||
29
core/mount/temp_unsupported.go
Normal file
29
core/mount/temp_unsupported.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build windows || darwin
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
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 mount
|
||||
|
||||
// SetTempMountLocation sets the temporary mount location
|
||||
func SetTempMountLocation(root string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupTempMounts all temp mounts and remove the directories
|
||||
func CleanupTempMounts(flags int) ([]error, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user