Merge pull request #6308 from thaJeztah/rootless_devices

OCI: Mount (accessible) host devices in privileged rootless containers
This commit is contained in:
Michael Crosby 2021-12-14 15:21:31 -05:00 committed by GitHub
commit 89437597ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 243 additions and 16 deletions

View File

@ -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

View 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}
}

View 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

View File

@ -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()