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:
parent
7f70ff9672
commit
b7f673790f
@ -23,6 +23,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/pkg/userns"
|
||||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
@ -31,6 +32,13 @@ import (
|
|||||||
// ErrNotADevice denotes that a file is not a valid linux device.
|
// ErrNotADevice denotes that a file is not a valid linux device.
|
||||||
var ErrNotADevice = errors.New("not a device node")
|
var ErrNotADevice = errors.New("not a device node")
|
||||||
|
|
||||||
|
// Testing dependencies
|
||||||
|
var (
|
||||||
|
osReadDir = os.ReadDir
|
||||||
|
usernsRunningInUserNS = userns.RunningInUserNS
|
||||||
|
overrideDeviceFromPath func(path string) error
|
||||||
|
)
|
||||||
|
|
||||||
// HostDevices returns all devices that can be found under /dev directory.
|
// HostDevices returns all devices that can be found under /dev directory.
|
||||||
func HostDevices() ([]specs.LinuxDevice, error) {
|
func HostDevices() ([]specs.LinuxDevice, error) {
|
||||||
return getDevices("/dev", "")
|
return getDevices("/dev", "")
|
||||||
@ -53,7 +61,7 @@ func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
|
|||||||
return []specs.LinuxDevice{*dev}, nil
|
return []specs.LinuxDevice{*dev}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := os.ReadDir(path)
|
files, err := osReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -73,6 +81,12 @@ func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
|
|||||||
}
|
}
|
||||||
sub, err := getDevices(filepath.Join(path, f.Name()), cp)
|
sub, err := getDevices(filepath.Join(path, f.Name()), cp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrPermission) && usernsRunningInUserNS() {
|
||||||
|
// ignore the "permission denied" error if running in userns.
|
||||||
|
// This allows rootless containers to use devices that are
|
||||||
|
// accessible, ignoring devices / subdirectories that are not.
|
||||||
|
continue
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +104,12 @@ func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
|
|||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, os.ErrPermission) && usernsRunningInUserNS() {
|
||||||
|
// ignore the "permission denied" error if running in userns.
|
||||||
|
// This allows rootless containers to use devices that are
|
||||||
|
// accessible, ignoring devices that are not.
|
||||||
|
continue
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if device.Type == fifoDevice {
|
if device.Type == fifoDevice {
|
||||||
@ -115,6 +135,12 @@ const (
|
|||||||
// DeviceFromPath takes the path to a device to look up the information about a
|
// DeviceFromPath takes the path to a device to look up the information about a
|
||||||
// linux device and returns that information as a LinuxDevice struct.
|
// linux device and returns that information as a LinuxDevice struct.
|
||||||
func DeviceFromPath(path string) (*specs.LinuxDevice, error) {
|
func DeviceFromPath(path string) (*specs.LinuxDevice, error) {
|
||||||
|
if overrideDeviceFromPath != nil {
|
||||||
|
if err := overrideDeviceFromPath(path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var stat unix.Stat_t
|
var stat unix.Stat_t
|
||||||
if err := unix.Lstat(path, &stat); err != nil {
|
if err := unix.Lstat(path, &stat); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
55
oci/utils_unix_go116_test.go
Normal file
55
oci/utils_unix_go116_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
//go:build !go1.17 && !windows && !darwin
|
||||||
|
// +build !go1.17,!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 oci
|
||||||
|
|
||||||
|
import "io/fs"
|
||||||
|
|
||||||
|
// The following code is adapted from go1.17.1/src/io/fs/readdir.go
|
||||||
|
// to compensate for the lack of fs.FileInfoToDirEntry in Go 1.16.
|
||||||
|
|
||||||
|
// dirInfo is a DirEntry based on a FileInfo.
|
||||||
|
type dirInfo struct {
|
||||||
|
fileInfo fs.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (di dirInfo) IsDir() bool {
|
||||||
|
return di.fileInfo.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (di dirInfo) Type() fs.FileMode {
|
||||||
|
return di.fileInfo.Mode().Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (di dirInfo) Info() (fs.FileInfo, error) {
|
||||||
|
return di.fileInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (di dirInfo) Name() string {
|
||||||
|
return di.fileInfo.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileInfoToDirEntry returns a DirEntry that returns information from info.
|
||||||
|
// If info is nil, FileInfoToDirEntry returns nil.
|
||||||
|
func fileInfoToDirEntry(info fs.FileInfo) fs.DirEntry {
|
||||||
|
if info == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dirInfo{fileInfo: info}
|
||||||
|
}
|
24
oci/utils_unix_go117_test.go
Normal file
24
oci/utils_unix_go117_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//go:build go1.17 && !windows && !darwin
|
||||||
|
// +build go1.17,!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 oci
|
||||||
|
|
||||||
|
import "io/fs"
|
||||||
|
|
||||||
|
var fileInfoToDirEntry = fs.FileInfoToDirEntry
|
@ -19,7 +19,128 @@
|
|||||||
|
|
||||||
package oci
|
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) {
|
func TestHostDevicesAllValid(t *testing.T) {
|
||||||
devices, err := HostDevices()
|
devices, err := HostDevices()
|
||||||
|
Loading…
Reference in New Issue
Block a user