diff --git a/Makefile b/Makefile index 9f15ee9..0ec5418 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PACKAGES ?= atomicwriter capability mountinfo mount reexec sequential signal symlink user userns +PACKAGES ?= atomicwriter capability devices mountinfo mount reexec sequential signal symlink user userns CROSS ?= linux/arm linux/arm64 linux/ppc64le linux/s390x \ freebsd/amd64 openbsd/amd64 darwin/amd64 darwin/arm64 windows/amd64 SUDO ?= sudo -n diff --git a/devices/device_unix.go b/devices/device_unix.go new file mode 100644 index 0000000..bd401d3 --- /dev/null +++ b/devices/device_unix.go @@ -0,0 +1,133 @@ +//go:build !windows + +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2015-2026 Open Containers Initiative Contributors + * + * 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. + */ + +// This code originally comes from runc and was taken from this tree: +// . + +package devices + +import ( + "errors" + "os" + "path/filepath" + + "github.com/opencontainers/cgroups/devices/config" + "golang.org/x/sys/unix" +) + +// ErrNotADevice denotes that a file is not a valid linux device. +var ErrNotADevice = errors.New("not a device node") + +// Testing dependencies +var ( + unixLstat = unix.Lstat + osReadDir = os.ReadDir +) + +// DeviceFromPath takes the path to a device and its cgroup_permissions (which +// cannot be easily queried) to look up the information about a linux device +// and returns that information as a Device struct. +func DeviceFromPath(path, permissions string) (*config.Device, error) { + var stat unix.Stat_t + err := unixLstat(path, &stat) + if err != nil { + return nil, err + } + + var ( + devType config.Type + mode = stat.Mode + devNumber = uint64(stat.Rdev) //nolint:unconvert // Rdev is uint32 on e.g. MIPS. + major = unix.Major(devNumber) + minor = unix.Minor(devNumber) + ) + switch mode & unix.S_IFMT { + case unix.S_IFBLK: + devType = config.BlockDevice + case unix.S_IFCHR: + devType = config.CharDevice + case unix.S_IFIFO: + devType = config.FifoDevice + default: + return nil, ErrNotADevice + } + return &config.Device{ + Rule: config.Rule{ + Type: devType, + Major: int64(major), + Minor: int64(minor), + Permissions: config.Permissions(permissions), + }, + Path: path, + FileMode: os.FileMode(mode &^ unix.S_IFMT), + Uid: stat.Uid, + Gid: stat.Gid, + }, nil +} + +// HostDevices returns all devices that can be found under /dev directory. +func HostDevices() ([]*config.Device, error) { + return GetDevices("/dev") +} + +// GetDevices recursively traverses a directory specified by path +// and returns all devices found there. +func GetDevices(path string) ([]*config.Device, error) { + files, err := osReadDir(path) + if err != nil { + return nil, err + } + var out []*config.Device + for _, f := range files { + switch { + case f.IsDir(): + switch f.Name() { + // ".lxc" & ".lxd-mounts" added to address https://github.com/lxc/lxd/issues/2825 + // ".udev" added to address https://github.com/opencontainers/runc/issues/2093 + case "pts", "shm", "fd", "mqueue", ".lxc", ".lxd-mounts", ".udev": + continue + default: + sub, err := GetDevices(filepath.Join(path, f.Name())) + if err != nil { + return nil, err + } + + out = append(out, sub...) + continue + } + case f.Name() == "console": + continue + } + device, err := DeviceFromPath(filepath.Join(path, f.Name()), "rwm") + if err != nil { + if errors.Is(err, ErrNotADevice) { + continue + } + if errors.Is(err, os.ErrNotExist) { + continue + } + return nil, err + } + if device.Type == config.FifoDevice { + continue + } + out = append(out, device) + } + return out, nil +} diff --git a/devices/device_unix_test.go b/devices/device_unix_test.go new file mode 100644 index 0000000..262671c --- /dev/null +++ b/devices/device_unix_test.go @@ -0,0 +1,122 @@ +//go:build !windows + +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2015-2026 Open Containers Initiative Contributors + * + * 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. + */ + +// This code originally comes from runc and was taken from this tree: +// . + +package devices + +import ( + "errors" + "io/fs" + "os" + "runtime" + "testing" + + "github.com/opencontainers/cgroups/devices/config" + "golang.org/x/sys/unix" +) + +func cleanupTest() { + unixLstat = unix.Lstat + osReadDir = os.ReadDir +} + +func TestDeviceFromPathLstatFailure(t *testing.T) { + testError := errors.New("test error") + + // Override unix.Lstat to inject error. + unixLstat = func(path string, stat *unix.Stat_t) error { + return testError + } + defer cleanupTest() + + _, err := DeviceFromPath("", "") + if !errors.Is(err, testError) { + t.Fatalf("Unexpected error %v, expected %v", err, testError) + } +} + +func TestHostDevicesIoutilReadDirFailure(t *testing.T) { + testError := errors.New("test error") + + // Override os.ReadDir to inject error. + osReadDir = func(dirname string) ([]fs.DirEntry, error) { + return nil, testError + } + defer cleanupTest() + + _, err := HostDevices() + if !errors.Is(err, testError) { + t.Fatalf("Unexpected error %v, expected %v", err, testError) + } +} + +func TestHostDevicesIoutilReadDirDeepFailure(t *testing.T) { + testError := errors.New("test error") + called := false + + // Override os.ReadDir to inject error after the first call. + osReadDir = func(dirname string) ([]fs.DirEntry, error) { + if called { + return nil, testError + } + called = true + + // Provoke a second call. + fi, err := os.Stat("/tmp") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + return []fs.DirEntry{fs.FileInfoToDirEntry(fi)}, nil + } + defer cleanupTest() + + _, err := HostDevices() + if !errors.Is(err, testError) { + t.Fatalf("Unexpected error %v, expected %v", err, testError) + } +} + +func TestHostDevicesAllValid(t *testing.T) { + devices, err := HostDevices() + if err != nil { + t.Fatalf("failed to get host devices: %v", err) + } + + for _, device := range devices { + // Devices can't have major number 0 on Linux. + if device.Major == 0 { + logFn := t.Logf + if runtime.GOOS == "linux" { + logFn = t.Errorf + } + logFn("device entry %+v has zero major number", device) + } + switch device.Type { + case config.BlockDevice, config.CharDevice: + case config.FifoDevice: + t.Logf("fifo devices shouldn't show up from HostDevices") + fallthrough + default: + t.Errorf("device entry %+v has unexpected type %v", device, device.Type) + } + } +} diff --git a/devices/doc.go b/devices/doc.go new file mode 100644 index 0000000..a590f44 --- /dev/null +++ b/devices/doc.go @@ -0,0 +1,4 @@ +// Package devices provides some helper functions for constructing device +// configurations for runc. These are exclusively used by higher-level runtimes +// that need to configure runc's device list based on existing devices. +package devices diff --git a/devices/go.mod b/devices/go.mod new file mode 100644 index 0000000..456f444 --- /dev/null +++ b/devices/go.mod @@ -0,0 +1,8 @@ +module github.com/moby/sys/devices + +go 1.24 + +require ( + github.com/opencontainers/cgroups v0.0.6 + golang.org/x/sys v0.30.0 +) diff --git a/devices/go.sum b/devices/go.sum new file mode 100644 index 0000000..1e88a27 --- /dev/null +++ b/devices/go.sum @@ -0,0 +1,4 @@ +github.com/opencontainers/cgroups v0.0.6 h1:tfZFWTIIGaUUFImTyuTg+Mr5x8XRiSdZESgEBW7UxuI= +github.com/opencontainers/cgroups v0.0.6/go.mod h1:oWVzJsKK0gG9SCRBfTpnn16WcGEqDI8PAcpMGbqWxcs= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=