neverallows in Soong

Straightforward way of expressing policy inspired by a similar
syntax in SELinux.

Bug: 70165717
Test: no neverallows hit
Test: manually checking neverallow rules by changing them/adding violations
Change-Id: I7e15a0094d1861391bfe21a2ea30797d7593c142
diff --git a/android/neverallow.go b/android/neverallow.go
new file mode 100644
index 0000000..261f2ee
--- /dev/null
+++ b/android/neverallow.go
@@ -0,0 +1,273 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// 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.
+
+package android
+
+import (
+	"path/filepath"
+	"reflect"
+	"strconv"
+	"strings"
+
+	"github.com/google/blueprint/proptools"
+)
+
+// "neverallow" rules for the build system.
+//
+// This allows things which aren't related to the build system and are enforced
+// for sanity, in progress code refactors, or policy to be expressed in a
+// straightforward away disjoint from implementations and tests which should
+// work regardless of these restrictions.
+//
+// A module is disallowed if all of the following are true:
+// - it is in one of the "in" paths
+// - it is not in one of the "notIn" paths
+// - it has all "with" properties matched
+// - - values are matched in their entirety
+// - - nil is interpreted as an empty string
+// - - nested properties are separated with a '.'
+// - - if the property is a list, any of the values in the list being matches
+//     counts as a match
+// - it has none of the "without" properties matched (same rules as above)
+
+func registerNeverallowMutator(ctx RegisterMutatorsContext) {
+	ctx.BottomUp("neverallow", neverallowMutator).Parallel()
+}
+
+var neverallows = []*rule{
+	neverallow().in("vendor", "device").with("vndk.enabled", "true").
+		because("the VNDK can never contain a library that is device dependent."),
+	neverallow().with("vndk.enabled", "true").without("owner", "").
+		because("a VNDK module can never have an owner."),
+	neverallow().notIn("libcore").with("no_standard_libs", "true"),
+
+	// TODO(b/67974785): always enforce the manifest
+	neverallow().
+		without("name", "libhidltransport").
+		with("product_variables.enforce_vintf_manifest.cflags", "*").
+		because("manifest enforcement should be independent of ."),
+
+	// TODO(b/67975799): vendor code should always use /vendor/bin/sh
+	neverallow().
+		without("name", "libc_bionic_ndk").
+		with("product_variables.treble_linker_namespaces.cflags", "*").
+		because("nothing should care if linker namespaces are enabled or not"),
+
+	// Example:
+	// *neverallow().with("Srcs", "main.cpp"),
+}
+
+func neverallowMutator(ctx BottomUpMutatorContext) {
+	m, ok := ctx.Module().(Module)
+	if !ok {
+		return
+	}
+
+	dir := ctx.ModuleDir() + "/"
+	properties := m.GetProperties()
+
+	for _, n := range neverallows {
+		if !n.appliesToPath(dir) {
+			continue
+		}
+
+		if !n.appliesToProperties(properties) {
+			continue
+		}
+
+		ctx.ModuleErrorf("violates " + n.String())
+	}
+}
+
+type ruleProperty struct {
+	fields []string // e.x.: Vndk.Enabled
+	value  string   // e.x.: true
+}
+
+type rule struct {
+	// User string for why this is a thing.
+	reason string
+
+	paths       []string
+	unlessPaths []string
+
+	props       []ruleProperty
+	unlessProps []ruleProperty
+}
+
+func neverallow() *rule {
+	return &rule{}
+}
+func (r *rule) in(path ...string) *rule {
+	r.paths = append(r.paths, cleanPaths(path)...)
+	return r
+}
+func (r *rule) notIn(path ...string) *rule {
+	r.unlessPaths = append(r.unlessPaths, cleanPaths(path)...)
+	return r
+}
+func (r *rule) with(properties, value string) *rule {
+	r.props = append(r.props, ruleProperty{
+		fields: fieldNamesForProperties(properties),
+		value:  value,
+	})
+	return r
+}
+func (r *rule) without(properties, value string) *rule {
+	r.unlessProps = append(r.unlessProps, ruleProperty{
+		fields: fieldNamesForProperties(properties),
+		value:  value,
+	})
+	return r
+}
+func (r *rule) because(reason string) *rule {
+	r.reason = reason
+	return r
+}
+
+func (r *rule) String() string {
+	s := "neverallow"
+	for _, v := range r.paths {
+		s += " dir:" + v + "*"
+	}
+	for _, v := range r.unlessPaths {
+		s += " -dir:" + v + "*"
+	}
+	for _, v := range r.props {
+		s += " " + strings.Join(v.fields, ".") + "=" + v.value
+	}
+	for _, v := range r.unlessProps {
+		s += " -" + strings.Join(v.fields, ".") + "=" + v.value
+	}
+	if len(r.reason) != 0 {
+		s += " which is restricted because " + r.reason
+	}
+	return s
+}
+
+func (r *rule) appliesToPath(dir string) bool {
+	includePath := len(r.paths) == 0 || hasAnyPrefix(dir, r.paths)
+	excludePath := hasAnyPrefix(dir, r.unlessPaths)
+	return includePath && !excludePath
+}
+
+func (r *rule) appliesToProperties(properties []interface{}) bool {
+	includeProps := hasAllProperties(properties, r.props)
+	excludeProps := hasAnyProperty(properties, r.unlessProps)
+	return includeProps && !excludeProps
+}
+
+// assorted utils
+
+func cleanPaths(paths []string) []string {
+	res := make([]string, len(paths))
+	for i, v := range paths {
+		res[i] = filepath.Clean(v) + "/"
+	}
+	return res
+}
+
+func fieldNamesForProperties(propertyNames string) []string {
+	names := strings.Split(propertyNames, ".")
+	for i, v := range names {
+		names[i] = proptools.FieldNameForProperty(v)
+	}
+	return names
+}
+
+func hasAnyPrefix(s string, prefixes []string) bool {
+	for _, prefix := range prefixes {
+		if strings.HasPrefix(s, prefix) {
+			return true
+		}
+	}
+	return false
+}
+
+func hasAnyProperty(properties []interface{}, props []ruleProperty) bool {
+	for _, v := range props {
+		if hasProperty(properties, v) {
+			return true
+		}
+	}
+	return false
+}
+
+func hasAllProperties(properties []interface{}, props []ruleProperty) bool {
+	for _, v := range props {
+		if !hasProperty(properties, v) {
+			return false
+		}
+	}
+	return true
+}
+
+func hasProperty(properties []interface{}, prop ruleProperty) bool {
+	for _, propertyStruct := range properties {
+		propertiesValue := reflect.ValueOf(propertyStruct).Elem()
+		for _, v := range prop.fields {
+			if !propertiesValue.IsValid() {
+				break
+			}
+			propertiesValue = propertiesValue.FieldByName(v)
+		}
+		if !propertiesValue.IsValid() {
+			continue
+		}
+
+		check := func(v string) bool {
+			return prop.value == "*" || prop.value == v
+		}
+
+		if matchValue(propertiesValue, check) {
+			return true
+		}
+	}
+	return false
+}
+
+func matchValue(value reflect.Value, check func(string) bool) bool {
+	if !value.IsValid() {
+		return false
+	}
+
+	if value.Kind() == reflect.Ptr {
+		if value.IsNil() {
+			return check("")
+		}
+		value = value.Elem()
+	}
+
+	switch value.Kind() {
+	case reflect.String:
+		return check(value.String())
+	case reflect.Bool:
+		return check(strconv.FormatBool(value.Bool()))
+	case reflect.Int:
+		return check(strconv.FormatInt(value.Int(), 10))
+	case reflect.Slice:
+		slice, ok := value.Interface().([]string)
+		if !ok {
+			panic("Can only handle slice of string")
+		}
+		for _, v := range slice {
+			if check(v) {
+				return true
+			}
+		}
+		return false
+	}
+
+	panic("Can't handle type: " + value.Kind().String())
+}