#!/usr/bin/python # Copyright (c) 2017-2018 ARM Limited # # SPDX-License-Identifier: Apache-2.0 # # 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. import os from os.path import join as path_join import json from jsonschema import validate import fnmatch from six import integer_types, string_types from jinja2 import Environment, FileSystemLoader, StrictUndefined SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) MBED_OS_ROOT = os.path.abspath(path_join(SCRIPT_DIR, os.pardir, os.pardir)) SERVICES_DIR = path_join(MBED_OS_ROOT, "components", "TARGET_PSA", "services") TESTS_DIR = path_join(MBED_OS_ROOT, "TESTS", "psa") MANIFEST_FILE_PATTERN = '*_psa.json' def _assert_int(num): """ Tries to parse an integer num from a given string :param num: Number in int/string type :return: Numeric value """ if isinstance(num, int): return num num_str = str(num) radix = 16 if num_str.lower().startswith('0x') else 10 res = int(num_str, radix) # Python converts str to int as a signed integer if res > 0x7FFFFFFF: res -= 0x100000000 return res class RotService(object): MINOR_POLICIES = ['STRICT', 'RELAXED'] def __init__( self, name, identifier, signal, non_secure_clients, minor_version=1, minor_policy='STRICT' ): """ Root of Trust Service C'tor (Aligned with json schema) :param name: Root of Trust Service identifier (available to user) :param identifier: Root of Trust Service numeric enumeration. :param signal: Root of Trust Service identifier inside the partition :param non_secure_clients: True to allow connections from non-secure partitions :param minor_version: Root of Trust Service version :param minor_policy: Enforcement level of minor version """ self.name = name self.id = identifier self.signal = signal assert _assert_int(identifier) assert isinstance(non_secure_clients, bool), \ 'non_secure_clients parameter must be of boolean type' self.nspe_callable = non_secure_clients self.minor_version = _assert_int(minor_version) assert self.minor_version > 0, 'minor_version parameter is invalid' assert minor_policy in self.MINOR_POLICIES, \ 'minor_policy parameter is invalid' self.minor_policy = minor_policy @property def numeric_id(self): return _assert_int(self.id) def __eq__(self, other): return ( (self.name == other.name) and (self.id == other.id) and (self.signal == other.signal) and (self.nspe_callable == other.nspe_callable) and (self.minor_version == other.minor_version) and (self.minor_policy == other.minor_policy) ) class MmioRegion(object): MMIO_PERMISSIONS = { 'READ-ONLY': 'PSA_MMIO_PERM_READ_ONLY', 'READ-WRITE': 'PSA_MMIO_PERM_READ_WRITE' } def __init__(self, **kwargs): """ MMIO Region C'tor (Aligned with json schema) Supports both named and numeric regions In case of named region the acceptable params are name and permission In case of numeric region the acceptable params are name, size and permission :param name: C definition name of the region (size will be auto-generated) :param base: C hex string defining a memory address (must be 32bit) :param size: size of a region (Applicable only for numbered regions) :param permission: Access permissions to the described region (R/RW) """ assert 'permission' in kwargs self.permission = self.MMIO_PERMISSIONS[kwargs['permission']] if 'name' in kwargs: self.base = kwargs['name'] self.size = '(sizeof(*({})))'.format(kwargs['name']) if 'base' in kwargs: self.base = kwargs['base'] self.size = _assert_int(kwargs['size']) assert 'partition_id' in kwargs self.partition_id = _assert_int(kwargs['partition_id']) assert hasattr(self, 'base') assert hasattr(self, 'size') assert hasattr(self, 'permission') assert hasattr(self, 'partition_id') def __eq__(self, other): return ( (self.base == other.base) and (self.size == other.size) and (self.permission == other.permission) ) class Irq(object): def __init__(self, line_num, signal): """ IRQ line C'tor (Aligned with json schema) :param line_num: number of interrupt used by the partition :param signal: IRQ line identifier inside the partition """ self.line_num = _assert_int(line_num) assert isinstance(signal, string_types) self.signal = signal def __eq__(self, other): return (self.line_num == other.line_num) and \ (self.signal == other.signal) class Manifest(object): PRIORITY = { 'LOW': 'osPriorityLow', 'NORMAL': 'osPriorityNormal', 'HIGH': 'osPriorityHigh' } PARTITION_TYPES = ['APPLICATION-ROT', 'PSA-ROT'] # The following signal bits cannot be used: # bit[0-2] | Reserved # bit[3] | PSA Doorbell # bit[31] | RTX error bit RESERVED_SIGNALS = 5 def __init__( self, manifest_file, name, partition_id, partition_type, priority, entry_point, heap_size, stack_size, source_files, mmio_regions=None, rot_services=None, extern_sids=None, irqs=None ): """ Manifest C'tor (Aligned with json schema) :param manifest_file: Path to json manifest :param name: Partition unique name :param partition_id: Partition identifier :param partition_type: Whether the partition is unprivileged or part of the trusted computing base :param priority: Priority of the partition's thread :param entry_point: C symbol name of the partition's main function :param heap_size: Size of heap required for the partition :param stack_size: Size of stack required for the partition :param source_files: List of files assembling the partition (relative paths) :param mmio_regions: List of MMIO regions used by the partition :param rot_services: List of Root of Trust Services declared by the partition :param extern_sids: List of Root of Trust Services the partition can call :param irqs: List of interrupts the partition can handle """ assert manifest_file is not None assert name is not None assert partition_id is not None assert partition_type is not None assert entry_point is not None assert priority is not None assert heap_size is not None assert stack_size is not None assert source_files is not None mmio_regions = [] if mmio_regions is None else mmio_regions rot_services = [] if rot_services is None else rot_services extern_sids = [] if extern_sids is None else extern_sids irqs = [] if irqs is None else irqs assert os.path.isfile(manifest_file) assert isinstance(partition_id, integer_types) assert isinstance(heap_size, int) assert isinstance(stack_size, int) assert isinstance(entry_point, string_types) assert partition_type in self.PARTITION_TYPES assert partition_id > 0 self.file = manifest_file self.name = name self.id = partition_id self.type = partition_type self.priority_tfm = priority self.priority_mbed = self.PRIORITY[priority] self.heap_size = heap_size self.stack_size = stack_size self.entry_point = entry_point if isinstance(source_files, list): self.source_files = source_files else: self.source_files = [source_files] self.mmio_regions = mmio_regions self.rot_services = rot_services self.extern_sids = extern_sids self.irqs = irqs for src_file in self.source_files: assert os.path.isfile(src_file), \ "The source file {} mentioned in {} doesn't exist.".format( src_file, self.file ) for rot_srv in self.rot_services: assert isinstance(rot_srv, RotService) for extern_sid in self.extern_sids: assert isinstance(extern_sid, string_types) assert len(self.extern_sids) == len(set(self.extern_sids)), \ 'Detected duplicates external SIDs in {}'.format(self.file) for irq in self.irqs: assert isinstance(irq, Irq) total_signals = len(self.rot_services) + len(self.irqs) assert total_signals <= 32 - self.RESERVED_SIGNALS, \ 'Manifest {} - {} exceeds limit of RoT services and IRQs allowed ' \ '({}).'.format( self.name, self.file, 32 - self.RESERVED_SIGNALS ) def __eq__(self, other): return ( (self.file == other.file) and (self.name == other.name) and (self.id == other.id) and (self.type == other.type) and (self.priority_mbed == other.priority_mbed) and (self.priority_tfm == other.priority_tfm) and (self.heap_size == other.heap_size) and (self.stack_size == other.stack_size) and (self.entry_point == other.entry_point) and (self.source_files == other.source_files) and (self.mmio_regions == other.mmio_regions) and (self.rot_services == other.rot_services) and (self.extern_sids == other.extern_sids) and (self.irqs == other.irqs) ) @classmethod def from_json(cls, manifest_file, skip_src=False): """ Load a partition manifest file :param manifest_file: Manifest file path :param skip_src: Ignore the `source_files` entry :return: Manifest object """ partition_schema_path = path_join( SCRIPT_DIR, 'partition_description_schema.json' ) with open(partition_schema_path) as schema_fh: partition_schema = json.load(schema_fh) # Load partition manifest file. with open(manifest_file) as fh: manifest = json.load(fh) validate(manifest, partition_schema) manifest_dir = os.path.dirname(manifest_file) source_files = [] if not skip_src: for src_file in manifest['source_files']: source_files.append( os.path.normpath(path_join(manifest_dir, src_file))) mmio_regions = [] for mmio_region in manifest.get('mmio_regions', []): mmio_regions.append( MmioRegion(partition_id=manifest['id'], **mmio_region)) rot_services = [] for rot_srv in manifest.get('services', []): rot_services.append(RotService(**rot_srv)) irqs = [] for irq in manifest.get('irqs', []): irqs.append(Irq(**irq)) return Manifest( manifest_file=manifest_file, name=manifest['name'], partition_id=_assert_int(manifest['id']), partition_type=manifest['type'], priority=manifest['priority'], heap_size=_assert_int(manifest['heap_size']), stack_size=_assert_int(manifest['stack_size']), entry_point=manifest['entry_point'], source_files=source_files, mmio_regions=mmio_regions, rot_services=rot_services, extern_sids=manifest.get('extern_sids', []), irqs=irqs ) @property def sids(self): return [rot_srv.name for rot_srv in self.rot_services] @property def autogen_folder(self): return os.path.abspath(os.path.dirname(self.file)) def find_dependencies(self, manifests): """ Find other manifests which holds Root of Trust Services that are declared as extern in this manifest :param manifests: list of manifests to filter :return: list of manifest's names that holds current extern Root of Trust Services """ manifests = [man for man in manifests if man != self] extern_sids_set = set(self.extern_sids) return [manifest.name for manifest in manifests if extern_sids_set.intersection(set(manifest.sids))] def templates_to_files(self, templates, templates_base, output_dir): """ Translates a list of partition templates to file names :param templates: List of partition templates :param templates_base: Base directory of the templates :param output_dir: Output directory (Default is autogen folder property) :return: Dictionary of template to output file translation """ generated_files = {} for t in templates: fname = os.path.relpath(t, templates_base) _tpl = fname.replace('NAME', self.name.lower()) full_path = path_join( output_dir, os.path.splitext(_tpl)[0] ) generated_files[t] = full_path return generated_files def check_circular_call_dependencies(manifests): """ Check if there is a circular dependency between the partitions described by the manifests. A circular dependency might happen if there is a scenario in which a partition calls a Root of Trust Service in another partition which than calls another Root of Trust Service which resides in the originating partition. For example: Partition A has a Root of Trust Service A1 and extern sid B1, partition B has a Root of Trust Service B1 and extern sid A1. :param manifests: List of the partition manifests. :return: True if a circular dependency exists, false otherwise. """ # Construct a call graph. call_graph = {} for manifest in manifests: call_graph[manifest.name] = { 'calls': manifest.find_dependencies(manifests), 'called_by': set() } for manifest_name in call_graph: for called in call_graph[manifest_name]['calls']: call_graph[called]['called_by'].add(manifest_name) # Run topological sort on the call graph. while len(call_graph) > 0: # Find all the nodes that aren't called by anyone and # therefore can be removed. nodes_to_remove = [x for x in list(call_graph.keys()) if len(call_graph[x]['called_by']) == 0] # If no node can be removed we have a circle. if not nodes_to_remove: return True # Remove the nodes. for node in nodes_to_remove: for called in call_graph[node]['calls']: call_graph[called]['called_by'].remove(node) call_graph.pop(node) return False def validate_partition_manifests(manifests): """ Check the correctness of the manifests list (no conflicts, no missing elements, etc.) :param manifests: List of the partition manifests """ for manifest in manifests: assert isinstance(manifest, Manifest) partitions_names = {} partitions_ids = {} rot_service_ids = {} rot_service_names = {} rot_service_signals = {} irq_signals = {} irq_numbers = {} all_extern_sids = set() spe_contained_manifests = [] for manifest in manifests: # Make sure the partition names are unique. if manifest.name in partitions_names: raise ValueError( 'Partition name {} is not unique, ' 'found in both {} and {}.'.format( manifest.name, partitions_names[manifest.name], manifest.file ) ) partitions_names[manifest.name] = manifest.file # Make sure the partition ID's are unique. if manifest.id in partitions_ids: raise ValueError( 'Partition id {} is not unique, ' 'found in both {} and {}.'.format( manifest.id, partitions_ids[manifest.id], manifest.file ) ) partitions_ids[manifest.id] = manifest.file is_nspe_callabale = False # Make sure all the Root of Trust Service IDs and signals are unique. for rot_service in manifest.rot_services: if rot_service.name in rot_service_names: raise ValueError( 'Root of Trust Service name {} is found ' 'in both {} and {}.'.format( rot_service.name, rot_service_names[rot_service.name], manifest.file ) ) rot_service_names[rot_service.name] = manifest.file if rot_service.signal in rot_service_signals: raise ValueError( 'Root of Trust Service signal {} is found ' 'in both {} and {}.'.format( rot_service.signal, rot_service_signals[rot_service.signal], manifest.file ) ) rot_service_signals[rot_service.signal] = manifest.file if rot_service.numeric_id in rot_service_ids: raise ValueError( 'Root of Trust Service identifier {} is found ' 'in both {} and {}.'.format( rot_service.numeric_id, rot_service_ids[rot_service.numeric_id], manifest.file ) ) rot_service_ids[rot_service.numeric_id] = manifest.file is_nspe_callabale |= rot_service.nspe_callable if not is_nspe_callabale: spe_contained_manifests.append(manifest) # Make sure all the IRQ signals and line-numbers are unique. for irq in manifest.irqs: if irq.signal in irq_signals: raise ValueError( 'IRQ signal {} is found in both {} and {}.'.format( irq.signal, irq_signals[irq.signal], manifest.file ) ) irq_signals[irq.signal] = manifest.file if irq.line_num in irq_numbers: raise ValueError( 'IRQ line number {} is found in both {} and {}.'.format( irq.line_num, irq_numbers[irq.line_num], manifest.file ) ) irq_numbers[irq.line_num] = manifest.file all_extern_sids.update(manifest.extern_sids) # Check that all the external SIDs can be found. declared_sids = set(rot_service_names.keys()) for manifest in manifests: extern_sids = set(manifest.extern_sids) if not extern_sids.issubset(declared_sids): missing_sids = extern_sids.difference(declared_sids) raise ValueError( "External SID(s) {} required by {} can't be found in " "any partition manifest.".format( ', '.join(missing_sids), manifest.file) ) if check_circular_call_dependencies(manifests): raise ValueError( "Detected a circular call dependency between the partitions.") for manifest in spe_contained_manifests: rot_services = set([service.name for service in manifest.rot_services]) if not rot_services.intersection(all_extern_sids) and len( manifest.irqs) == 0: raise ValueError( 'Partition {} (defined by {}) is not accessible from NSPE ' 'and not referenced by any other partition.'.format( manifest.name, manifest.file ) ) def is_test_manifest(manifest): return 'TESTS' in manifest def is_service_manifest(manifest): return not is_test_manifest(manifest) def manifests_discovery(root_dirs, ignore_paths): service_manifest_files = set() test_manifest_files = set() for root_dir in root_dirs: for root, dirs, files in os.walk(root_dir, followlinks=True): # Filters paths if they are inside one of the ignore paths if next((True for igp in ignore_paths if igp in root), False): continue to_add = [path_join(root, f) for f in fnmatch.filter(files, MANIFEST_FILE_PATTERN)] service_manifest_files.update(filter(is_service_manifest, to_add)) test_manifest_files.update(filter(is_test_manifest, to_add)) service_manifest_files = sorted(list(service_manifest_files)) test_manifest_files = sorted(list(test_manifest_files)) return service_manifest_files, test_manifest_files def parse_manifests(manifests_files): region_list = [] manifests = [] for manifest_file in manifests_files: manifest_obj = Manifest.from_json(manifest_file) manifests.append(manifest_obj) for region in manifest_obj.mmio_regions: region_list.append(region) return manifests, region_list def generate_source_files( templates, render_args, extra_filters=None ): """ Generate SPM common C code from manifests using given templates :param templates: Dictionary of template and their auto-generated products :param render_args: Dictionary of arguments that should be passed to render :param extra_filters: Dictionary of extra filters to use in the rendering process :return: Path to generated folder containing common generated files """ rendered_files = [] templates_dirs = list( set([os.path.dirname(path) for path in templates]) ) template_files = {os.path.basename(t): t for t in templates} # Load templates for the code generation. env = Environment( loader=FileSystemLoader(templates_dirs), lstrip_blocks=True, trim_blocks=True, undefined=StrictUndefined ) if extra_filters: env.filters.update(extra_filters) for tf in template_files: template = env.get_template(tf) rendered_files.append( (templates[template_files[tf]], template.render(**render_args))) rendered_file_dir = os.path.dirname(templates[template_files[tf]]) if not os.path.exists(rendered_file_dir): os.makedirs(rendered_file_dir) for fname, data in rendered_files: output_folder = os.path.dirname(fname) if not os.path.isdir(output_folder): os.makedirs(output_folder) with open(fname, 'wt') as fh: fh.write(data)