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"
|
"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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,24 +95,31 @@ func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
|
|||||||
}
|
}
|
||||||
case f.Name() == "console":
|
case f.Name() == "console":
|
||||||
continue
|
continue
|
||||||
}
|
default:
|
||||||
device, err := DeviceFromPath(filepath.Join(path, f.Name()))
|
device, err := DeviceFromPath(filepath.Join(path, f.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == ErrNotADevice {
|
if err == ErrNotADevice {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
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 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if os.IsNotExist(err) {
|
if containerPath != "" {
|
||||||
continue
|
device.Path = filepath.Join(containerPath, filepath.Base(f.Name()))
|
||||||
}
|
}
|
||||||
return nil, err
|
out = append(out, *device)
|
||||||
}
|
}
|
||||||
if device.Type == fifoDevice {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if containerPath != "" {
|
|
||||||
device.Path = filepath.Join(containerPath, filepath.Base(f.Name()))
|
|
||||||
}
|
|
||||||
out = append(out, *device)
|
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@ -114,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