Files
podman/pkg/util/utils_linux.go
Paul Holzinger d492b08442 [v4.6.1-rhel] inspect: ignore ENOENT during device lookup
When we walk the /dev tree we need to lookup all device paths. Now in
order to get the major and minor version we have to actually stat each
device. This can again fail of course. There is at least a race between
the readdir at stat call so it must ignore ENOENT errors to avoid
the race condition as this is not a user problem. Second, we should
also not return other errors and just log them instead, returning an
error means stopping the walk and returning early which means inspect
fails with an error which would be bad.

Also there seems to be cases were ENOENT will be returned all the time,
e.g. when a device is forcefully removed. In the reported bug this is
triggered with iSCSI devices.

Because the caller does already lookup the device from the created map
it reports a warning there if the device is missing on the host so it
is not a problem to ignore a error during lookup here.

[NO NEW TESTS NEEDED] Requires special device setup to trigger
consistentlyand we cannot do that in CI.

Original Fixed https://issues.redhat.com/browse/RHEL-11158
This fixes: https://issues.redhat.com/browse/RHEL-20491

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
Signed-off-by: tomsweeneyredhat <tsweeney@redhat.com>
2024-04-05 16:53:22 -04:00

257 lines
7.3 KiB
Go

package util
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/rootless"
"github.com/containers/psgo"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
var (
errNotADevice = errors.New("not a device node")
)
// GetContainerPidInformationDescriptors returns a string slice of all supported
// format descriptors of GetContainerPidInformation.
func GetContainerPidInformationDescriptors() ([]string, error) {
return psgo.ListDescriptors(), nil
}
// FindDeviceNodes parses /dev/ into a set of major:minor -> path, where
// [major:minor] is the device's major and minor numbers formatted as, for
// example, 2:0 and path is the path to the device node.
// Symlinks to nodes are ignored.
func FindDeviceNodes() (map[string]string, error) {
nodes := make(map[string]string)
err := filepath.WalkDir("/dev", func(path string, d fs.DirEntry, err error) error {
if err != nil {
logrus.Warnf("Error descending into path %s: %v", path, err)
return filepath.SkipDir
}
// If we aren't a device node, do nothing.
if d.Type()&(os.ModeDevice|os.ModeCharDevice) == 0 {
return nil
}
info, err := d.Info()
if err != nil {
// Info() can return ErrNotExist if the file was deleted between the readdir and stat call.
// This race can happen and is no reason to log an ugly error. If this is a container device
// that is used the code later will print a proper error in such case.
// There also seem to be cases were ErrNotExist is always returned likely due a weird device
// state, e.g. removing a device forcefully. This can happen with iSCSI devices.
if !errors.Is(err, fs.ErrNotExist) {
logrus.Errorf("Failed to get device information for %s: %v", path, err)
}
// return nil here as we want to continue looking for more device and not stop the WalkDir()
return nil
}
// We are a device node. Get major/minor.
sysstat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return errors.New("could not convert stat output for use")
}
// We must typeconvert sysstat.Rdev from uint64->int to avoid constant overflow
rdev := int(sysstat.Rdev)
major := ((rdev >> 8) & 0xfff) | ((rdev >> 32) & ^0xfff)
minor := (rdev & 0xff) | ((rdev >> 12) & ^0xff)
nodes[fmt.Sprintf("%d:%d", major, minor)] = path
return nil
})
if err != nil {
return nil, err
}
return nodes, nil
}
// isVirtualConsoleDevice returns true if path is a virtual console device
// (/dev/tty\d+).
// The passed path must be clean (filepath.Clean).
func isVirtualConsoleDevice(path string) bool {
/*
Virtual consoles are of the form `/dev/tty\d+`, any other device such as
/dev/tty, ttyUSB0, or ttyACM0 should not be matched.
See `man 4 console` for more information.
*/
suffix := strings.TrimPrefix(path, "/dev/tty")
if suffix == path || suffix == "" {
return false
}
// 16bit because, max. supported TTY devices is 512 in Linux 6.1.5.
_, err := strconv.ParseUint(suffix, 10, 16)
return err == nil
}
func AddPrivilegedDevices(g *generate.Generator, systemdMode bool) error {
hostDevices, err := getDevices("/dev")
if err != nil {
return err
}
g.ClearLinuxDevices()
if rootless.IsRootless() {
mounts := make(map[string]interface{})
for _, m := range g.Mounts() {
mounts[m.Destination] = true
}
newMounts := []spec.Mount{}
for _, d := range hostDevices {
devMnt := spec.Mount{
Destination: d.Path,
Type: define.TypeBind,
Source: d.Path,
Options: []string{"slave", "nosuid", "noexec", "rw", "rbind"},
}
/* The following devices should not be mounted in rootless containers:
*
* /dev/ptmx: The host-provided /dev/ptmx should not be shared to
* the rootless containers for security reasons, and
* the container runtime will create it for us
* anyway (ln -s /dev/pts/ptmx /dev/ptmx);
* /dev/tty and
* /dev/tty[0-9]+: Prevent the container from taking over the host's
* virtual consoles, even when not in systemd mode
* for backwards compatibility.
*/
if d.Path == "/dev/ptmx" || d.Path == "/dev/tty" || isVirtualConsoleDevice(d.Path) {
continue
}
if _, found := mounts[d.Path]; found {
continue
}
newMounts = append(newMounts, devMnt)
}
g.Config.Mounts = append(newMounts, g.Config.Mounts...)
if g.Config.Linux.Resources != nil {
g.Config.Linux.Resources.Devices = nil
}
} else {
for _, d := range hostDevices {
/* Restrict access to the virtual consoles *only* when running
* in systemd mode to improve backwards compatibility. See
* https://github.com/containers/podman/issues/15878.
*
* NOTE: May need revisiting in the future to drop the systemd
* condition if more use cases end up breaking the virtual terminals
* of people who specifically disable the systemd mode. It would
* also provide a more consistent behaviour between rootless and
* rootfull containers.
*/
if systemdMode && isVirtualConsoleDevice(d.Path) {
continue
}
g.AddDevice(d)
}
// Add resources device - need to clear the existing one first.
if g.Config.Linux.Resources != nil {
g.Config.Linux.Resources.Devices = nil
}
g.AddLinuxResourcesDevice(true, "", nil, nil, "rwm")
}
return nil
}
// based on getDevices from runc (libcontainer/devices/devices.go)
func getDevices(path string) ([]spec.LinuxDevice, error) {
files, err := os.ReadDir(path)
if err != nil {
if rootless.IsRootless() && os.IsPermission(err) {
return nil, nil
}
return nil, err
}
out := []spec.LinuxDevice{}
for _, f := range files {
switch {
case f.IsDir():
switch f.Name() {
// ".lxc" & ".lxd-mounts" added to address https://github.com/lxc/lxd/issues/2825
case "pts", "shm", "fd", "mqueue", ".lxc", ".lxd-mounts":
continue
default:
sub, err := getDevices(filepath.Join(path, f.Name()))
if err != nil {
return nil, err
}
if sub != nil {
out = append(out, sub...)
}
continue
}
case f.Name() == "console":
continue
case f.Type()&os.ModeSymlink != 0:
continue
}
device, err := DeviceFromPath(filepath.Join(path, f.Name()))
if err != nil {
if err == errNotADevice {
continue
}
if os.IsNotExist(err) {
continue
}
return nil, err
}
out = append(out, *device)
}
return out, nil
}
// Copied from github.com/opencontainers/runc/libcontainer/devices
// Given the path to a device look up the information about a linux device
func DeviceFromPath(path string) (*spec.LinuxDevice, error) {
var stat unix.Stat_t
err := unix.Lstat(path, &stat)
if err != nil {
return nil, err
}
var (
devType string
mode = stat.Mode
devNumber = uint64(stat.Rdev) //nolint: unconvert
m = os.FileMode(mode)
)
switch {
case mode&unix.S_IFBLK == unix.S_IFBLK:
devType = "b"
case mode&unix.S_IFCHR == unix.S_IFCHR:
devType = "c"
case mode&unix.S_IFIFO == unix.S_IFIFO:
devType = "p"
default:
return nil, errNotADevice
}
return &spec.LinuxDevice{
Type: devType,
Path: path,
FileMode: &m,
UID: &stat.Uid,
GID: &stat.Gid,
Major: int64(unix.Major(devNumber)),
Minor: int64(unix.Minor(devNumber)),
}, nil
}