diff --git a/pkg/specgen/generate/config_common.go b/pkg/specgen/generate/config_common.go new file mode 100644 index 0000000000..5a73ca4c9b --- /dev/null +++ b/pkg/specgen/generate/config_common.go @@ -0,0 +1,64 @@ +//go:build linux || ignore || freebsd +// +build linux ignore freebsd + +package generate + +import ( + "fmt" + "strings" +) + +// ParseDevice parses device mapping string to a src, dest & permissions string +func ParseDevice(device string) (string, string, string, error) { + var src string + var dst string + permissions := "rwm" + arr := strings.Split(device, ":") + switch len(arr) { + case 3: + if !IsValidDeviceMode(arr[2]) { + return "", "", "", fmt.Errorf("invalid device mode: %s", arr[2]) + } + permissions = arr[2] + fallthrough + case 2: + if IsValidDeviceMode(arr[1]) { + permissions = arr[1] + } else { + if len(arr[1]) > 0 && arr[1][0] != '/' { + return "", "", "", fmt.Errorf("invalid device mode: %s", arr[1]) + } + dst = arr[1] + } + fallthrough + case 1: + src = arr[0] + default: + return "", "", "", fmt.Errorf("invalid device specification: %s", device) + } + + if dst == "" { + dst = src + } + return src, dst, permissions, nil +} + +// IsValidDeviceMode checks if the mode for device is valid or not. +// IsValid mode is a composition of r (read), w (write), and m (mknod). +func IsValidDeviceMode(mode string) bool { + var legalDeviceMode = map[rune]bool{ + 'r': true, + 'w': true, + 'm': true, + } + if mode == "" { + return false + } + for _, c := range mode { + if !legalDeviceMode[c] { + return false + } + legalDeviceMode[c] = false + } + return true +} diff --git a/pkg/specgen/generate/config_common_test.go b/pkg/specgen/generate/config_common_test.go new file mode 100644 index 0000000000..b93577cf85 --- /dev/null +++ b/pkg/specgen/generate/config_common_test.go @@ -0,0 +1,29 @@ +package generate + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseDevice(t *testing.T) { + tests := []struct { + device string + src string + dst string + perm string + }{ + {"/dev/foo", "/dev/foo", "/dev/foo", "rwm"}, + {"/dev/foo:/dev/bar", "/dev/foo", "/dev/bar", "rwm"}, + {"/dev/foo:/dev/bar:rw", "/dev/foo", "/dev/bar", "rw"}, + {"/dev/foo:rw", "/dev/foo", "/dev/foo", "rw"}, + {"/dev/foo::rw", "/dev/foo", "/dev/foo", "rw"}, + } + for _, test := range tests { + src, dst, perm, err := ParseDevice(test.device) + assert.NoError(t, err) + assert.Equal(t, src, test.src) + assert.Equal(t, dst, test.dst) + assert.Equal(t, perm, test.perm) + } +} diff --git a/pkg/specgen/generate/config_freebsd.go b/pkg/specgen/generate/config_freebsd.go new file mode 100644 index 0000000000..67a06394e4 --- /dev/null +++ b/pkg/specgen/generate/config_freebsd.go @@ -0,0 +1,118 @@ +package generate + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi" + "github.com/opencontainers/runtime-tools/generate" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +// DevicesFromPath computes a list of devices +func DevicesFromPath(g *generate.Generator, devicePath string) error { + if isCDIDevice(devicePath) { + registry := cdi.GetRegistry( + cdi.WithAutoRefresh(false), + ) + if err := registry.Refresh(); err != nil { + logrus.Debugf("The following error was triggered when refreshing the CDI registry: %v", err) + } + _, err := registry.InjectDevices(g.Config, devicePath) + if err != nil { + return fmt.Errorf("setting up CDI devices: %w", err) + } + return nil + } + devs := strings.Split(devicePath, ":") + resolvedDevicePath := devs[0] + // check if it is a symbolic link + if src, err := os.Lstat(resolvedDevicePath); err == nil && src.Mode()&os.ModeSymlink == os.ModeSymlink { + if linkedPathOnHost, err := filepath.EvalSymlinks(resolvedDevicePath); err == nil { + resolvedDevicePath = linkedPathOnHost + } + } + st, err := os.Stat(resolvedDevicePath) + if err != nil { + return err + } + if st.IsDir() { + // For devfs, we need to add the directory as well + addDevice(g, resolvedDevicePath) + + found := false + src := resolvedDevicePath + dest := src + var devmode string + if len(devs) > 1 { + if len(devs[1]) > 0 && devs[1][0] == '/' { + dest = devs[1] + } else { + devmode = devs[1] + } + } + if len(devs) > 2 { + if devmode != "" { + return fmt.Errorf("invalid device specification %s: %w", devicePath, unix.EINVAL) + } + devmode = devs[2] + } + + // mount the internal devices recursively + if err := filepath.WalkDir(resolvedDevicePath, func(dpath string, d fs.DirEntry, e error) error { + if d.Type()&os.ModeDevice == os.ModeDevice { + found = true + device := fmt.Sprintf("%s:%s", dpath, filepath.Join(dest, strings.TrimPrefix(dpath, src))) + if devmode != "" { + device = fmt.Sprintf("%s:%s", device, devmode) + } + if err := addDevice(g, device); err != nil { + return fmt.Errorf("failed to add %s device: %w", dpath, err) + } + } + return nil + }); err != nil { + return err + } + if !found { + return fmt.Errorf("no devices found in %s: %w", devicePath, unix.EINVAL) + } + return nil + } + return addDevice(g, strings.Join(append([]string{resolvedDevicePath}, devs[1:]...), ":")) +} + +func addDevice(g *generate.Generator, device string) error { + src, dst, permissions, err := ParseDevice(device) + if err != nil { + return err + } + if src != dst { + return fmt.Errorf("container device must be the same as host device on FreeBSD") + } + mode := 0 + if strings.Contains(permissions, "r") { + mode |= unix.S_IRUSR + } + if strings.Contains(permissions, "w") { + mode |= unix.S_IWUSR + } + // Find the devfs mount so that we can add rules to expose the device + for k, m := range g.Config.Mounts { + if m.Type == "devfs" { + if dev, ok := strings.CutPrefix(src, "/dev/"); ok { + m.Options = append(m.Options, + fmt.Sprintf("rule=path %s unhide mode %04o", dev, mode)) + } else { + return fmt.Errorf("expected device to start with \"/dev\": %v", dev) + } + g.Config.Mounts[k] = m + return nil + } + } + return fmt.Errorf("devfs not found in generator") +} diff --git a/pkg/specgen/generate/config_linux.go b/pkg/specgen/generate/config_linux.go index 6f5484db6e..57a46d63a7 100644 --- a/pkg/specgen/generate/config_linux.go +++ b/pkg/specgen/generate/config_linux.go @@ -176,61 +176,6 @@ func addDevice(g *generate.Generator, device string) error { return nil } -// ParseDevice parses device mapping string to a src, dest & permissions string -func ParseDevice(device string) (string, string, string, error) { - var src string - var dst string - permissions := "rwm" - arr := strings.Split(device, ":") - switch len(arr) { - case 3: - if !IsValidDeviceMode(arr[2]) { - return "", "", "", fmt.Errorf("invalid device mode: %s", arr[2]) - } - permissions = arr[2] - fallthrough - case 2: - if IsValidDeviceMode(arr[1]) { - permissions = arr[1] - } else { - if len(arr[1]) > 0 && arr[1][0] != '/' { - return "", "", "", fmt.Errorf("invalid device mode: %s", arr[1]) - } - dst = arr[1] - } - fallthrough - case 1: - src = arr[0] - default: - return "", "", "", fmt.Errorf("invalid device specification: %s", device) - } - - if dst == "" { - dst = src - } - return src, dst, permissions, nil -} - -// IsValidDeviceMode checks if the mode for device is valid or not. -// IsValid mode is a composition of r (read), w (write), and m (mknod). -func IsValidDeviceMode(mode string) bool { - var legalDeviceMode = map[rune]bool{ - 'r': true, - 'w': true, - 'm': true, - } - if mode == "" { - return false - } - for _, c := range mode { - if !legalDeviceMode[c] { - return false - } - legalDeviceMode[c] = false - } - return true -} - func supportAmbientCapabilities() bool { err := unix.Prctl(unix.PR_CAP_AMBIENT, unix.PR_CAP_AMBIENT_IS_SET, 0, 0, 0) return err == nil diff --git a/pkg/specgen/generate/config_linux_test.go b/pkg/specgen/generate/config_linux_test.go index 47703cdb20..39973324b3 100644 --- a/pkg/specgen/generate/config_linux_test.go +++ b/pkg/specgen/generate/config_linux_test.go @@ -26,25 +26,3 @@ func TestShouldMask(t *testing.T) { assert.Equal(t, val, test.shouldMask) } } - -func TestParseDevice(t *testing.T) { - tests := []struct { - device string - src string - dst string - perm string - }{ - {"/dev/foo", "/dev/foo", "/dev/foo", "rwm"}, - {"/dev/foo:/dev/bar", "/dev/foo", "/dev/bar", "rwm"}, - {"/dev/foo:/dev/bar:rw", "/dev/foo", "/dev/bar", "rw"}, - {"/dev/foo:rw", "/dev/foo", "/dev/foo", "rw"}, - {"/dev/foo::rw", "/dev/foo", "/dev/foo", "rw"}, - } - for _, test := range tests { - src, dst, perm, err := ParseDevice(test.device) - assert.NoError(t, err) - assert.Equal(t, src, test.src) - assert.Equal(t, dst, test.dst) - assert.Equal(t, perm, test.perm) - } -} diff --git a/pkg/specgen/generate/oci_freebsd.go b/pkg/specgen/generate/oci_freebsd.go index dd00622dd3..6f9b2ac342 100644 --- a/pkg/specgen/generate/oci_freebsd.go +++ b/pkg/specgen/generate/oci_freebsd.go @@ -48,6 +48,28 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt g.AddAnnotation(key, val) } + // Devices + var userDevices []spec.LinuxDevice + if !s.Privileged { + // add default devices from containers.conf + for _, device := range rtc.Containers.Devices { + if err = DevicesFromPath(&g, device); err != nil { + return nil, err + } + } + if len(compatibleOptions.HostDeviceList) > 0 && len(s.Devices) == 0 { + userDevices = compatibleOptions.HostDeviceList + } else { + userDevices = s.Devices + } + // add default devices specified by caller + for _, device := range userDevices { + if err = DevicesFromPath(&g, device.Path); err != nil { + return nil, err + } + } + } + g.ClearProcessEnv() for name, val := range s.Env { g.AddProcessEnv(name, val) diff --git a/pkg/specgen/generate/security_freebsd.go b/pkg/specgen/generate/security_freebsd.go index 5fd66c7695..27dc0e5931 100644 --- a/pkg/specgen/generate/security_freebsd.go +++ b/pkg/specgen/generate/security_freebsd.go @@ -15,5 +15,17 @@ func setLabelOpts(s *specgen.SpecGenerator, runtime *libpod.Runtime, pidConfig s } func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, newImage *libimage.Image, rtc *config.Config) error { + // If this is a privileged container, change the devfs ruleset to expose all devices. + if s.Privileged { + for k, m := range g.Config.Mounts { + if m.Type == "devfs" { + m.Options = []string{ + "ruleset=0", + } + g.Config.Mounts[k] = m + } + } + } + return nil }