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/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)