Merge pull request #6308 from thaJeztah/rootless_devices
OCI: Mount (accessible) host devices in privileged rootless containers
This commit is contained in:
commit
89437597ac
@ -23,6 +23,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containerd/containerd/pkg/userns"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
@ -31,6 +32,13 @@ import (
|
||||
// ErrNotADevice denotes that a file is not a valid linux device.
|
||||
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.
|
||||
func HostDevices() ([]specs.LinuxDevice, error) {
|
||||
return getDevices("/dev", "")
|
||||
@ -53,7 +61,7 @@ func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
|
||||
return []specs.LinuxDevice{*dev}, nil
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(path)
|
||||
files, err := osReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -73,6 +81,12 @@ func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
|
||||
}
|
||||
sub, err := getDevices(filepath.Join(path, f.Name()), cp)
|
||||
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
|
||||
}
|
||||
|
||||
@ -81,7 +95,7 @@ func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
|
||||
}
|
||||
case f.Name() == "console":
|
||||
continue
|
||||
}
|
||||
default:
|
||||
device, err := DeviceFromPath(filepath.Join(path, f.Name()))
|
||||
if err != nil {
|
||||
if err == ErrNotADevice {
|
||||
@ -90,6 +104,12 @@ func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
|
||||
if os.IsNotExist(err) {
|
||||
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
|
||||
}
|
||||
if device.Type == fifoDevice {
|
||||
@ -100,6 +120,7 @@ func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
|
||||
}
|
||||
out = append(out, *device)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@ -114,6 +135,12 @@ const (
|
||||
// DeviceFromPath takes the path to a device to look up the information about a
|
||||
// linux device and returns that information as a LinuxDevice struct.
|
||||
func DeviceFromPath(path string) (*specs.LinuxDevice, error) {
|
||||
if overrideDeviceFromPath != nil {
|
||||
if err := overrideDeviceFromPath(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var stat unix.Stat_t
|
||||
if err := unix.Lstat(path, &stat); err != nil {
|
||||
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
|
||||
|
||||
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()
|
||||
|
Loading…
Reference in New Issue
Block a user