Verify correct application of labels and attributes
With project Treble, we're relying heavily on attributes for
permission inheritance and enforcement of separation between
platform and vendor components.
We neead tests that verify those attributes are correctly applied.
This change adds the framework for those tests including a wrapper
around libsepol for loading and querying policy, and a python module
for running tests on policy and file_contexts.
Included with the testing framework is a test asserting that the
coredomain attribute is only applied to core processes. This
verification is done using the following rules:
1. Domain's entrypoint is on /system - coredomain
2. Domain's entrypoint is on /vendor - not coredomain
3. Domain belongs to a whitelist of known coredomains - coredomain
In a subsequent commit these tests will be applied at build time.
However, I first need to fix existing Treble violations exposed by
this test. These tests will also be applied during CTS.
Test: LD_PRELOAD=$ANDROID_HOST_OUT/lib64/libsepolwrap.so python \
treble.py -p $OUT/vendor/etc/selinux/precompiled_sepolicy \
-f $OUT/vendor/etc/selinux/nonplat_file_contexts \
-f $OUT/system/etc/selinux/plat_file_contexts
Bug: 37008075
Change-Id: I7825f5c2909a5801deaccf2bef2bfd227adb0ae9
(cherry picked from commit 0366afdf14000da84e5350fff169346594639488)
diff --git a/tests/treble.py b/tests/treble.py
new file mode 100644
index 0000000..901f702
--- /dev/null
+++ b/tests/treble.py
@@ -0,0 +1,257 @@
+from optparse import OptionParser
+from optparse import Option, OptionValueError
+import os
+import policy
+import re
+import sys
+
+'''
+Use file_contexts and policy to verify Treble requirements
+are not violated.
+'''
+###
+# Differentiate between domains that are part of the core Android platform and
+# domains introduced by vendors
+coreAppdomain = {
+ 'bluetooth',
+ 'ephemeral_app',
+ 'isolated_app',
+ 'nfc',
+ 'platform_app',
+ 'priv_app',
+ 'radio',
+ 'shared_relro',
+ 'shell',
+ 'system_app',
+ 'untrusted_app',
+ 'untrusted_app_25',
+ 'untrusted_v2_app',
+ }
+coredomainWhitelist = {
+ 'adbd',
+ 'kernel',
+ 'postinstall',
+ 'postinstall_dexopt',
+ 'recovery',
+ 'system_server',
+ }
+coredomainWhitelist |= coreAppdomain
+
+class scontext:
+ def __init__(self):
+ self.fromSystem = False
+ self.fromVendor = False
+ self.coredomain = False
+ self.appdomain = False
+ self.attributes = set()
+ self.entrypoints = []
+ self.entrypointpaths = []
+
+def PrintScontext(domain, sctx):
+ print domain
+ print "\tcoredomain="+str(sctx.coredomain)
+ print "\tappdomain="+str(sctx.appdomain)
+ print "\tfromSystem="+str(sctx.fromSystem)
+ print "\tfromVendor="+str(sctx.fromVendor)
+ print "\tattributes="+str(sctx.attributes)
+ print "\tentrypoints="+str(sctx.entrypoints)
+ print "\tentrypointpaths="
+ if sctx.entrypointpaths is not None:
+ for path in sctx.entrypointpaths:
+ print "\t\t"+str(path)
+
+alldomains = {}
+coredomains = set()
+appdomains = set()
+vendordomains = set()
+
+###
+# Check whether the regex will match a file path starting with the provided
+# prefix
+#
+# Compares regex entries in file_contexts with a path prefix. Regex entries
+# are often more specific than this file prefix. For example, the regex could
+# be /system/bin/foo\.sh and the prefix could be /system. This function
+# loops over the regex removing characters from the end until
+# 1) there is a match - return True or 2) run out of characters - return
+# False.
+#
+def MatchPathPrefix(pathregex, prefix):
+ for i in range(len(pathregex), 0, -1):
+ try:
+ pattern = re.compile('^' + pathregex[0:i] + "$")
+ except:
+ continue
+ if pattern.match(prefix):
+ return True
+ return False
+
+def GetAllDomains(pol):
+ global alldomains
+ for result in pol.QueryTypeAttribute("domain", True):
+ alldomains[result] = scontext()
+
+def GetAppDomains():
+ global appdomains
+ global alldomains
+ for d in alldomains:
+ # The application of the "appdomain" attribute is trusted because core
+ # selinux policy contains neverallow rules that enforce that only zygote
+ # and runas spawned processes may transition to processes that have
+ # the appdomain attribute.
+ if "appdomain" in alldomains[d].attributes:
+ alldomains[d].appdomain = True
+ appdomains.add(d)
+
+
+def GetCoreDomains():
+ global alldomains
+ global coredomains
+ for d in alldomains:
+ # TestCoredomainViolators will verify if coredomain was incorrectly
+ # applied.
+ if "coredomain" in alldomains[d].attributes:
+ alldomains[d].coredomain = True
+ coredomains.add(d)
+ # check whether domains are executed off of /system or /vendor
+ if d in coredomainWhitelist:
+ continue
+ # TODO, add checks to prevent app domains from being incorrectly
+ # labeled as coredomain. Apps don't have entrypoints as they're always
+ # dynamically transitioned to by zygote.
+ if d in appdomains:
+ continue
+ if not alldomains[d].entrypointpaths:
+ continue
+ for path in alldomains[d].entrypointpaths:
+ # Processes with entrypoint on /system
+ if ((MatchPathPrefix(path, "/system") and not
+ MatchPathPrefix(path, "/system/vendor")) or
+ MatchPathPrefix(path, "/init") or
+ MatchPathPrefix(path, "/charger")):
+ alldomains[d].fromSystem = True
+ # Processes with entrypoint on /vendor or /system/vendor
+ if (MatchPathPrefix(path, "/vendor") or
+ MatchPathPrefix(path, "/system/vendor")):
+ alldomains[d].fromVendor = True
+
+###
+# Add the entrypoint type and path(s) to each domain.
+#
+def GetDomainEntrypoints(pol):
+ global alldomains
+ for x in pol.QueryTERule(tclass="file", perms=["entrypoint"]):
+ if not x.sctx in alldomains:
+ continue
+ alldomains[x.sctx].entrypoints.append(str(x.tctx))
+ # postinstall_file represents a special case specific to A/B OTAs.
+ # Update_engine mounts a partition and relabels it postinstall_file.
+ # There is no file_contexts entry associated with postinstall_file
+ # so skip the lookup.
+ if x.tctx == "postinstall_file":
+ continue
+ alldomains[x.sctx].entrypointpaths = pol.QueryFc(x.tctx)
+###
+# Get attributes associated with each domain
+#
+def GetAttributes(pol):
+ global alldomains
+ for domain in alldomains:
+ for result in pol.QueryTypeAttribute(domain, False):
+ alldomains[domain].attributes.add(result)
+
+def setup(pol):
+ GetAllDomains(pol)
+ GetAttributes(pol)
+ GetDomainEntrypoints(pol)
+ GetAppDomains()
+ GetCoreDomains()
+
+#############################################################
+# Tests
+#############################################################
+def TestCoredomainViolations():
+ global alldomains
+ # verify that all domains launched from /system have the coredomain
+ # attribute
+ ret = ""
+ violators = []
+ for d in alldomains:
+ domain = alldomains[d]
+ if domain.fromSystem and "coredomain" not in domain.attributes:
+ violators.append(d);
+ if len(violators) > 0:
+ ret += "The following domain(s) must be associated with the "
+ ret += "\"coredomain\" attribute because they are executed off of "
+ ret += "/system:\n"
+ ret += " ".join(str(x) for x in sorted(violators)) + "\n"
+
+ # verify that all domains launched form /vendor do not have the coredomain
+ # attribute
+ violators = []
+ for d in alldomains:
+ domain = alldomains[d]
+ if domain.fromVendor and "coredomain" in domain.attributes:
+ violators.append(d)
+ if len(violators) > 0:
+ ret += "The following domains must not be associated with the "
+ ret += "\"coredomain\" attribute because they are executed off of "
+ ret += "/vendor or /system/vendor:\n"
+ ret += " ".join(str(x) for x in sorted(violators)) + "\n"
+
+ return ret
+
+###
+# extend OptionParser to allow the same option flag to be used multiple times.
+# This is used to allow multiple file_contexts files and tests to be
+# specified.
+#
+class MultipleOption(Option):
+ ACTIONS = Option.ACTIONS + ("extend",)
+ STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",)
+ TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",)
+ ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",)
+
+ def take_action(self, action, dest, opt, value, values, parser):
+ if action == "extend":
+ values.ensure_value(dest, []).append(value)
+ else:
+ Option.take_action(self, action, dest, opt, value, values, parser)
+
+Tests = ["CoredomainViolators"]
+
+if __name__ == '__main__':
+ usage = "sepolicy-trebletests -f nonplat_file_contexts -f "
+ usage +="plat_file_contexts -p policy [--test test] [--help]"
+ parser = OptionParser(option_class=MultipleOption, usage=usage)
+ parser.add_option("-f", "--file_contexts", dest="file_contexts",
+ metavar="FILE", action="extend", type="string")
+ parser.add_option("-p", "--policy", dest="policy", metavar="FILE")
+ parser.add_option("-t", "--test", dest="test", action="extend",
+ help="Test options include "+str(Tests))
+
+ (options, args) = parser.parse_args()
+
+ if not options.policy:
+ sys.exit("Must specify monolithic policy file\n" + parser.usage)
+ if not os.path.exists(options.policy):
+ sys.exit("Error: policy file " + options.policy + " does not exist\n"
+ + parser.usage)
+
+ if not options.file_contexts:
+ sys.exit("Error: Must specify file_contexts file(s)\n" + parser.usage)
+ for f in options.file_contexts:
+ if not os.path.exists(f):
+ sys.exit("Error: File_contexts file " + f + " does not exist\n" +
+ parser.usage)
+
+ pol = policy.Policy(options.policy, options.file_contexts)
+ setup(pol)
+
+ results = ""
+ # If an individual test is not specified, run all tests.
+ if options.test is None or "CoredomainViolations" in options.tests:
+ results += TestCoredomainViolations()
+
+ if len(results) > 0:
+ sys.exit(results)