OCI: Mount (accessible) host devices in privileged rootless containers
Allow rootless containers with privileged to mount devices that are accessible (ignore permission errors in rootless mode). This patch updates oci.getDevices() to ignore access denied errors on sub- directories and files within the given path if the container is running with userns enabled. Note that these errors are _only_ ignored on paths _under_ the specified path, and not the path itself, so if `HostDevices()` is used, and `/dev` itself is not accessible, or `WithDevices()` is used to specify a device that is not accessible, an error is still produced. Tests were added, which includes a temporary workaround for compatibility with Go 1.16 (we could decide to skip these tests on Go 1.16 instead). To verify the patch in a container: docker run --rm -v $(pwd):/go/src/github.com/containerd/containerd -w /go/src/github.com/containerd/containerd golang:1.17 sh -c 'go test -v -run TestHostDevices ./oci' === RUN TestHostDevicesOSReadDirFailure --- PASS: TestHostDevicesOSReadDirFailure (0.00s) === RUN TestHostDevicesOSReadDirFailureInUserNS --- PASS: TestHostDevicesOSReadDirFailureInUserNS (0.00s) === RUN TestHostDevicesDeviceFromPathFailure --- PASS: TestHostDevicesDeviceFromPathFailure (0.00s) === RUN TestHostDevicesDeviceFromPathFailureInUserNS --- PASS: TestHostDevicesDeviceFromPathFailureInUserNS (0.00s) === RUN TestHostDevicesAllValid --- PASS: TestHostDevicesAllValid (0.00s) PASS ok github.com/containerd/containerd/oci 0.006s Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
@@ -19,7 +19,128 @@
|
||||
|
||||
package oci
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/containerd/containerd/pkg/userns"
|
||||
)
|
||||
|
||||
func cleanupTest() {
|
||||
overrideDeviceFromPath = nil
|
||||
osReadDir = os.ReadDir
|
||||
usernsRunningInUserNS = userns.RunningInUserNS
|
||||
}
|
||||
|
||||
// Based on test from runc:
|
||||
// https://github.com/opencontainers/runc/blob/v1.0.0/libcontainer/devices/device_unix_test.go#L34-L47
|
||||
func TestHostDevicesOSReadDirFailure(t *testing.T) {
|
||||
testError := fmt.Errorf("test error: %w", os.ErrPermission)
|
||||
|
||||
// Override os.ReadDir to inject error.
|
||||
osReadDir = func(dirname string) ([]os.DirEntry, error) {
|
||||
return nil, testError
|
||||
}
|
||||
|
||||
// Override userns.RunningInUserNS to ensure not running in user namespace.
|
||||
usernsRunningInUserNS = func() bool {
|
||||
return false
|
||||
}
|
||||
defer cleanupTest()
|
||||
|
||||
_, err := HostDevices()
|
||||
if !errors.Is(err, testError) {
|
||||
t.Fatalf("Unexpected error %v, expected %v", err, testError)
|
||||
}
|
||||
}
|
||||
|
||||
// Based on test from runc:
|
||||
// https://github.com/opencontainers/runc/blob/v1.0.0/libcontainer/devices/device_unix_test.go#L34-L47
|
||||
func TestHostDevicesOSReadDirFailureInUserNS(t *testing.T) {
|
||||
testError := fmt.Errorf("test error: %w", os.ErrPermission)
|
||||
|
||||
// Override os.ReadDir to inject error.
|
||||
osReadDir = func(dirname string) ([]os.DirEntry, error) {
|
||||
if dirname == "/dev" {
|
||||
fi, err := os.Lstat("/dev/null")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
return []os.DirEntry{fileInfoToDirEntry(fi)}, nil
|
||||
}
|
||||
return nil, testError
|
||||
}
|
||||
// Override userns.RunningInUserNS to ensure running in user namespace.
|
||||
usernsRunningInUserNS = func() bool {
|
||||
return true
|
||||
}
|
||||
defer cleanupTest()
|
||||
|
||||
_, err := HostDevices()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("Unexpected error %v, expected %v", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Based on test from runc:
|
||||
// https://github.com/opencontainers/runc/blob/v1.0.0/libcontainer/devices/device_unix_test.go#L49-L74
|
||||
func TestHostDevicesDeviceFromPathFailure(t *testing.T) {
|
||||
testError := fmt.Errorf("test error: %w", os.ErrPermission)
|
||||
|
||||
// Override DeviceFromPath to produce an os.ErrPermission on /dev/null.
|
||||
overrideDeviceFromPath = func(path string) error {
|
||||
if path == "/dev/null" {
|
||||
return testError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Override userns.RunningInUserNS to ensure not running in user namespace.
|
||||
usernsRunningInUserNS = func() bool {
|
||||
return false
|
||||
}
|
||||
defer cleanupTest()
|
||||
|
||||
d, err := HostDevices()
|
||||
if !errors.Is(err, testError) {
|
||||
t.Fatalf("Unexpected error %v, expected %v", err, testError)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(d))
|
||||
}
|
||||
|
||||
// Based on test from runc:
|
||||
// https://github.com/opencontainers/runc/blob/v1.0.0/libcontainer/devices/device_unix_test.go#L49-L74
|
||||
func TestHostDevicesDeviceFromPathFailureInUserNS(t *testing.T) {
|
||||
testError := fmt.Errorf("test error: %w", os.ErrPermission)
|
||||
|
||||
// Override DeviceFromPath to produce an os.ErrPermission on all devices,
|
||||
// except for /dev/null.
|
||||
overrideDeviceFromPath = func(path string) error {
|
||||
if path == "/dev/null" {
|
||||
return nil
|
||||
}
|
||||
return testError
|
||||
}
|
||||
|
||||
// Override userns.RunningInUserNS to ensure running in user namespace.
|
||||
usernsRunningInUserNS = func() bool {
|
||||
return true
|
||||
}
|
||||
defer cleanupTest()
|
||||
|
||||
d, err := HostDevices()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("Unexpected error %v, expected %v", err, nil)
|
||||
}
|
||||
assert.Equal(t, 1, len(d))
|
||||
assert.Equal(t, d[0].Path, "/dev/null")
|
||||
}
|
||||
|
||||
func TestHostDevicesAllValid(t *testing.T) {
|
||||
devices, err := HostDevices()
|
||||
|
Reference in New Issue
Block a user