diff --git a/libcontainer/cgroups/devices/devices_emulator.go b/libcontainer/cgroups/devices/devices_emulator.go new file mode 100644 index 000000000..9d9a60e39 --- /dev/null +++ b/libcontainer/cgroups/devices/devices_emulator.go @@ -0,0 +1,351 @@ +// +build linux + +package devices + +import ( + "bufio" + "io" + "regexp" + "sort" + "strconv" + + "github.com/opencontainers/runc/libcontainer/configs" + + "github.com/pkg/errors" +) + +// deviceMeta is a DeviceRule without the Allow or Permissions fields, and no +// wildcard-type support. It's effectively the "match" portion of a metadata +// rule, for the purposes of our emulation. +type deviceMeta struct { + node configs.DeviceType + major int64 + minor int64 +} + +// deviceRule is effectively the tuple (deviceMeta, DevicePermissions). +type deviceRule struct { + meta deviceMeta + perms configs.DevicePermissions +} + +// deviceRules is a mapping of device metadata rules to the associated +// permissions in the ruleset. +type deviceRules map[deviceMeta]configs.DevicePermissions + +func (r deviceRules) orderedEntries() []deviceRule { + var rules []deviceRule + for meta, perms := range r { + rules = append(rules, deviceRule{meta: meta, perms: perms}) + } + sort.Slice(rules, func(i, j int) bool { + // Sort by (major, minor, type). + a, b := rules[i].meta, rules[j].meta + return a.major < b.major || + (a.major == b.major && a.minor < b.minor) || + (a.major == b.major && a.minor == b.minor && a.node < b.node) + }) + return rules +} + +type Emulator struct { + defaultAllow bool + rules deviceRules +} + +func (e *Emulator) IsBlacklist() bool { + return e.defaultAllow +} + +var devicesListRegexp = regexp.MustCompile(`^([abc])\s+(\d+|\*):(\d+|\*)\s+([rwm]+)$`) + +func parseLine(line string) (*deviceRule, error) { + matches := devicesListRegexp.FindStringSubmatch(line) + if matches == nil { + return nil, errors.Errorf("line doesn't match devices.list format") + } + var ( + rule deviceRule + node = matches[1] + major = matches[2] + minor = matches[3] + perms = matches[4] + ) + + // Parse the node type. + switch node { + case "a": + // Super-special case -- "a" always means every device with every + // access mode. In fact, for devices.list this actually indicates that + // the cgroup is in black-list mode. + // TODO: Double-check that the entire file is "a *:* rwm". + return nil, nil + case "b": + rule.meta.node = configs.BlockDevice + case "c": + rule.meta.node = configs.CharDevice + default: + // Should never happen! + return nil, errors.Errorf("unknown device type %q", node) + } + + // Parse the major number. + if major == "*" { + rule.meta.major = configs.Wildcard + } else { + val, err := strconv.ParseUint(major, 10, 32) + if err != nil { + return nil, errors.Wrap(err, "parse major number") + } + rule.meta.major = int64(val) + } + + // Parse the minor number. + if minor == "*" { + rule.meta.minor = configs.Wildcard + } else { + val, err := strconv.ParseUint(minor, 10, 32) + if err != nil { + return nil, errors.Wrap(err, "parse minor number") + } + rule.meta.minor = int64(val) + } + + // Parse the access permissions. + rule.perms = configs.DevicePermissions(perms) + if !rule.perms.IsValid() || rule.perms.IsEmpty() { + // Should never happen! + return nil, errors.Errorf("parse access mode: contained unknown modes or is empty: %q", perms) + } + return &rule, nil +} + +func (e *Emulator) addRule(rule deviceRule) error { + if e.rules == nil { + e.rules = make(map[deviceMeta]configs.DevicePermissions) + } + + // Merge with any pre-existing permissions. + oldPerms := e.rules[rule.meta] + newPerms := rule.perms.Union(oldPerms) + e.rules[rule.meta] = newPerms + return nil +} + +func (e *Emulator) rmRule(rule deviceRule) error { + // Give an error if any of the permissions requested to be removed are + // present in a partially-matching wildcard rule, because such rules will + // be ignored by cgroupv1. + // + // This is a diversion from cgroupv1, but is necessary to avoid leading + // users into a false sense of security. cgroupv1 will silently(!) ignore + // requests to remove partial exceptions, but we really shouldn't do that. + // + // It may seem like we could just "split" wildcard rules which hit this + // issue, but unfortunately there are 2^32 possible major and minor + // numbers, which would exhaust kernel memory quickly if we did this. Not + // to mention it'd be really slow (the kernel side is implemented as a + // linked-list of exceptions). + for _, partialMeta := range []deviceMeta{ + {node: rule.meta.node, major: configs.Wildcard, minor: rule.meta.minor}, + {node: rule.meta.node, major: rule.meta.major, minor: configs.Wildcard}, + {node: rule.meta.node, major: configs.Wildcard, minor: configs.Wildcard}, + } { + // This wildcard rule is equivalent to the requested rule, so skip it. + if rule.meta == partialMeta { + continue + } + // Only give an error if the set of permissions overlap. + partialPerms := e.rules[partialMeta] + if !partialPerms.Intersection(rule.perms).IsEmpty() { + return errors.Errorf("requested rule [%v %v] not supported by devices cgroupv1 (cannot punch hole in existing wildcard rule [%v %v])", rule.meta, rule.perms, partialMeta, partialPerms) + } + } + + // Subtract all of the permissions listed from the full match rule. If the + // rule didn't exist, all of this is a no-op. + newPerms := e.rules[rule.meta].Difference(rule.perms) + if newPerms.IsEmpty() { + delete(e.rules, rule.meta) + } else { + e.rules[rule.meta] = newPerms + } + // TODO: The actual cgroup code doesn't care if an exception didn't exist + // during removal, so not erroring out here is /accurate/ but quite + // worrying. Maybe we should do additional validation, but again we + // have to worry about backwards-compatibility. + return nil +} + +func (e *Emulator) allow(rule *deviceRule) error { + // This cgroup is configured as a black-list. Reset the entire emulator, + // and put is into black-list mode. + if rule == nil || rule.meta.node == configs.WildcardDevice { + *e = Emulator{ + defaultAllow: true, + rules: nil, + } + return nil + } + + var err error + if e.defaultAllow { + err = errors.Wrap(e.rmRule(*rule), "remove 'deny' exception") + } else { + err = errors.Wrap(e.addRule(*rule), "add 'allow' exception") + } + return err +} + +func (e *Emulator) deny(rule *deviceRule) error { + // This cgroup is configured as a white-list. Reset the entire emulator, + // and put is into white-list mode. + if rule == nil || rule.meta.node == configs.WildcardDevice { + *e = Emulator{ + defaultAllow: false, + rules: nil, + } + return nil + } + + var err error + if e.defaultAllow { + err = errors.Wrap(e.addRule(*rule), "add 'deny' exception") + } else { + err = errors.Wrap(e.rmRule(*rule), "remove 'allow' exception") + } + return err +} + +func (e *Emulator) Apply(rule configs.DeviceRule) error { + if !rule.Type.CanCgroup() { + return errors.Errorf("cannot add rule [%#v] with non-cgroup type %q", rule, rule.Type) + } + + innerRule := &deviceRule{ + meta: deviceMeta{ + node: rule.Type, + major: rule.Major, + minor: rule.Minor, + }, + perms: rule.Permissions, + } + if innerRule.meta.node == configs.WildcardDevice { + innerRule = nil + } + + if rule.Allow { + return e.allow(innerRule) + } else { + return e.deny(innerRule) + } +} + +// EmulatorFromList takes a reader to a "devices.list"-like source, and returns +// a new Emulator that represents the state of the devices cgroup. Note that +// black-list devices cgroups cannot be fully reconstructed, due to limitations +// in the devices cgroup API. Instead, such cgroups are always treated as +// "allow all" cgroups. +func EmulatorFromList(list io.Reader) (*Emulator, error) { + // Normally cgroups are in black-list mode by default, but the way we + // figure out the current mode is whether or not devices.list has an + // allow-all rule. So we default to a white-list, and the existence of an + // "a *:* rwm" entry will tell us otherwise. + e := &Emulator{ + defaultAllow: false, + } + + // Parse the "devices.list". + s := bufio.NewScanner(list) + for s.Scan() { + line := s.Text() + deviceRule, err := parseLine(line) + if err != nil { + return nil, errors.Wrapf(err, "parsing line %q", line) + } + // "devices.list" is an allow list. Note that this means that in + // black-list mode, we have no idea what rules are in play. As a + // result, we need to be very careful in Transition(). + if err := e.allow(deviceRule); err != nil { + return nil, errors.Wrapf(err, "adding devices.list rule") + } + } + if err := s.Err(); err != nil { + return nil, errors.Wrap(err, "reading devices.list lines") + } + return e, nil +} + +// Transition calculates what is the minimally-disruptive set of rules need to +// be applied to a devices cgroup in order to transition to the given target. +// This means that any already-existing rules will not be applied, and +// disruptive rules (like denying all device access) will only be applied if +// necessary. +// +// This function is the sole reason for all of Emulator -- to allow us +// to figure out how to update a containers' cgroups without causing spurrious +// device errors (if possible). +func (source *Emulator) Transition(target *Emulator) ([]*configs.DeviceRule, error) { + var transitionRules []*configs.DeviceRule + oldRules := source.rules + + // If the default policy doesn't match, we need to include a "disruptive" + // rule (either allow-all or deny-all) in order to switch the cgroup to the + // correct default policy. + // + // However, due to a limitation in "devices.list" we cannot be sure what + // deny rules are in place in a black-list cgroup. Thus if the source is a + // black-list we also have to include a disruptive rule. + if source.IsBlacklist() || source.defaultAllow != target.defaultAllow { + transitionRules = append(transitionRules, &configs.DeviceRule{ + Type: 'a', + Major: -1, + Minor: -1, + Permissions: configs.DevicePermissions("rwm"), + Allow: target.defaultAllow, + }) + // The old rules are only relevant if we aren't starting out with a + // disruptive rule. + oldRules = nil + } + + // NOTE: We traverse through the rules in a sorted order so we always write + // the same set of rules (this is to aid testing). + + // First, we create inverse rules for any old rules not in the new set. + // This includes partial-inverse rules for specific permissions. This is a + // no-op if we added a disruptive rule, since oldRules will be empty. + for _, rule := range oldRules.orderedEntries() { + meta, oldPerms := rule.meta, rule.perms + newPerms := target.rules[meta] + droppedPerms := oldPerms.Difference(newPerms) + if !droppedPerms.IsEmpty() { + transitionRules = append(transitionRules, &configs.DeviceRule{ + Type: meta.node, + Major: meta.major, + Minor: meta.minor, + Permissions: droppedPerms, + Allow: target.defaultAllow, + }) + } + } + + // Add any additional rules which weren't in the old set. We happen to + // filter out rules which are present in both sets, though this isn't + // strictly necessary. + for _, rule := range target.rules.orderedEntries() { + meta, newPerms := rule.meta, rule.perms + oldPerms := oldRules[meta] + gainedPerms := newPerms.Difference(oldPerms) + if !gainedPerms.IsEmpty() { + transitionRules = append(transitionRules, &configs.DeviceRule{ + Type: meta.node, + Major: meta.major, + Minor: meta.minor, + Permissions: gainedPerms, + Allow: !target.defaultAllow, + }) + } + } + return transitionRules, nil +} diff --git a/libcontainer/cgroups/devices/devices_emulator_test.go b/libcontainer/cgroups/devices/devices_emulator_test.go new file mode 100644 index 000000000..f813accef --- /dev/null +++ b/libcontainer/cgroups/devices/devices_emulator_test.go @@ -0,0 +1,1096 @@ +package devices + +import ( + "bytes" + "reflect" + "testing" + + "github.com/opencontainers/runc/libcontainer/configs" +) + +func TestDeviceEmulatorLoad(t *testing.T) { + tests := []struct { + name, list string + expected *Emulator + }{ + { + name: "BlacklistMode", + list: `a *:* rwm`, + expected: &Emulator{ + defaultAllow: true, + }, + }, + { + name: "WhitelistBasic", + list: `c 4:2 rw`, + expected: &Emulator{ + defaultAllow: false, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 4, + minor: 2, + }: configs.DevicePermissions("rw"), + }, + }, + }, + { + name: "WhitelistWildcard", + list: `b 0:* m`, + expected: &Emulator{ + defaultAllow: false, + rules: deviceRules{ + { + node: configs.BlockDevice, + major: 0, + minor: configs.Wildcard, + }: configs.DevicePermissions("m"), + }, + }, + }, + { + name: "WhitelistDuplicate", + list: `c *:* rwm +c 1:1 r`, + expected: &Emulator{ + defaultAllow: false, + rules: deviceRules{ + { + node: configs.CharDevice, + major: configs.Wildcard, + minor: configs.Wildcard, + }: configs.DevicePermissions("rwm"), + // To match the kernel, we allow redundant rules. + { + node: configs.CharDevice, + major: 1, + minor: 1, + }: configs.DevicePermissions("r"), + }, + }, + }, + { + name: "WhitelistComplicated", + list: `c *:* m +b *:* m +c 1:3 rwm +c 1:5 rwm +c 1:7 rwm +c 1:8 rwm +c 1:9 rwm +c 5:0 rwm +c 5:2 rwm +c 136:* rwm +c 10:200 rwm`, + expected: &Emulator{ + defaultAllow: false, + rules: deviceRules{ + { + node: configs.CharDevice, + major: configs.Wildcard, + minor: configs.Wildcard, + }: configs.DevicePermissions("m"), + { + node: configs.BlockDevice, + major: configs.Wildcard, + minor: configs.Wildcard, + }: configs.DevicePermissions("m"), + { + node: configs.CharDevice, + major: 1, + minor: 3, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 1, + minor: 7, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 1, + minor: 8, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 1, + minor: 9, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 5, + minor: 0, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 5, + minor: 2, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 136, + minor: configs.Wildcard, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 10, + minor: 200, + }: configs.DevicePermissions("rwm"), + }, + }, + }, + // Some invalid lists. + { + name: "InvalidFieldNumber", + list: `b 1:0`, + expected: nil, + }, + { + name: "InvalidDeviceType", + list: `p *:* rwm`, + expected: nil, + }, + { + name: "InvalidMajorNumber1", + list: `p -1:3 rwm`, + expected: nil, + }, + { + name: "InvalidMajorNumber2", + list: `c foo:27 rwm`, + expected: nil, + }, + { + name: "InvalidMinorNumber1", + list: `b 1:-4 rwm`, + expected: nil, + }, + { + name: "InvalidMinorNumber2", + list: `b 1:foo rwm`, + expected: nil, + }, + { + name: "InvalidPermissions", + list: `b 1:7 rwk`, + expected: nil, + }, + } + + for _, test := range tests { + test := test // capture range variable + t.Run(test.name, func(t *testing.T) { + list := bytes.NewBufferString(test.list) + emu, err := EmulatorFromList(list) + if err != nil && test.expected != nil { + t.Fatalf("unexpected failure when creating emulator: %v", err) + } else if err == nil && test.expected == nil { + t.Fatalf("unexpected success when creating emulator: %#v", emu) + } + + if !reflect.DeepEqual(emu, test.expected) { + t.Errorf("final emulator state mismatch: %#v != %#v", emu, test.expected) + } + }) + } +} + +func testDeviceEmulatorApply(t *testing.T, baseDefaultAllow bool) { + tests := []struct { + name string + rule configs.DeviceRule + base, expected *Emulator + }{ + // Switch between default modes. + { + name: "SwitchToOtherMode", + rule: configs.DeviceRule{ + Type: configs.WildcardDevice, + Major: configs.Wildcard, + Minor: configs.Wildcard, + Permissions: configs.DevicePermissions("rwm"), + Allow: !baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: configs.Wildcard, + minor: configs.Wildcard, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 1, + minor: 1, + }: configs.DevicePermissions("r"), + }, + }, + expected: &Emulator{ + defaultAllow: !baseDefaultAllow, + rules: nil, + }, + }, + { + name: "SwitchToSameModeNoop", + rule: configs.DeviceRule{ + Type: configs.WildcardDevice, + Major: configs.Wildcard, + Minor: configs.Wildcard, + Permissions: configs.DevicePermissions("rwm"), + Allow: baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: nil, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: nil, + }, + }, + { + name: "SwitchToSameMode", + rule: configs.DeviceRule{ + Type: configs.WildcardDevice, + Major: configs.Wildcard, + Minor: configs.Wildcard, + Permissions: configs.DevicePermissions("rwm"), + Allow: baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: configs.Wildcard, + minor: configs.Wildcard, + }: configs.DevicePermissions("rwm"), + { + node: configs.CharDevice, + major: 1, + minor: 1, + }: configs.DevicePermissions("r"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: nil, + }, + }, + // Rule addition logic. + { + name: "RuleAdditionBasic", + rule: configs.DeviceRule{ + Type: configs.CharDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("rm"), + Allow: !baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 2, + minor: 1, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 2, + minor: 1, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("rm"), + }, + }, + }, + { + name: "RuleAdditionBasicDuplicate", + rule: configs.DeviceRule{ + Type: configs.CharDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("rm"), + Allow: !baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: configs.Wildcard, + }: configs.DevicePermissions("rwm"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: configs.Wildcard, + }: configs.DevicePermissions("rwm"), + // To match the kernel, we allow redundant rules. + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("rm"), + }, + }, + }, + { + name: "RuleAdditionBasicDuplicateNoop", + rule: configs.DeviceRule{ + Type: configs.CharDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("rm"), + Allow: !baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("rm"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("rm"), + }, + }, + }, + { + name: "RuleAdditionMerge", + rule: configs.DeviceRule{ + Type: configs.BlockDevice, + Major: 5, + Minor: 12, + Permissions: configs.DevicePermissions("rm"), + Allow: !baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 2, + minor: 1, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 5, + minor: 12, + }: configs.DevicePermissions("rw"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 2, + minor: 1, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 5, + minor: 12, + }: configs.DevicePermissions("rwm"), + }, + }, + }, + { + name: "RuleAdditionMergeWildcard", + rule: configs.DeviceRule{ + Type: configs.BlockDevice, + Major: 5, + Minor: configs.Wildcard, + Permissions: configs.DevicePermissions("rm"), + Allow: !baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 2, + minor: 1, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 5, + minor: configs.Wildcard, + }: configs.DevicePermissions("rw"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 2, + minor: 1, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 5, + minor: configs.Wildcard, + }: configs.DevicePermissions("rwm"), + }, + }, + }, + { + name: "RuleAdditionMergeNoop", + rule: configs.DeviceRule{ + Type: configs.BlockDevice, + Major: 5, + Minor: 12, + Permissions: configs.DevicePermissions("r"), + Allow: !baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 2, + minor: 1, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 5, + minor: 12, + }: configs.DevicePermissions("rw"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 2, + minor: 1, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 5, + minor: 12, + }: configs.DevicePermissions("rw"), + }, + }, + }, + // Rule removal logic. + { + name: "RuleRemovalBasic", + rule: configs.DeviceRule{ + Type: configs.CharDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("rm"), + Allow: baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("rm"), + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + }, + }, + }, + { + name: "RuleRemovalNonexistent", + rule: configs.DeviceRule{ + Type: configs.CharDevice, + Major: 4, + Minor: 1, + Permissions: configs.DevicePermissions("rw"), + Allow: baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + }, + }, + }, + { + name: "RuleRemovalFull", + rule: configs.DeviceRule{ + Type: configs.CharDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("rw"), + Allow: baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("w"), + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + }, + }, + }, + { + name: "RuleRemovalPartial", + rule: configs.DeviceRule{ + Type: configs.CharDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("r"), + Allow: baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("rm"), + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("m"), + { + node: configs.BlockDevice, + major: 1, + minor: 5, + }: configs.DevicePermissions("r"), + }, + }, + }, + // Check our non-canonical behaviour when it comes to try to "punch + // out" holes in a wildcard rule. + { + name: "RuleRemovalWildcardPunchoutImpossible", + rule: configs.DeviceRule{ + Type: configs.CharDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("r"), + Allow: baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: configs.Wildcard, + }: configs.DevicePermissions("rm"), + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("r"), + }, + }, + expected: nil, + }, + { + name: "RuleRemovalWildcardPunchoutPossible", + rule: configs.DeviceRule{ + Type: configs.CharDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("r"), + Allow: baseDefaultAllow, + }, + base: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: configs.Wildcard, + }: configs.DevicePermissions("wm"), + { + node: configs.CharDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("r"), + }, + }, + expected: &Emulator{ + defaultAllow: baseDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: configs.Wildcard, + }: configs.DevicePermissions("wm"), + }, + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + err := test.base.Apply(test.rule) + if err != nil && test.expected != nil { + t.Fatalf("unexpected failure when applying apply rule: %v", err) + } else if err == nil && test.expected == nil { + t.Fatalf("unexpected success when applying apply rule: %#v", test.base) + } + + if test.expected != nil && !reflect.DeepEqual(test.base, test.expected) { + t.Errorf("final emulator state mismatch: %#v != %#v", test.base, test.expected) + } + }) + } +} + +func TestDeviceEmulatorWhitelistApply(t *testing.T) { + testDeviceEmulatorApply(t, false) +} + +func TestDeviceEmulatorBlacklistApply(t *testing.T) { + testDeviceEmulatorApply(t, true) +} + +func testDeviceEmulatorTransition(t *testing.T, sourceDefaultAllow bool) { + tests := []struct { + name string + source, target *Emulator + expected []*configs.DeviceRule + }{ + // No-op changes. + { + name: "Noop", + source: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: configs.Wildcard, + }: configs.DevicePermissions("wm"), + }, + }, + target: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 42, + minor: configs.Wildcard, + }: configs.DevicePermissions("wm"), + }, + }, + // Identical white-lists produce no extra rules. + expected: nil, + }, + // Switching modes. + { + name: "SwitchToOtherMode", + source: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rwm"), + }, + }, + target: &Emulator{ + defaultAllow: !sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.BlockDevice, + major: 42, + minor: configs.Wildcard, + }: configs.DevicePermissions("wm"), + }, + }, + expected: []*configs.DeviceRule{ + // Clear-all rule. + { + Type: configs.WildcardDevice, + Major: configs.Wildcard, + Minor: configs.Wildcard, + Permissions: configs.DevicePermissions("rwm"), + Allow: !sourceDefaultAllow, + }, + // The actual rule-set. + { + Type: configs.BlockDevice, + Major: 42, + Minor: configs.Wildcard, + Permissions: configs.DevicePermissions("wm"), + Allow: sourceDefaultAllow, + }, + }, + }, + // Rule changes. + { + name: "RuleAddition", + source: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rwm"), + }, + }, + target: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("rwm"), + }, + }, + expected: []*configs.DeviceRule{ + { + Type: configs.BlockDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("rwm"), + Allow: !sourceDefaultAllow, + }, + }, + }, + { + name: "RuleRemoval", + source: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 42, + minor: 1337, + }: configs.DevicePermissions("rwm"), + }, + }, + target: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rwm"), + }, + }, + expected: []*configs.DeviceRule{ + { + Type: configs.BlockDevice, + Major: 42, + Minor: 1337, + Permissions: configs.DevicePermissions("rwm"), + Allow: sourceDefaultAllow, + }, + }, + }, + { + name: "RuleMultipleAdditionRemoval", + source: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rwm"), + { + node: configs.BlockDevice, + major: 3, + minor: 9, + }: configs.DevicePermissions("rw"), + }, + }, + target: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rwm"), + }, + }, + expected: []*configs.DeviceRule{ + { + Type: configs.BlockDevice, + Major: 3, + Minor: 9, + Permissions: configs.DevicePermissions("rw"), + Allow: sourceDefaultAllow, + }, + }, + }, + // Modifying the access permissions. + { + name: "RulePartialAddition", + source: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("r"), + }, + }, + target: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rwm"), + }, + }, + expected: []*configs.DeviceRule{ + { + Type: configs.CharDevice, + Major: 1, + Minor: 2, + Permissions: configs.DevicePermissions("wm"), + Allow: !sourceDefaultAllow, + }, + }, + }, + { + name: "RulePartialRemoval", + source: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rw"), + }, + }, + target: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("w"), + }, + }, + expected: []*configs.DeviceRule{ + { + Type: configs.CharDevice, + Major: 1, + Minor: 2, + Permissions: configs.DevicePermissions("r"), + Allow: sourceDefaultAllow, + }, + }, + }, + { + name: "RulePartialBoth", + source: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rw"), + }, + }, + target: &Emulator{ + defaultAllow: sourceDefaultAllow, + rules: deviceRules{ + { + node: configs.CharDevice, + major: 1, + minor: 2, + }: configs.DevicePermissions("rm"), + }, + }, + expected: []*configs.DeviceRule{ + { + Type: configs.CharDevice, + Major: 1, + Minor: 2, + Permissions: configs.DevicePermissions("w"), + Allow: sourceDefaultAllow, + }, + { + Type: configs.CharDevice, + Major: 1, + Minor: 2, + Permissions: configs.DevicePermissions("m"), + Allow: !sourceDefaultAllow, + }, + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + // If we are in black-list mode, we need to prepend the relevant + // clear-all rule (the expected rule lists are written with + // white-list mode in mind), and then make a full copy of the + // target rules. + if sourceDefaultAllow && test.source.defaultAllow == test.target.defaultAllow { + test.expected = []*configs.DeviceRule{{ + Type: configs.WildcardDevice, + Major: configs.Wildcard, + Minor: configs.Wildcard, + Permissions: configs.DevicePermissions("rwm"), + Allow: test.target.defaultAllow, + }} + for _, rule := range test.target.rules.orderedEntries() { + test.expected = append(test.expected, &configs.DeviceRule{ + Type: rule.meta.node, + Major: rule.meta.major, + Minor: rule.meta.minor, + Permissions: rule.perms, + Allow: !test.target.defaultAllow, + }) + } + } + + rules, err := test.source.Transition(test.target) + if err != nil { + t.Fatalf("unexpected error while calculating transition rules: %#v", err) + } + + if !reflect.DeepEqual(rules, test.expected) { + t.Errorf("rules don't match expected set: %#v != %#v", rules, test.expected) + } + + // Apply the rules to the source to see if it actually transitions + // correctly. This is all emulated but it's a good thing to + // double-check. + for _, rule := range rules { + if err := test.source.Apply(*rule); err != nil { + t.Fatalf("error while applying transition rule [%#v]: %v", rule, err) + } + } + if !reflect.DeepEqual(test.source, test.target) { + t.Errorf("transition incomplete after applying all rules: %#v != %#v", test.source, test.target) + } + }) + } +} + +func TestDeviceEmulatorTransitionFromBlacklist(t *testing.T) { + testDeviceEmulatorTransition(t, true) +} + +func TestDeviceEmulatorTransitionFromWhitelist(t *testing.T) { + testDeviceEmulatorTransition(t, false) +}