configs: use different types for .Devices and .Resources.Devices

Making them the same type is simply confusing, but also means that you
could accidentally use one in the wrong context. This eliminates that
problem. This also includes a whole bunch of cleanups for the types
within DeviceRule, so that they can be used more ergonomically.

Signed-off-by: Aleksa Sarai <asarai@suse.de>
This commit is contained in:
Aleksa Sarai 2020-05-07 13:59:36 +10:00
parent 60e21ec26e
commit 24388be71e
No known key found for this signature in database
GPG Key ID: 9E18AA267DDB8DB4
10 changed files with 330 additions and 209 deletions

View File

@ -22,7 +22,7 @@ const (
) )
// DeviceFilter returns eBPF device filter program and its license string // DeviceFilter returns eBPF device filter program and its license string
func DeviceFilter(devices []*configs.Device) (asm.Instructions, string, error) { func DeviceFilter(devices []*configs.DeviceRule) (asm.Instructions, string, error) {
p := &program{} p := &program{}
p.init() p.init()
for i := len(devices) - 1; i >= 0; i-- { for i := len(devices) - 1; i >= 0; i-- {
@ -68,7 +68,7 @@ func (p *program) init() {
} }
// appendDevice needs to be called from the last element of OCI linux.resources.devices to the head element. // appendDevice needs to be called from the last element of OCI linux.resources.devices to the head element.
func (p *program) appendDevice(dev *configs.Device) error { func (p *program) appendDevice(dev *configs.DeviceRule) error {
if p.blockID < 0 { if p.blockID < 0 {
return errors.New("the program is finalized") return errors.New("the program is finalized")
} }

View File

@ -20,7 +20,7 @@ func hash(s, comm string) string {
return strings.Join(res, "\n") return strings.Join(res, "\n")
} }
func testDeviceFilter(t testing.TB, devices []*configs.Device, expectedStr string) { func testDeviceFilter(t testing.TB, devices []*configs.DeviceRule, expectedStr string) {
insts, _, err := DeviceFilter(devices) insts, _, err := DeviceFilter(devices)
if err != nil { if err != nil {
t.Fatalf("%s: %v (devices: %+v)", t.Name(), err, devices) t.Fatalf("%s: %v (devices: %+v)", t.Name(), err, devices)
@ -137,11 +137,15 @@ block-11:
62: Mov32Imm dst: r0 imm: 0 62: Mov32Imm dst: r0 imm: 0
63: Exit 63: Exit
` `
testDeviceFilter(t, specconv.AllowedDevices, expected) var devices []*configs.DeviceRule
for _, device := range specconv.AllowedDevices {
devices = append(devices, &device.DeviceRule)
}
testDeviceFilter(t, devices, expected)
} }
func TestDeviceFilter_Privileged(t *testing.T) { func TestDeviceFilter_Privileged(t *testing.T) {
devices := []*configs.Device{ devices := []*configs.DeviceRule{
{ {
Type: 'a', Type: 'a',
Major: -1, Major: -1,
@ -168,7 +172,7 @@ block-0:
} }
func TestDeviceFilter_PrivilegedExceptSingleDevice(t *testing.T) { func TestDeviceFilter_PrivilegedExceptSingleDevice(t *testing.T) {
devices := []*configs.Device{ devices := []*configs.DeviceRule{
{ {
Type: 'a', Type: 'a',
Major: -1, Major: -1,
@ -208,7 +212,7 @@ block-1:
} }
func TestDeviceFilter_Weird(t *testing.T) { func TestDeviceFilter_Weird(t *testing.T) {
devices := []*configs.Device{ devices := []*configs.DeviceRule{
{ {
Type: 'b', Type: 'b',
Major: 8, Major: 8,

View File

@ -18,14 +18,12 @@ func TestDevicesSetAllow(t *testing.T) {
"devices.deny": "", "devices.deny": "",
}) })
helper.CgroupData.config.Resources.Devices = []*configs.Device{ helper.CgroupData.config.Resources.Devices = []*configs.DeviceRule{
{ {
Path: "/dev/zero", Type: configs.CharDevice,
Type: 'c',
Major: 1, Major: 1,
Minor: 5, Minor: 5,
Permissions: "rwm", Permissions: configs.DevicePermissions("rwm"),
FileMode: 0666,
Allow: true, Allow: true,
}, },
} }

View File

@ -10,12 +10,10 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func isRWM(cgroupPermissions string) bool { func isRWM(perms configs.DevicePermissions) bool {
r := false var r, w, m bool
w := false for _, perm := range perms {
m := false switch perm {
for _, rn := range cgroupPermissions {
switch rn {
case 'r': case 'r':
r = true r = true
case 'w': case 'w':

View File

@ -42,7 +42,7 @@ type Cgroup struct {
type Resources struct { type Resources struct {
// Devices is the set of access rules for devices in the container. // Devices is the set of access rules for devices in the container.
Devices []*Device `json:"devices"` Devices []*DeviceRule `json:"devices"`
// Memory limit (in bytes) // Memory limit (in bytes)
Memory int64 `json:"memory"` Memory int64 `json:"memory"`

View File

@ -1,8 +1,12 @@
package configs package configs
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"strconv"
"golang.org/x/sys/unix"
) )
const ( const (
@ -12,21 +16,11 @@ const (
// TODO Windows: This can be factored out in the future // TODO Windows: This can be factored out in the future
type Device struct { type Device struct {
// Device type, block, char, etc. DeviceRule
Type rune `json:"type"`
// Path to the device. // Path to the device.
Path string `json:"path"` Path string `json:"path"`
// Major is the device's major number.
Major int64 `json:"major"`
// Minor is the device's minor number.
Minor int64 `json:"minor"`
// Cgroup permissions format, rwm.
Permissions string `json:"permissions"`
// FileMode permission bits for the device. // FileMode permission bits for the device.
FileMode os.FileMode `json:"file_mode"` FileMode os.FileMode `json:"file_mode"`
@ -35,23 +29,154 @@ type Device struct {
// Gid of the device. // Gid of the device.
Gid uint32 `json:"gid"` Gid uint32 `json:"gid"`
}
// Write the file to the allowed list // DevicePermissions is a cgroupv1-style string to represent device access. It
// has to be a string for backward compatibility reasons, hence why it has
// methods to do set operations.
type DevicePermissions string
const (
deviceRead uint = (1 << iota)
deviceWrite
deviceMknod
)
func (p DevicePermissions) toSet() uint {
var set uint
for _, perm := range p {
switch perm {
case 'r':
set |= deviceRead
case 'w':
set |= deviceWrite
case 'm':
set |= deviceMknod
}
}
return set
}
func fromSet(set uint) DevicePermissions {
var perm string
if set&deviceRead == deviceRead {
perm += "r"
}
if set&deviceWrite == deviceWrite {
perm += "w"
}
if set&deviceMknod == deviceMknod {
perm += "m"
}
return DevicePermissions(perm)
}
// Union returns the union of the two sets of DevicePermissions.
func (p DevicePermissions) Union(o DevicePermissions) DevicePermissions {
lhs := p.toSet()
rhs := o.toSet()
return fromSet(lhs | rhs)
}
// Difference returns the set difference of the two sets of DevicePermissions.
// In set notation, A.Difference(B) gives you A\B.
func (p DevicePermissions) Difference(o DevicePermissions) DevicePermissions {
lhs := p.toSet()
rhs := o.toSet()
return fromSet(lhs &^ rhs)
}
// Intersection computes the intersection of the two sets of DevicePermissions.
func (p DevicePermissions) Intersection(o DevicePermissions) DevicePermissions {
lhs := p.toSet()
rhs := o.toSet()
return fromSet(lhs & rhs)
}
// IsEmpty returns whether the set of permissions in a DevicePermissions is
// empty.
func (p DevicePermissions) IsEmpty() bool {
return p == DevicePermissions("")
}
// IsValid returns whether the set of permissions is a subset of valid
// permissions (namely, {r,w,m}).
func (p DevicePermissions) IsValid() bool {
return p == fromSet(p.toSet())
}
type DeviceType rune
const (
WildcardDevice DeviceType = 'a'
BlockDevice DeviceType = 'b'
CharDevice DeviceType = 'c' // or 'u'
FifoDevice DeviceType = 'p'
)
func (t DeviceType) IsValid() bool {
switch t {
case WildcardDevice, BlockDevice, CharDevice, FifoDevice:
return true
default:
return false
}
}
func (t DeviceType) CanMknod() bool {
switch t {
case BlockDevice, CharDevice, FifoDevice:
return true
default:
return false
}
}
func (t DeviceType) CanCgroup() bool {
switch t {
case WildcardDevice, BlockDevice, CharDevice:
return true
default:
return false
}
}
type DeviceRule struct {
// Type of device ('c' for char, 'b' for block). If set to 'a', this rule
// acts as a wildcard and all fields other than Allow are ignored.
Type DeviceType `json:"type"`
// Major is the device's major number.
Major int64 `json:"major"`
// Minor is the device's minor number.
Minor int64 `json:"minor"`
// Permissions is the set of permissions that this rule applies to (in the
// cgroupv1 format -- any combination of "rwm").
Permissions DevicePermissions `json:"permissions"`
// Allow specifies whether this rule is allowed.
Allow bool `json:"allow"` Allow bool `json:"allow"`
} }
func (d *Device) CgroupString() string { func (d *DeviceRule) CgroupString() string {
return fmt.Sprintf("%c %s:%s %s", d.Type, deviceNumberString(d.Major), deviceNumberString(d.Minor), d.Permissions) var (
major = strconv.FormatInt(d.Major, 10)
minor = strconv.FormatInt(d.Minor, 10)
)
if d.Major == Wildcard {
major = "*"
}
if d.Minor == Wildcard {
minor = "*"
}
return fmt.Sprintf("%c %s:%s %s", d.Type, major, minor, d.Permissions)
} }
func (d *Device) Mkdev() int { func (d *DeviceRule) Mkdev() (uint64, error) {
return int((d.Major << 8) | (d.Minor & 0xff) | ((d.Minor & 0xfff00) << 12)) if d.Major == Wildcard || d.Minor == Wildcard {
return 0, errors.New("cannot mkdev() device with wildcards")
} }
return unix.Mkdev(uint32(d.Major), uint32(d.Minor)), nil
// deviceNumberString converts the device number to a string return result.
func deviceNumberString(number int64) string {
if number == Wildcard {
return "*"
}
return fmt.Sprint(number)
} }

View File

@ -31,30 +31,30 @@ func DeviceFromPath(path, permissions string) (*configs.Device, error) {
} }
var ( var (
devType configs.DeviceType
mode = stat.Mode
devNumber = uint64(stat.Rdev) devNumber = uint64(stat.Rdev)
major = unix.Major(devNumber) major = unix.Major(devNumber)
minor = unix.Minor(devNumber) minor = unix.Minor(devNumber)
) )
if major == 0 {
return nil, ErrNotADevice
}
var (
devType rune
mode = stat.Mode
)
switch { switch {
case mode&unix.S_IFBLK == unix.S_IFBLK: case mode&unix.S_IFBLK == unix.S_IFBLK:
devType = 'b' devType = configs.BlockDevice
case mode&unix.S_IFCHR == unix.S_IFCHR: case mode&unix.S_IFCHR == unix.S_IFCHR:
devType = 'c' devType = configs.CharDevice
case mode&unix.S_IFIFO == unix.S_IFIFO:
devType = configs.FifoDevice
default:
return nil, ErrNotADevice
} }
return &configs.Device{ return &configs.Device{
DeviceRule: configs.DeviceRule{
Type: devType, Type: devType,
Path: path,
Major: int64(major), Major: int64(major),
Minor: int64(minor), Minor: int64(minor),
Permissions: permissions, Permissions: configs.DevicePermissions(permissions),
},
Path: path,
FileMode: os.FileMode(mode), FileMode: os.FileMode(mode),
Uid: stat.Uid, Uid: stat.Uid,
Gid: stat.Gid, Gid: stat.Gid,

View File

@ -21,6 +21,10 @@ const defaultMountFlags = unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV
// it uses a network strategy of just setting a loopback interface // it uses a network strategy of just setting a loopback interface
// and the default setup for devices // and the default setup for devices
func newTemplateConfig(rootfs string) *configs.Config { func newTemplateConfig(rootfs string) *configs.Config {
var allowedDevices []*configs.DeviceRule
for _, device := range specconv.AllowedDevices {
allowedDevices = append(allowedDevices, &device.DeviceRule)
}
return &configs.Config{ return &configs.Config{
Rootfs: rootfs, Rootfs: rootfs,
Capabilities: &configs.Capabilities{ Capabilities: &configs.Capabilities{
@ -116,7 +120,7 @@ func newTemplateConfig(rootfs string) *configs.Config {
Path: "integration/test", Path: "integration/test",
Resources: &configs.Resources{ Resources: &configs.Resources{
MemorySwappiness: nil, MemorySwappiness: nil,
Devices: specconv.AllowedDevices, Devices: allowedDevices,
}, },
}, },
MaskPaths: []string{ MaskPaths: []string{

View File

@ -631,16 +631,20 @@ func createDeviceNode(rootfs string, node *configs.Device, bind bool) error {
func mknodDevice(dest string, node *configs.Device) error { func mknodDevice(dest string, node *configs.Device) error {
fileMode := node.FileMode fileMode := node.FileMode
switch node.Type { switch node.Type {
case 'c', 'u': case configs.BlockDevice:
fileMode |= unix.S_IFCHR
case 'b':
fileMode |= unix.S_IFBLK fileMode |= unix.S_IFBLK
case 'p': case configs.CharDevice:
fileMode |= unix.S_IFCHR
case configs.FifoDevice:
fileMode |= unix.S_IFIFO fileMode |= unix.S_IFIFO
default: default:
return fmt.Errorf("%c is not a valid device type for device %s", node.Type, node.Path) return fmt.Errorf("%c is not a valid device type for device %s", node.Type, node.Path)
} }
if err := unix.Mknod(dest, uint32(fileMode), node.Mkdev()); err != nil { dev, err := node.Mkdev()
if err != nil {
return err
}
if err := unix.Mknod(dest, uint32(fileMode), int(dev)); err != nil {
return err return err
} }
return unix.Chown(dest, int(node.Uid), int(node.Gid)) return unix.Chown(dest, int(node.Uid), int(node.Gid))

View File

@ -66,93 +66,130 @@ var mountPropagationMapping = map[string]int{
var AllowedDevices = []*configs.Device{ var AllowedDevices = []*configs.Device{
// allow mknod for any device // allow mknod for any device
{ {
Type: 'c', DeviceRule: configs.DeviceRule{
Major: wildcard, Type: configs.CharDevice,
Minor: wildcard, Major: configs.Wildcard,
Minor: configs.Wildcard,
Permissions: "m", Permissions: "m",
Allow: true, Allow: true,
}, },
},
{ {
Type: 'b', DeviceRule: configs.DeviceRule{
Major: wildcard, Type: configs.BlockDevice,
Minor: wildcard, Major: configs.Wildcard,
Minor: configs.Wildcard,
Permissions: "m", Permissions: "m",
Allow: true, Allow: true,
}, },
},
{ {
Type: 'c',
Path: "/dev/null", Path: "/dev/null",
FileMode: 0666,
Uid: 0,
Gid: 0,
DeviceRule: configs.DeviceRule{
Type: configs.CharDevice,
Major: 1, Major: 1,
Minor: 3, Minor: 3,
Permissions: "rwm", Permissions: "rwm",
Allow: true, Allow: true,
}, },
},
{ {
Type: 'c',
Path: "/dev/random", Path: "/dev/random",
FileMode: 0666,
Uid: 0,
Gid: 0,
DeviceRule: configs.DeviceRule{
Type: configs.CharDevice,
Major: 1, Major: 1,
Minor: 8, Minor: 8,
Permissions: "rwm", Permissions: "rwm",
Allow: true, Allow: true,
}, },
},
{ {
Type: 'c',
Path: "/dev/full", Path: "/dev/full",
FileMode: 0666,
Uid: 0,
Gid: 0,
DeviceRule: configs.DeviceRule{
Type: configs.CharDevice,
Major: 1, Major: 1,
Minor: 7, Minor: 7,
Permissions: "rwm", Permissions: "rwm",
Allow: true, Allow: true,
}, },
},
{ {
Type: 'c',
Path: "/dev/tty", Path: "/dev/tty",
FileMode: 0666,
Uid: 0,
Gid: 0,
DeviceRule: configs.DeviceRule{
Type: configs.CharDevice,
Major: 5, Major: 5,
Minor: 0, Minor: 0,
Permissions: "rwm", Permissions: "rwm",
Allow: true, Allow: true,
}, },
},
{ {
Type: 'c',
Path: "/dev/zero", Path: "/dev/zero",
FileMode: 0666,
Uid: 0,
Gid: 0,
DeviceRule: configs.DeviceRule{
Type: configs.CharDevice,
Major: 1, Major: 1,
Minor: 5, Minor: 5,
Permissions: "rwm", Permissions: "rwm",
Allow: true, Allow: true,
}, },
},
{ {
Type: 'c',
Path: "/dev/urandom", Path: "/dev/urandom",
FileMode: 0666,
Uid: 0,
Gid: 0,
DeviceRule: configs.DeviceRule{
Type: configs.CharDevice,
Major: 1, Major: 1,
Minor: 9, Minor: 9,
Permissions: "rwm", Permissions: "rwm",
Allow: true, Allow: true,
}, },
},
// /dev/pts/ - pts namespaces are "coming soon" // /dev/pts/ - pts namespaces are "coming soon"
{ {
Path: "", DeviceRule: configs.DeviceRule{
Type: 'c', Type: configs.CharDevice,
Major: 136, Major: 136,
Minor: wildcard, Minor: configs.Wildcard,
Permissions: "rwm", Permissions: "rwm",
Allow: true, Allow: true,
}, },
},
{ {
Path: "", DeviceRule: configs.DeviceRule{
Type: 'c', Type: configs.CharDevice,
Major: 5, Major: 5,
Minor: 2, Minor: 2,
Permissions: "rwm", Permissions: "rwm",
Allow: true, Allow: true,
}, },
},
// tuntap // tuntap
{ {
Path: "", DeviceRule: configs.DeviceRule{
Type: 'c', Type: configs.CharDevice,
Major: 10, Major: 10,
Minor: 200, Minor: 200,
Permissions: "rwm", Permissions: "rwm",
Allow: true, Allow: true,
}, },
},
} }
type CreateOpts struct { type CreateOpts struct {
@ -451,14 +488,13 @@ func CreateCgroupConfig(opts *CreateOpts) (*configs.Cgroup, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
dd := &configs.Device{ c.Resources.Devices = append(c.Resources.Devices, &configs.DeviceRule{
Type: dt, Type: dt,
Major: major, Major: major,
Minor: minor, Minor: minor,
Permissions: d.Access, Permissions: configs.DevicePermissions(d.Access),
Allow: d.Allow, Allow: d.Allow,
} })
c.Resources.Devices = append(c.Resources.Devices, dd)
} }
if r.Memory != nil { if r.Memory != nil {
if r.Memory.Limit != nil { if r.Memory.Limit != nil {
@ -583,98 +619,48 @@ func CreateCgroupConfig(opts *CreateOpts) (*configs.Cgroup, error) {
} }
} }
} }
// append the default allowed devices to the end of the list // Append the default allowed devices to the end of the list.
c.Resources.Devices = append(c.Resources.Devices, AllowedDevices...) // XXX: Really this should be prefixed...
for _, device := range AllowedDevices {
c.Resources.Devices = append(c.Resources.Devices, &device.DeviceRule)
}
return c, nil return c, nil
} }
func stringToCgroupDeviceRune(s string) (rune, error) { func stringToCgroupDeviceRune(s string) (configs.DeviceType, error) {
switch s { switch s {
case "a": case "a":
return 'a', nil return configs.WildcardDevice, nil
case "b": case "b":
return 'b', nil return configs.BlockDevice, nil
case "c": case "c":
return 'c', nil return configs.CharDevice, nil
default: default:
return 0, fmt.Errorf("invalid cgroup device type %q", s) return 0, fmt.Errorf("invalid cgroup device type %q", s)
} }
} }
func stringToDeviceRune(s string) (rune, error) { func stringToDeviceRune(s string) (configs.DeviceType, error) {
switch s { switch s {
case "p": case "p":
return 'p', nil return configs.FifoDevice, nil
case "u": case "u", "c":
return 'u', nil return configs.CharDevice, nil
case "b": case "b":
return 'b', nil return configs.BlockDevice, nil
case "c":
return 'c', nil
default: default:
return 0, fmt.Errorf("invalid device type %q", s) return 0, fmt.Errorf("invalid device type %q", s)
} }
} }
func createDevices(spec *specs.Spec, config *configs.Config) error { func createDevices(spec *specs.Spec, config *configs.Config) error {
// add whitelisted devices // Add default set of devices.
config.Devices = []*configs.Device{ for _, device := range AllowedDevices {
{ if device.Path != "" {
Type: 'c', config.Devices = append(config.Devices, device)
Path: "/dev/null",
Major: 1,
Minor: 3,
FileMode: 0666,
Uid: 0,
Gid: 0,
},
{
Type: 'c',
Path: "/dev/random",
Major: 1,
Minor: 8,
FileMode: 0666,
Uid: 0,
Gid: 0,
},
{
Type: 'c',
Path: "/dev/full",
Major: 1,
Minor: 7,
FileMode: 0666,
Uid: 0,
Gid: 0,
},
{
Type: 'c',
Path: "/dev/tty",
Major: 5,
Minor: 0,
FileMode: 0666,
Uid: 0,
Gid: 0,
},
{
Type: 'c',
Path: "/dev/zero",
Major: 1,
Minor: 5,
FileMode: 0666,
Uid: 0,
Gid: 0,
},
{
Type: 'c',
Path: "/dev/urandom",
Major: 1,
Minor: 9,
FileMode: 0666,
Uid: 0,
Gid: 0,
},
} }
// merge in additional devices from the spec }
// Merge in additional devices from the spec.
if spec.Linux != nil { if spec.Linux != nil {
for _, d := range spec.Linux.Devices { for _, d := range spec.Linux.Devices {
var uid, gid uint32 var uid, gid uint32
@ -694,10 +680,12 @@ func createDevices(spec *specs.Spec, config *configs.Config) error {
filemode = *d.FileMode filemode = *d.FileMode
} }
device := &configs.Device{ device := &configs.Device{
DeviceRule: configs.DeviceRule{
Type: dt, Type: dt,
Path: d.Path,
Major: d.Major, Major: d.Major,
Minor: d.Minor, Minor: d.Minor,
},
Path: d.Path,
FileMode: filemode, FileMode: filemode,
Uid: uid, Uid: uid,
Gid: gid, Gid: gid,