Add apex_sepolicy_tests
This is to prevent common mistakes when building an APEX. For example,
etc/vintf should be read-able by servicemanager.
Bug: 267269895
Test: apex_sepolicy_tests -f <(deapexer list --dir -Z foo.apex)
Test: atest apex_sepolicy_tests_test
Change-Id: I2e86096add1bb4c9daa0e841b10732c16a09efa3
diff --git a/tests/Android.bp b/tests/Android.bp
index e271346..719f9a0 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -70,6 +70,39 @@
}
python_binary_host {
+ name: "apex_sepolicy_tests",
+ srcs: [
+ "apex_sepolicy_tests.py",
+ ],
+ version: {
+ py3: {
+ embedded_launcher: true,
+ },
+ },
+ libs: ["pysepolwrap"],
+ data: [
+ ":libsepolwrap",
+ ":precompiled_sepolicy",
+ ],
+}
+
+python_test_host {
+ name: "apex_sepolicy_tests_test",
+ srcs: [
+ "apex_sepolicy_tests.py",
+ "apex_sepolicy_tests_test.py",
+ ],
+ libs: ["pysepolwrap"],
+ data: [
+ ":libsepolwrap",
+ ":precompiled_sepolicy",
+ ],
+ test_options: {
+ unit_test: true,
+ },
+}
+
+python_binary_host {
name: "searchpolicy",
srcs: [
"searchpolicy.py",
diff --git a/tests/apex_sepolicy_tests.py b/tests/apex_sepolicy_tests.py
new file mode 100644
index 0000000..2cdde3c
--- /dev/null
+++ b/tests/apex_sepolicy_tests.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+#
+# Copyright 2023 The Android Open Source Project
+#
+# 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.
+""" A tool to test APEX file_contexts
+
+Usage:
+ $ deapexer list -Z foo.apex > /tmp/fc
+ $ apex_sepolicy_tests -f /tmp/fc
+"""
+
+
+import argparse
+import os
+import pathlib
+import pkgutil
+import re
+import sys
+import tempfile
+from dataclasses import dataclass
+from typing import List
+
+import policy
+
+
+SHARED_LIB_EXTENSION = '.dylib' if sys.platform == 'darwin' else '.so'
+LIBSEPOLWRAP = "libsepolwrap" + SHARED_LIB_EXTENSION
+
+
+@dataclass
+class Is:
+ """Exact matcher for a path."""
+ path: str
+
+
+@dataclass
+class Glob:
+ """Path matcher with pathlib.PurePath.match"""
+ pattern: str
+
+
+@dataclass
+class Regex:
+ """Path matcher with re.match"""
+ pattern: str
+
+
+Matcher = Is | Glob | Regex
+
+@dataclass
+class AllowRead:
+ """Rule checking if scontext can read the entity"""
+ tclass: str
+ scontext: set[str]
+
+
+Rule = AllowRead
+
+
+def match_path(path: str, matcher: Matcher) -> bool:
+ """True if path matches with the given matcher"""
+ match matcher:
+ case Is(target):
+ return path == target
+ case Glob(pattern):
+ return pathlib.PurePath(path).match(pattern)
+ case Regex(pattern):
+ return re.match(pattern, path)
+
+
+def check_rule(pol, path: str, tcontext: str, rule: Rule) -> List[str]:
+ """Returns error message if scontext can't read the target"""
+ match rule:
+ case AllowRead(tclass, scontext):
+ te_rules = list(pol.QueryTERule(scontext=scontext,
+ tcontext={tcontext},
+ tclass={tclass},
+ perms={'read'}))
+ if len(te_rules) > 0:
+ return [] # no errors
+
+ return [f"Error: {path}: {scontext} can't read. (tcontext={tcontext})"]
+
+
+rules = [
+ # permissions
+ (Is('./etc/permissions/'), AllowRead('dir', {'system_server'})),
+ (Glob('./etc/permissions/*.xml'), AllowRead('file', {'system_server'})),
+ # init scripts with optional SDK version (e.g. foo.rc, foo.32rc)
+ (Regex('\./etc/.*\.\d*rc'), AllowRead('file', {'init'})),
+ # vintf fragments
+ (Is('./etc/vintf/'), AllowRead('dir', {'servicemanager', 'apexd'})),
+ (Glob('./etc/vintf/*.xml'), AllowRead('file', {'servicemanager', 'apexd'})),
+ # ./ and apex_manifest.pb
+ (Is('./apex_manifest.pb'), AllowRead('file', {'linkerconfig', 'apexd'})),
+ (Is('./'), AllowRead('dir', {'linkerconfig', 'apexd'})),
+]
+
+
+def check_line(pol: policy.Policy, line: str) -> List[str]:
+ """Parses a file_contexts line and runs checks"""
+ # skip empty/comment line
+ line = line.strip()
+ if line == '' or line[0] == '#':
+ return []
+
+ # parse
+ split = line.split()
+ if len(split) != 2:
+ return [f"Error: invalid file_contexts: {line}"]
+ path, context = split[0], split[1]
+ if len(context.split(':')) != 4:
+ return [f"Error: invalid file_contexts: {line}"]
+ tcontext = context.split(':')[2]
+
+ # check rules
+ errors = []
+ for matcher, rule in rules:
+ if match_path(path, matcher):
+ errors.extend(check_rule(pol, path, tcontext, rule))
+ return errors
+
+
+def extract_data(name, temp_dir):
+ out_path = os.path.join(temp_dir, name)
+ with open(out_path, 'wb') as f:
+ blob = pkgutil.get_data('apex_sepolicy_tests', name)
+ if not blob:
+ sys.exit(f"Error: {name} does not exist. Is this binary corrupted?\n")
+ f.write(blob)
+ return out_path
+
+
+def do_main(work_dir):
+ """Do testing"""
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-f', '--file_contexts', help='output of "deapexer list -Z"')
+ args = parser.parse_args()
+
+ lib_path = extract_data(LIBSEPOLWRAP, work_dir)
+ policy_path = extract_data('precompiled_sepolicy', work_dir)
+ pol = policy.Policy(policy_path, None, lib_path)
+
+ errors = []
+ with open(args.file_contexts, 'rt', encoding='utf-8') as file_contexts:
+ for line in file_contexts:
+ errors.extend(check_line(pol, line))
+ if len(errors) > 0:
+ sys.exit('\n'.join(errors))
+
+
+if __name__ == '__main__':
+ with tempfile.TemporaryDirectory() as temp_dir:
+ do_main(temp_dir)
diff --git a/tests/apex_sepolicy_tests_test.py b/tests/apex_sepolicy_tests_test.py
new file mode 100644
index 0000000..125290c
--- /dev/null
+++ b/tests/apex_sepolicy_tests_test.py
@@ -0,0 +1,93 @@
+# Copyright 2023 The Android Open Source Project
+#
+# 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.
+"""Tests for apex_sepolicy_tests"""
+
+import re
+import shutil
+import tempfile
+import unittest
+
+import apex_sepolicy_tests as apex
+import policy
+
+
+# pylint: disable=missing-docstring
+class ApexSepolicyTests(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ cls.temp_dir = tempfile.mkdtemp()
+ lib_path = apex.extract_data(apex.LIBSEPOLWRAP, cls.temp_dir)
+ policy_path = apex.extract_data('precompiled_sepolicy', cls.temp_dir)
+ cls.pol = policy.Policy(policy_path, None, lib_path)
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ shutil.rmtree(cls.temp_dir)
+
+ # helpers
+
+ @property
+ def pol(self):
+ return self.__class__.pol
+
+ def assert_ok(self, line: str):
+ errors = apex.check_line(self.pol, line)
+ self.assertEqual(errors, [], "Should be no errors")
+
+ def assert_error(self, line: str, expected_error: str):
+ pattern = re.compile(expected_error)
+ errors = apex.check_line(self.pol, line)
+ for err in errors:
+ if re.search(pattern, err):
+ return
+ self.fail(f"Expected error '{expected_error}' is not found in {errors}")
+
+ # tests
+
+ def test_parse_lines(self):
+ self.assert_ok('# commented line')
+ self.assert_ok('') # empty line
+ self.assert_error('./path1 invalid_contexts',
+ r'Error: invalid file_contexts: .*')
+ self.assert_error('./path1 u:object_r:vendor_file',
+ r'Error: invalid file_contexts: .*')
+ self.assert_ok('./path1 u:object_r:vendor_file:s0')
+
+ def test_vintf(self):
+ self.assert_ok('./etc/vintf/fragment.xml u:object_r:vendor_configs_file:s0')
+ self.assert_error('./etc/vintf/fragment.xml u:object_r:vendor_file:s0',
+ r'Error: \./etc/vintf/fragment\.xml: .* can\'t read')
+
+ def test_permissions(self):
+ self.assert_ok('./etc/permissions/permisssion.xml u:object_r:vendor_configs_file:s0')
+ self.assert_error('./etc/permissions/permisssion.xml u:object_r:vendor_file:s0',
+ r'Error: \./etc/permissions/permisssion.xml: .* can\'t read')
+
+ def test_initscripts(self):
+ # init reads .rc file
+ self.assert_ok('./etc/init.rc u:object_r:vendor_file:s0')
+ self.assert_error('./etc/init.rc u:object_r:unknown:s0',
+ r'Error: .* can\'t read')
+ # init reads .#rc file
+ self.assert_ok('./etc/init.32rc u:object_r:vendor_file:s0')
+ self.assert_error('./etc/init.32rc u:object_r:unknown:s0',
+ r'Error: .* can\'t read')
+ # init skips file with unknown extension => no errors
+ self.assert_ok('./etc/init.x32rc u:object_r:vendor_file:s0')
+ self.assert_ok('./etc/init.x32rc u:object_r:unknown:s0')
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)