• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3# Copyright 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17from pathlib import Path
18import argparse
19import glob
20import logging
21import mini_parser
22import os
23import pkgutil
24import policy
25import shutil
26import subprocess
27import sys
28import tempfile
29import zipfile
30"""This tool generates a mapping file for {ver} core sepolicy."""
31
32temp_dir = ''
33mapping_cil_footer = ";; mapping information from ToT policy's types to %s policy's types.\n"
34compat_cil_template = """;; complement CIL file for compatibility between ToT policy and %s vendors.
35;; will be compiled along with other normal policy files, on %s vendors.
36;;
37"""
38ignore_cil_template = """;; new_objects - a collection of types that have been introduced with ToT policy
39;;   that have no analogue in %s policy.  Thus, we do not need to map these types to
40;;   previous ones.  Add here to pass checkapi tests.
41(type new_objects)
42(typeattribute new_objects)
43(typeattributeset new_objects
44  ( new_objects
45    %s
46  ))
47"""
48
49SHARED_LIB_EXTENSION = '.dylib' if sys.platform == 'darwin' else '.so'
50
51def check_run(cmd, cwd=None):
52    if cwd:
53        logging.debug('Running cmd at %s: %s' % (cwd, cmd))
54    else:
55        logging.debug('Running cmd: %s' % cmd)
56    subprocess.run(cmd, cwd=cwd, check=True)
57
58
59def check_output(cmd):
60    logging.debug('Running cmd: %s' % cmd)
61    return subprocess.run(cmd, check=True, stdout=subprocess.PIPE)
62
63
64def get_android_build_top():
65    ANDROID_BUILD_TOP = os.getenv('ANDROID_BUILD_TOP')
66    if not ANDROID_BUILD_TOP:
67        sys.exit(
68            'Error: Missing ANDROID_BUILD_TOP env variable. Please run '
69            '\'. build/envsetup.sh; lunch <build target>\'. Exiting script.')
70    return ANDROID_BUILD_TOP
71
72
73def fetch_artifact(branch, build, pattern, destination='.'):
74    """Fetches build artifacts from Android Build server.
75
76    Args:
77      branch: string, branch to pull build artifacts from
78      build: string, build ID or "latest"
79      pattern: string, pattern of build artifact file name
80      destination: string, destination to pull build artifact to
81    """
82    fetch_artifact_path = '/google/data/ro/projects/android/fetch_artifact'
83    cmd = [
84        fetch_artifact_path, '--branch', branch, '--target',
85        'aosp_arm64-userdebug'
86    ]
87    if build == 'latest':
88        cmd.append('--latest')
89    else:
90        cmd.extend(['--bid', build])
91    cmd.extend([pattern, destination])
92    check_run(cmd)
93
94
95def extract_mapping_file_from_img_zip(zip_path, ver, destination='.'):
96    """ Extracts system/etc/selinux/mapping/{ver}.cil from img.zip file.
97
98    Args:
99      zip_path: string, path to img.zip file
100      ver: string, version of designated mapping file
101      destination: string, destination to pull the mapping file to
102
103    Returns:
104      string, path to extracted mapping file
105    """
106    with zipfile.ZipFile(zip_path) as zip_file:
107        logging.debug(f'Extracting system.img to {temp_dir}')
108        zip_file.extract('system.img', temp_dir)
109
110    system_img_path = os.path.join(temp_dir, 'system.img')
111    mapping_file_path = os.path.join(destination, f'{ver}.cil')
112    cmd = [
113        'debugfs', '-R',
114        f'dump system/etc/selinux/mapping/{ver}.cil {mapping_file_path}',
115        system_img_path
116    ]
117    logging.debug(f'Extracting {ver}.cil to {destination}')
118    check_run(cmd)
119    return mapping_file_path
120
121
122def download_img_zip(branch, build, destination='.'):
123    """ Downloads system/etc/selinux/mapping/{ver}.cil from Android Build server.
124
125    Args:
126      branch: string, branch to pull build artifacts from (e.g. "sc-v2-dev")
127      build: string, build ID or "latest"
128      ver: string, version of designated mapping file (e.g. "32.0")
129      destination: string, destination to pull build artifact to
130
131    Returns:
132      string, path to img.zip file
133    """
134    logging.info('Downloading %s mapping file from branch %s build %s...' %
135                 (ver, branch, build))
136    artifact_pattern = 'aosp_arm64-img-*.zip'
137    fetch_artifact(branch, build, artifact_pattern, temp_dir)
138
139    # glob must succeed
140    return glob.glob(os.path.join(temp_dir, artifact_pattern))[0]
141
142
143def build_base_files(target_version):
144    """ Builds needed base policy files from the source code.
145
146    Args:
147      target_version: string, target version to gerenate the mapping file
148
149    Returns:
150      (string, string, string), paths to base policy, old policy, and pub policy
151      cil
152    """
153    logging.info('building base sepolicy files')
154    build_top = get_android_build_top()
155
156    cmd = [
157        'build/soong/soong_ui.bash',
158        '--make-mode',
159        'dist',
160        'base-sepolicy-files-for-mapping',
161        'TARGET_PRODUCT=aosp_arm64',
162        'TARGET_BUILD_VARIANT=userdebug',
163    ]
164    check_run(cmd, cwd=build_top)
165
166    dist_dir = os.path.join(build_top, 'out', 'dist')
167    base_policy_path = os.path.join(dist_dir, 'base_plat_sepolicy')
168    old_policy_path = os.path.join(dist_dir,
169                                   '%s_plat_policy' % target_version)
170    pub_policy_cil_path = os.path.join(dist_dir, 'base_plat_pub_policy.cil')
171
172    return base_policy_path, old_policy_path, pub_policy_cil_path
173
174
175def change_api_level(versioned_type, api_from, api_to):
176    """ Verifies the API version of versioned_type, and changes it to new API level.
177
178    For example, change_api_level("foo_202404", "202404", "202504") will return
179    "foo_202504".
180
181    Args:
182      versioned_type: string, type with version suffix
183      api_from: string, api version of versioned_type
184      api_to: string, new api version for versioned_type
185
186    Returns:
187      string, a new versioned type
188    """
189    if not versioned_type.endswith(api_from):
190        raise ValueError('Version of type %s is different from %s' %
191                         (versioned_type, api_from))
192    return versioned_type.removesuffix(api_from) + api_to
193
194
195def create_target_compat_modules(bp_path, target_ver):
196    """ Creates compat modules to Android.bp.
197
198    Args:
199      bp_path: string, path to Android.bp
200      target_ver: string, api version to generate
201    """
202
203    module_template = """
204se_build_files {{
205    name: "{ver}.board.compat.map",
206    srcs: ["compat/{ver}/{ver}.cil"],
207}}
208
209se_build_files {{
210    name: "{ver}.board.compat.cil",
211    srcs: ["compat/{ver}/{ver}.compat.cil"],
212}}
213
214se_build_files {{
215    name: "{ver}.board.ignore.map",
216    srcs: ["compat/{ver}/{ver}.ignore.cil"],
217}}
218
219se_cil_compat_map {{
220    name: "plat_{ver}.cil",
221    stem: "{ver}.cil",
222    bottom_half: [":{ver}.board.compat.map{{.plat_private}}"],
223    version: "{ver}",
224}}
225
226se_cil_compat_map {{
227    name: "system_ext_{ver}.cil",
228    stem: "{ver}.cil",
229    bottom_half: [":{ver}.board.compat.map{{.system_ext_private}}"],
230    system_ext_specific: true,
231    version: "{ver}",
232}}
233
234se_cil_compat_map {{
235    name: "product_{ver}.cil",
236    stem: "{ver}.cil",
237    bottom_half: [":{ver}.board.compat.map{{.product_private}}"],
238    product_specific: true,
239    version: "{ver}",
240}}
241
242se_cil_compat_map {{
243    name: "{ver}.ignore.cil",
244    bottom_half: [":{ver}.board.ignore.map{{.plat_private}}"],
245    version: "{ver}",
246}}
247
248se_cil_compat_map {{
249    name: "system_ext_{ver}.ignore.cil",
250    stem: "{ver}.ignore.cil",
251    bottom_half: [":{ver}.board.ignore.map{{.system_ext_private}}"],
252    system_ext_specific: true,
253    version: "{ver}",
254}}
255
256se_cil_compat_map {{
257    name: "product_{ver}.ignore.cil",
258    stem: "{ver}.ignore.cil",
259    bottom_half: [":{ver}.board.ignore.map{{.product_private}}"],
260    product_specific: true,
261    version: "{ver}",
262}}
263
264se_compat_cil {{
265    name: "{ver}.compat.cil",
266    srcs: [":{ver}.board.compat.cil{{.plat_private}}"],
267    version: "{ver}",
268}}
269
270se_compat_cil {{
271    name: "system_ext_{ver}.compat.cil",
272    stem: "{ver}.compat.cil",
273    srcs: [":{ver}.board.compat.cil{{.system_ext_private}}"],
274    system_ext_specific: true,
275    version: "{ver}",
276}}
277"""
278
279    with open(bp_path, 'a') as f:
280        f.write(module_template.format(ver=target_ver))
281
282
283def patch_top_half_of_latest_compat_modules(bp_path, latest_ver, target_ver):
284    """ Adds top_half property to latest compat modules in Android.bp.
285
286    Args:
287      bp_path: string, path to Android.bp
288      latest_ver: string, previous api version
289      target_ver: string, api version to generate
290    """
291
292    modules_to_patch = [
293        "plat_{ver}.cil",
294        "system_ext_{ver}.cil",
295        "product_{ver}.cil",
296        "{ver}.ignore.cil",
297        "system_ext_{ver}.ignore.cil",
298        "product_{ver}.ignore.cil",
299    ]
300
301    for module in modules_to_patch:
302        # set latest_ver module's top_half property to target_ver
303        # e.g.
304        #
305        # se_cil_compat_map {
306        #    name: "plat_33.0.cil",
307        #    top_half: "plat_34.0.cil", <== this
308        #    ...
309        # }
310        check_run([
311            "bpmodify",
312            "-m", module.format(ver=latest_ver),
313            "-property", "top_half",
314            "-str", module.format(ver=target_ver),
315            "-w",
316            bp_path
317        ])
318
319def get_args():
320    parser = argparse.ArgumentParser()
321    parser.add_argument(
322        '--branch',
323        help='Branch to pull build from. e.g. "sc-v2-dev"')
324    parser.add_argument('--build',
325        default='latest',
326        help='Build ID, or "latest"')
327    parser.add_argument(
328        '--target-version',
329        required=True,
330        help='Target version of designated mapping file. e.g. "202504"')
331    parser.add_argument(
332        '--latest-version',
333        required=True,
334        help='Latest version for mapping of newer types. e.g. "202404"')
335    parser.add_argument(
336        '--img-zip',
337        help='Pre-downloaded img.zip. e.g. "aosp_arm64-img-xxxxxxxx.zip"')
338    parser.add_argument(
339        '-v',
340        '--verbose',
341        action='count',
342        default=0,
343        help='Increase output verbosity, e.g. "-v", "-vv".')
344    return parser.parse_args()
345
346
347def main():
348    args = get_args()
349
350    verbosity = min(args.verbose, 2)
351    logging.basicConfig(
352        format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
353        level=(logging.WARNING, logging.INFO, logging.DEBUG)[verbosity])
354
355    global temp_dir
356    temp_dir = tempfile.mkdtemp()
357
358    try:
359        libname = "libsepolwrap" + SHARED_LIB_EXTENSION
360        libpath = os.path.join(temp_dir, libname)
361        with open(libpath, "wb") as f:
362            blob = pkgutil.get_data("sepolicy_generate_compat", libname)
363            if not blob:
364                sys.exit("Error: libsepolwrap does not exist. Is this binary corrupted?\n")
365            f.write(blob)
366
367        build_top = get_android_build_top()
368        sepolicy_path = os.path.join(build_top, 'system', 'sepolicy')
369
370        # Step 0. Create a placeholder files and compat modules
371        # These are needed to build base policy files below.
372        compat_bp_path = os.path.join(sepolicy_path, 'compat', 'Android.bp')
373        create_target_compat_modules(compat_bp_path, args.target_version)
374        patch_top_half_of_latest_compat_modules(compat_bp_path, args.latest_version,
375            args.target_version)
376
377        target_compat_path = os.path.join(sepolicy_path, 'private', 'compat',
378                                          args.target_version)
379        target_mapping_file = os.path.join(target_compat_path,
380                                           args.target_version + '.cil')
381        target_compat_file = os.path.join(target_compat_path,
382                                          args.target_version + '.compat.cil')
383        target_ignore_file = os.path.join(target_compat_path,
384                                          args.target_version + '.ignore.cil')
385        Path(target_compat_path).mkdir(parents=True, exist_ok=True)
386        Path(target_mapping_file).touch()
387        Path(target_compat_file).touch()
388        Path(target_ignore_file).touch()
389
390        # Step 1. Download system/etc/selinux/mapping/{ver}.cil, and remove types/typeattributes
391        if args.img_zip and args.branch:
392            sys.exit('Error: only one of --img-zip and --branch can be set')
393        elif args.img_zip:
394            img_zip = args.img_zip
395        elif args.branch:
396            img_zip = download_img_zip(args.branch, args.build, destination=temp_dir)
397        else:
398            sys.exit('Error: either one of --img-zip and --branch must be set')
399        mapping_file = extract_mapping_file_from_img_zip(img_zip, args.target_version,
400                                                         destination=temp_dir)
401        mapping_file_cil = mini_parser.MiniCilParser(mapping_file)
402        mapping_file_cil.types = set()
403        mapping_file_cil.typeattributes = set()
404
405        # Step 2. Build base policy files and parse latest mapping files
406        base_policy_path, old_policy_path, pub_policy_cil_path = build_base_files(
407            args.target_version)
408        base_policy = policy.Policy(base_policy_path, None, libpath)
409        old_policy = policy.Policy(old_policy_path, None, libpath)
410        pub_policy_cil = mini_parser.MiniCilParser(pub_policy_cil_path)
411
412        all_types = base_policy.GetAllTypes(False)
413        old_all_types = old_policy.GetAllTypes(False)
414        pub_types = pub_policy_cil.types
415
416        # Step 3. Find new types and removed types
417        new_types = pub_types & (all_types - old_all_types)
418        removed_types = (mapping_file_cil.pubtypes - mapping_file_cil.types) & (
419            old_all_types - all_types)
420
421        logging.info('new types: %s' % new_types)
422        logging.info('removed types: %s' % removed_types)
423
424        # Step 4. Map new types and removed types appropriately, based on the latest mapping
425        latest_compat_path = os.path.join(sepolicy_path, 'private', 'compat',
426                                          args.latest_version)
427        latest_mapping_cil = mini_parser.MiniCilParser(
428            os.path.join(latest_compat_path, args.latest_version + '.cil'))
429        latest_ignore_cil = mini_parser.MiniCilParser(
430            os.path.join(latest_compat_path,
431                         args.latest_version + '.ignore.cil'))
432
433        latest_ignored_types = list(latest_ignore_cil.rTypeattributesets.keys())
434        latest_removed_types = latest_mapping_cil.types
435        logging.debug('types ignored in latest policy: %s' %
436                      latest_ignored_types)
437        logging.debug('types removed in latest policy: %s' %
438                      latest_removed_types)
439
440        target_ignored_types = set()
441        target_removed_types = set()
442        invalid_new_types = set()
443        invalid_mapping_types = set()
444        invalid_removed_types = set()
445
446        logging.info('starting mapping')
447        for new_type in new_types:
448            # Either each new type should be in latest_ignore_cil, or mapped to existing types
449            if new_type in latest_ignored_types:
450                logging.debug('adding %s to ignore' % new_type)
451                target_ignored_types.add(new_type)
452            elif new_type in latest_mapping_cil.rTypeattributesets:
453                latest_mapped_types = latest_mapping_cil.rTypeattributesets[
454                    new_type]
455                target_mapped_types = {change_api_level(t, args.latest_version,
456                                        args.target_version)
457                       for t in latest_mapped_types}
458                logging.debug('mapping %s to %s' %
459                              (new_type, target_mapped_types))
460
461                for t in target_mapped_types:
462                    if t not in mapping_file_cil.typeattributesets:
463                        logging.error(
464                            'Cannot find desired type %s in mapping file' % t)
465                        invalid_mapping_types.add(t)
466                        continue
467                    mapping_file_cil.typeattributesets[t].add(new_type)
468            else:
469                logging.error('no mapping information for new type %s' %
470                              new_type)
471                invalid_new_types.add(new_type)
472
473        for removed_type in removed_types:
474            # Removed type should be in latest_mapping_cil
475            if removed_type in latest_removed_types:
476                logging.debug('adding %s to removed' % removed_type)
477                target_removed_types.add(removed_type)
478            else:
479                logging.error('no mapping information for removed type %s' %
480                              removed_type)
481                invalid_removed_types.add(removed_type)
482
483        error_msg = ''
484
485        if invalid_new_types:
486            error_msg += ('The following new types were not in the latest '
487                          'mapping: %s\n') % sorted(invalid_new_types)
488        if invalid_mapping_types:
489            error_msg += (
490                'The following existing types were not in the '
491                'downloaded mapping file: %s\n') % sorted(invalid_mapping_types)
492        if invalid_removed_types:
493            error_msg += ('The following removed types were not in the latest '
494                          'mapping: %s\n') % sorted(invalid_removed_types)
495
496        if error_msg:
497            error_msg += '\n'
498            error_msg += ('Please make sure the source tree and the build ID is'
499                          ' up to date.\n')
500            sys.exit(error_msg)
501
502        # Step 5. Write to system/sepolicy/private/compat
503        with open(target_mapping_file, 'w') as f:
504            logging.info('writing %s' % target_mapping_file)
505            if removed_types:
506                f.write(';; types removed from current policy\n')
507                f.write('\n'.join(f'(type {x})' for x in sorted(target_removed_types)))
508                f.write('\n\n')
509            f.write(mapping_cil_footer % args.target_version)
510            f.write(mapping_file_cil.unparse())
511
512        with open(target_compat_file, 'w') as f:
513            logging.info('writing %s' % target_compat_file)
514            f.write(compat_cil_template % (args.target_version, args.target_version))
515
516        with open(target_ignore_file, 'w') as f:
517            logging.info('writing %s' % target_ignore_file)
518            f.write(ignore_cil_template %
519                    (args.target_version, '\n    '.join(sorted(target_ignored_types))))
520
521        # TODO(b/391513934): add treble tests
522        # TODO(b/391513934): add mapping files to phony modules like selinux_policy_system
523    finally:
524        logging.info('Deleting temporary dir: {}'.format(temp_dir))
525        shutil.rmtree(temp_dir)
526
527
528if __name__ == '__main__':
529    main()
530