blob: 0e728ffd981a7d856fea2fca7ad03319ff9f3958 [file] [log] [blame] [edit]
#!/usr/bin/env python
# Copyright 2025 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.
"""Verify that Treble labeling are enforced.
Treble labeling means the following:
1) All platform processes are marked as "coredomain".
2) All platform processes are labeled by platform SEPolicy.
3) All platform files are labeled by platform SEPolicy.
4) All vendor processes are NOT marked as "coredomain".
Note that vendor processes can be "labeled" by platform SEPolicy, if the
processes are generic to all vendors, such as sh / toybox / etc.
"""
import argparse
import collections
import os
import pkgutil
import re
import subprocess
import sys
import tempfile
from concurrent.futures import ThreadPoolExecutor, as_completed
import fc_sort
import policy
import yaml
SHARED_LIB_EXTENSION = '.dylib' if sys.platform == 'darwin' else '.so'
App = collections.namedtuple("App", ["package_name", "apk_name"])
ContextEntry = collections.namedtuple("ContextEntry", ["partition", "values"])
def build_argument_parser():
parser = argparse.ArgumentParser()
parser.add_argument(
"--platform_apks",
dest="platform_apks",
metavar="FILE",
help="Path to a file containing a list of platform apks (one per line)",
required=True,
)
parser.add_argument(
"--vendor_apks",
dest="vendor_apks",
metavar="FILE",
help="Path to a file containing a list of vendor apks (one per line)",
required=True,
)
parser.add_argument(
"--precompiled_sepolicy_without_vendor",
dest="precompiled_sepolicy_without_vendor",
metavar="FILE",
help="Path to the precompiled SEPolicy file (excluding vendor)",
required=True,
)
parser.add_argument(
"--precompiled_sepolicy",
dest="precompiled_sepolicy",
metavar="FILE",
help="Path to the precompiled SEPolicy file (including vendor)",
required=True,
)
parser.add_argument(
"--platform_seapp_contexts",
dest="platform_seapp_contexts",
metavar="FILE",
nargs="+",
help="Path to the Platform seapp_contexts file",
required=True,
)
parser.add_argument(
"--vendor_file_contexts",
dest="vendor_file_contexts",
metavar="FILE",
nargs="+",
help="Path to the Vendor file_contexts file",
required=True,
)
parser.add_argument(
"--vendor_seapp_contexts",
dest="vendor_seapp_contexts",
metavar="FILE",
nargs="+",
help="Path to the Vendor seapp_contexts file",
required=True,
)
parser.add_argument(
"--aapt2_path",
dest="aapt2_path",
metavar="FILE",
help="Path to the aapt2 executable.",
required=True,
)
parser.add_argument(
"--tracking_list_file",
dest="tracking_list_file",
metavar="FILE",
help="Path to the yaml file containing a tracking_list_file.",
required=False,
)
parser.add_argument(
"--treat_as_warnings",
dest="treat_as_warnings",
action="store_true",
help="Treat test failures as warnings instead of errors.",
required=False,
)
parser.add_argument(
"--debuggable",
action="store_true",
help="Exempt treble_labeling_violators from tests.",
required=False,
)
return parser
def TestNoCoredomainInVendorSepolicy(pol_without_vendor, pol, allowlist,
exempted_types):
"""Tests if vendor SEPolicy adds more coredomain types.
coredomain is only for platform types, so vendor SEPolicy must not add one.
"""
platform_coredomain_types = pol_without_vendor.QueryTypeAttribute(
"coredomain", True)
coredomain_types = pol.QueryTypeAttribute("coredomain", True)
allowed_types = {entry["domain"] for entry in allowlist}
violations = coredomain_types - platform_coredomain_types - allowed_types
violations -= exempted_types
if violations:
return ("ERROR: The attribute 'coredomain' is only for platform "
"SEPolicy, and must not be added by vendor SEPolicy.\n"
"Please fix these by doing one of these.\n"
"1) Move these type definitions to system, system_ext, or "
"product's SEPolicy.\n"
"2) Remove 'coredomain' from these types.\n"
"Violations:\n"
f"{'\n'.join(violations)}\n\n")
return ""
def TestNoPlatformFilesInVendorFileContexts(vendor_file_contexts, allowlist):
"""Tests if vendor SEPolicy labels any of platform files.
"""
platform_prefixes = ["/system/", "/system_ext/", "/product/"]
allowed_platform_prefixes = ["/system/vendor/"]
fc_sorted = fc_sort.sort(vendor_file_contexts)
allowed_paths = {entry["path"] for entry in allowlist}
violations = ""
for entry in fc_sorted:
if entry.path in allowed_paths:
continue
if policy.MatchPathPrefixes(entry.path, allowed_platform_prefixes):
continue
if policy.MatchPathPrefixes(entry.path, platform_prefixes):
violations += f"{entry.path}\n"
if violations:
return ("ERROR: Vendor's file_contexts must not label files in "
"platform partitions.\nPlease fix these by doing one of "
"these.\n"
"1) Move corresponding file_contexts entries to correct "
"partition's file_contexts.\n"
"2) Move corresponding files to vendor or odm partition, and "
"then update file_contexts entries.\n"
"Violations:\n"
f"{violations}\n\n")
return ""
def parse_package_names(apk_list_file, aapt2_path):
package_names = []
with open(apk_list_file, "r", encoding="utf-8") as f:
apk_paths = f.read().split()
def extract_package_name(apk_path):
apk_name = os.path.basename(apk_path)
try:
package_name = subprocess.check_output(
[aapt2_path, "dump", "packagename", apk_path],
text=True,
encoding="utf-8"
).strip()
return App(package_name, apk_name)
except subprocess.CalledProcessError as e:
sys.exit(f"Failed to extract package name from {apk_path}: {e}")
with ThreadPoolExecutor() as executor:
futures = [executor.submit(extract_package_name, apk_path)
for apk_path in apk_paths]
for future in as_completed(futures):
package_names.append(future.result())
return package_names
def find_partition_of_contexts(contexts_path):
basename = os.path.basename(contexts_path)
partition_prefixes = {
"plat_": "system",
"system_ext_": "system_ext",
"product_": "product",
"vendor_": "vendor",
"odm_": "odm",
}
for prefix, partition in partition_prefixes.items():
if basename.startswith(prefix):
return partition
sys.exit(f"Can't guess the partition from '{contexts_path}', expected one "
f"of {list(partition_prefixes.keys())}\n")
def parse_seapp_contexts_values(line: str):
values = collections.defaultdict(str)
for token in line.split():
try:
key, value = token.split("=")
except ValueError:
sys.exit(f"malformed seapp_contexts line: '{line}' contains a "
"token without '='\n")
if key in values:
sys.exit(f"malformed seapp_contexts line: '{line}' contains two or "
"more '{key}='\n")
values[key] = value
# store the raw line for error messages.
values["_raw"] = line
return values
def parse_seapp_contexts(seapp_contexts_path):
entries = []
partition = find_partition_of_contexts(seapp_contexts_path)
with open(seapp_contexts_path, "r", encoding="utf-8") as f:
for line in f:
if "#" in line:
line = line[:line.find("#")]
line = line.strip()
if not line or line.startswith("neverallow"):
continue
entries.append(ContextEntry(partition,
parse_seapp_contexts_values(line)))
return entries
def package_name_match(package_name, pattern):
"""Checks if a package name matches a given pattern.
The pattern can end with an asterisk (*) for prefix matching
(case-insensitive). Otherwise, it performs an exact case-insensitive match.
This name matching code is from seapp_context_lookup_internal function in
external/selinux/libselinux/src/android/android_seapp.c.
"""
package_name = package_name.lower()
pattern = pattern.lower()
if pattern.endswith("*"):
return package_name.startswith(pattern[:-1])
else:
return package_name == pattern
def TestNoPlatformAppsInVendorSeappContexts(platform_apps,
vendor_seapp_entries,
allowlist,
exempted_types):
"""Tests if vendor SEPolicy labels any of preinstalled platform apps.
Platform apps can't be labeled with vendor SEPolicy. Also vendor's
seapp_contexts must use 'name=' condition, because vendor's seapp_contexts
shouldn't need generic entries.
"""
no_name_violations = []
partition_violations = []
allowed_apks = {entry["apk"] for entry in allowlist if "apk" in entry}
allowed_names = {entry["name"] for entry in allowlist if "name" in entry}
for entry in vendor_seapp_entries:
values = entry.values
# Entries in vendor seapp contexts must have "name={name}" condition
if "name" not in values or not values["name"]:
no_name_violations.append((entry.partition, values["_raw"]))
continue
if values["domain"] in exempted_types:
continue
# Such name patterns must not match with platform_apps
for app in platform_apps:
if app.apk_name in allowed_apks:
continue
if app.package_name in allowed_names:
continue
if package_name_match(app.package_name, values["name"]):
partition_violations.append((app.apk_name, app.package_name,
entry.partition, values["_raw"]))
break
result = ""
if no_name_violations:
result += ("ERROR: Entries without 'name=' condition are not allowed "
"in vendor seapp_contexts.\nPlease fix the following "
"violations by adding 'name=<package_name>' condition.\n"
"Violations:\n")
for partition, raw_line in no_name_violations:
result += f" seapp_contexts partition: {partition}\n"
result += f" line: '{raw_line}'\n"
result += "---\n"
result += "\n"
if partition_violations:
result += ("ERROR: Platform apps cannot be labeled within vendor's "
"seapp_contexts.\nPlease fix the following violations by "
"doing one of these.\n"
"1) Move these apps to vendor or odm partition, and then "
"label the app with types without 'coredomain'.\n"
"2) Move these seapp_contexts entries to system, system_ext,"
" or product partition's seapp_contexts, and then label the "
"apps with 'coredomain' types.\n"
"Violations:\n")
for apk_name, package_name, partition, raw_line in partition_violations:
result += f" APK name: {apk_name}\n"
result += f" package name: {package_name}\n"
result += f" seapp_contexts partition: {partition}\n"
result += f" line: '{raw_line}'\n"
result += "---\n"
result += "\n"
return result
def TestCoredomainForAllPlatformApps(platform_apps, platform_seapp_entries,
pol, allowlist, exempted_types):
"""Tests if platform SEPolicy labels any of preinstalled platform apps with
a non-coredomain label. All preinstalled platform apps are expected to be
labeled with a coredomain type.
"""
coredomains = pol.QueryTypeAttribute("coredomain", True)
violations = []
allowed_apks = {entry["apk"] for entry in allowlist if "apk" in entry}
allowed_names = {entry["name"] for entry in allowlist if "name" in entry}
for entry in platform_seapp_entries:
values = entry.values
if not values["name"] or values["domain"] in coredomains:
continue
if values["domain"] in exempted_types:
continue
for app in platform_apps:
if app.apk_name in allowed_apks:
continue
if app.package_name in allowed_names:
continue
if package_name_match(app.package_name, values["name"]):
violations.append((app.apk_name, app.package_name,
values["domain"]))
break
if violations:
error_message = ("ERROR: All platform apps must be labeled with a "
"coredomain type.\nPlease fix the following "
"violations by doing one of these.\n"
"1) Add 'coredomain' attribute to labels.\n"
"2) Move apps and corresponding seapp_contexts entries"
" to vendor or odm partition.\n"
"Violations:\n")
for apk_name, package_name, domain in violations:
error_message += f" APK name: {apk_name}\n"
error_message += f" package name: {package_name}\n"
error_message += f" assigned label: {domain}\n"
error_message += "---\n"
return error_message + "\n"
return ""
def TestNoCoredomainForAllVendorApps(vendor_apps, seapp_entries, pol,
allowlist, exempted_types):
"""Tests if any of preinstalled vendor apps are labeled with coredomain
labels. All preinstalled vendor apps are expected to be non-coredomain.
"""
coredomains = pol.QueryTypeAttribute("coredomain", True)
violations = []
allowed_apks = {entry["apk"] for entry in allowlist if "apk" in entry}
allowed_names = {entry["name"] for entry in allowlist if "name" in entry}
for entry in seapp_entries:
values = entry.values
if not values["name"] or values["domain"] not in coredomains:
continue
if values["domain"] in exempted_types:
continue
for app in vendor_apps:
if app.apk_name in allowed_apks:
continue
if app.package_name in allowed_names:
continue
if package_name_match(app.package_name, values["name"]):
violations.append((app.apk_name, app.package_name,
values["domain"]))
break
if violations:
error_message = ("ERROR: All vendor apps must not be labeled with a "
"coredomain type.\nPlease fix the following "
"violations by doing one of these.\n"
"1) Remove the 'coredomain' attribute from labels.\n"
"2) Use other labels which are not 'coredomain'.\n"
"Violations:\n")
for apk_name, package_name, domain in violations:
error_message += f" APK name: {apk_name}\n"
error_message += f" package name: {package_name}\n"
error_message += f" assigned label: {domain}\n"
error_message += "---\n"
return error_message + "\n"
return ""
# pylint: disable=redefined-outer-name
def do_main(libpath):
parser = build_argument_parser()
args = parser.parse_args()
tracking_list = {}
if args.tracking_list_file:
with open(args.tracking_list_file, "r", encoding="utf-8") as f:
tracking_list = yaml.safe_load(f) or {}
pol_without_vendor = policy.Policy(args.precompiled_sepolicy_without_vendor,
None, libpath)
pol = policy.Policy(args.precompiled_sepolicy, None, libpath)
platform_apps = parse_package_names(args.platform_apks, args.aapt2_path)
vendor_apps = parse_package_names(args.vendor_apks, args.aapt2_path)
platform_seapp_entries = sum([parse_seapp_contexts(path) for path
in args.platform_seapp_contexts], [])
vendor_seapp_entries = sum([parse_seapp_contexts(path) for path
in args.vendor_seapp_contexts], [])
seapp_entries = platform_seapp_entries + vendor_seapp_entries
exempted_types = pol.QueryTypeAttribute("treble_labeling_violators", True)
result = ""
if not args.debuggable:
if exempted_types:
sys.exit("treble_labeling_violators can't be used on user builds.\n"
"Please guard it with userdebug_or_eng macro.\n"
f"violators:\n{exempted_types}\n")
result += TestNoCoredomainInVendorSepolicy(
pol_without_vendor,
pol,
tracking_list.get('coredomain_in_vendor', []),
exempted_types
)
result += TestNoPlatformFilesInVendorFileContexts(
args.vendor_file_contexts,
tracking_list.get('platform_file_in_vendor_file_contexts', []),
)
result += TestNoPlatformAppsInVendorSeappContexts(
platform_apps,
vendor_seapp_entries,
tracking_list.get('platform_apps_in_vendor_seapp_contexts', []),
exempted_types
)
result += TestCoredomainForAllPlatformApps(
platform_apps,
platform_seapp_entries,
pol,
tracking_list.get('non_coredomain_for_platform_apps', []),
exempted_types
)
result += TestNoCoredomainForAllVendorApps(
vendor_apps,
seapp_entries,
pol,
tracking_list.get('coredomain_for_vendor_apps', []),
exempted_types
)
if not result:
return
result += ("******************************\n"
"ERROR: SELinux Treble Labeling violations found.\n"
"Treble Labeling will be enforced for devices with "
"BOARD_API_LEVEL >= 202604.\n"
"Please fix all violations. For more details, please refer to:\n"
"system/sepolicy/tests/treble_labeling_tests.md\n"
"******************************\n")
if args.treat_as_warnings:
result = re.sub(r'(^|\n)ERROR:', r'\1WARNING:', result)
print(result, file=sys.stderr)
if not args.treat_as_warnings:
sys.exit(1)
if __name__ == '__main__':
with tempfile.TemporaryDirectory() as temp_dir:
libname = "libsepolwrap" + SHARED_LIB_EXTENSION
libpath = os.path.join(temp_dir, libname)
with open(libpath, "wb") as lib:
blob = pkgutil.get_data("treble_labeling_tests", libname)
if not blob:
sys.exit("Error: libsepolwrap does not exist. Is this binary "
"corrupted?\n")
lib.write(blob)
do_main(libpath)