Secure Partition Runtime Library
- Organization
Arm Limited
- Contact
Background
Trusted Firmware - M (TF-M) uses a toolchain provided runtime library and supervisor calls to easily implement the PSA Firmware Framework (PSA FF) API. This working model works well under isolation level 1 since there are no data isolation requirements. While TF-M is evolving, this model is not suitable because:
The high-level isolation requires isolating data but some toolchain library interfaces have their own global data which cannot be shared between the Secure Partitions.
The toolchain libraries are designed without taking security as a core design principle.
A TF-M specific runtime library is needed for the following reasons:
Easier evaluation or certification by security standards.
Source code transparency.
Sharing code to save ROM and RAM space for TF-M.
PSA FF specification also describes the requirements of C runtime API for Secure Partitions.
This runtime library is named the Secure Partition Runtime Library
, and the
abbreviation is SPRTL
.
Design Principle
The following requirements are mandatory for SPRTL implementation:
Important
CODE ONLY - No read-write data should be introduced into runtime library implementation.
Thread safe - All functions are designed with thread-safe consideration. These APIs access caller stack and caller provided memory only.
Isolation - Runtime API code is set as executable and read-only in higher isolation levels.
Security first - SPRTL is designed for security and it may come with some performance loss.
API Categories
Several known types of functions are included in SPRTL:
C runtime API.
RoT Service API.
PSA Client and Service API.
[Future expansion, to be detailed later] other secure API.
Security Implementation Requirements
If malloc/realloc/free
are provided, they must obey additional requirements
compared to the C standard: newly allocated memory must be initialized to
ZERO, and freed memory must be wiped immediately in case the block contains
sensitive data.
The comparison API (‘memcmp’ e.g.), they should not return immediately when the fault case is detected. The implementation should execute in linear time based on input to avoid execution timing side channel attack.
The pointer validation needs to be considered. In general, at least the
‘non-NULL’ checking is mandatory. A detection for invalid pointer leads to a
psa_panic()
.
The following section describes the first 3 API types and the implementation requirements.
C Runtime API
PSA FF describes a small set of the C standard library. Part of toolchain library API can be used as default if these APIs meet the Design Principle and Security Implementation Requirements. The toolchain ‘header’ and ‘types’ can be reused to simplify the implementation.
These APIs can take the toolchain provided version, or separately implemented in case there are extra requirements:
Note
‘memcpy()/memmove()/memset()’
String API
These APIs are proposed to be implemented with the security consideration mentioned in Security Implementation Requirements:
Note
‘memcmp()’
Other comparison API if referenced (‘strcmp’ e.g.).
The following functions are optional, but if present, they must conform to additional Security Implementation Requirements:
Note
‘malloc()/free()/realloc()’
‘assert()/printf()’
The following APIs are coupled with toolchain library much so applying toolchain library implementation is recommended:
Note
Division and modulo - arithmetic operations.
Other low level or compiler specific functions (such as ‘va_list’).
Besides the APIs mentioned above, the following runtime APIs are required for runtime APIs with private runtime context (‘malloc’ e.g.):
Note
‘__sprtmain()’ - partition entry runtime wrapper.
RoT Service API
The description of RoT Service API in PSA FF:
Note
Arm recommends that the RoT Service developer also defines an RoT Service API and implementation to encapsulate the use of the IPC protocol, and improve the usability of the service for client firmware.
Part of the RoT Service API have proposed specifications, such as the PSA Cryptography API, PSA Storage API, and PSA Attestation API. It is suggested that the service developer create documents of their RoT Service API and make them publicly available.
The RoT Service API has a large amount and it is the main part of SPRTL. This chapter describes the general implementation of the RoT Service API and the reason for putting them into SPRTL.
In general, a client uses the PSA Client API to access a secure service. For example:
/* Example, not a real implementation */
caller_status_t psa_example_service(void)
{
...
handle = psa_connect(SERVICE_SID, SERVICE_VERSION);
if (INVALID_HANDLE(handle)) {
return INVALID_RETURN;
}
status = psa_call(handle, type, invecs, inlen, outvecs, outlen);
psa_close(handle);
return TO_CALLER_STATUS(status);
}
This example encapsulates the PSA Client API, and can be provided as a simpler and more generic API for clients to call. It is not possible to statically link this API to each Secure Partition because of the limited storage space. The ideal solution is to put it inside SPRTL and share it to all Secure Partitions. This would simplify the caller logic into this:
if (psa_example_service() != STATUS_SUCCESS) {
/* do something */
}
This is the simplest case of encapsulating PSA Client API. If a RoT Service API is connect heavy, then, the encapsulation can be changed to include a connection handle inside a context data structure. This context data structure type is defined in RoT Service headers and the instance is allocated by API caller since API implementation does not have private data.
Note
Even the RoT Service APIs are provided in SPRTL for all clients, the SPM performs the access check eventually and decides if the access to service can be processed.
For those RoT Service APIs only get called by a specific client, they can be implemented inside the caller client, instead of putting it into SPRTL.
PSA Client and Service API
Most of the PSA APIs can be called directly with supervisor calls. The only
special function is psa_call
, because it has 6 parameters. This makes
the supervisor call handler complex because it has to extract the parameters
from the stack. The definition of psa_call is the following:
psa_status_t psa_call(psa_handle_t handle, int32_t type,
const psa_invec *in_vec, size_t in_len,
psa_outvec *out_vec, size_t out_len);
The parameters need to be packed to avoid passing parameters on the stack, and the supervisor call needs to unpack the parameters back to 6 for subsequent processing.
Privileged Access Supporting
Due to specified API (printf, e.g.) need to access privileged resources, TF-M Core needs to provide interface for the resources accessing. The permission checking must happen in Core while caller is calling these interface.
Secure Partition Local Storage
There are APIs that need to reference specific partition private data (‘malloc’ references local heap, e.g.), and the APIs reference the data by mechanisms other than function parameters. The mechanism in TF-M is called ‘Secure Partition Local Storage’.
A straight way for accessing the local storage is to put the local storage pointer in a known position in the stack, but there is a bit of difficulty in particular scenarios.
Note
The partition’s stack is not fixed-size aligned, using stack address aligning method can not work.
It requires privileged permission to access special registers such as PSPLIMIT. And Armv6-M and Armv7-M don’t have PSPLIMIT.`
Another common method is to put the pointer in one shared global variable, and the scheduler maintains the value of this variable to point to the running partition’s local storage in runtime. It does not fully align with SPRTL design prerequisites listed above, hence extra settings are required to guarantee the isolation boundaries are not broken.
Important
This variable is put inside a dedicated shared region and it can not hold information not belonging to the owner.
And this mechanism has disadvantages:
It needs extra maintenance effort from the scheduler and extra resources for containing the variable.
TF-M chooses this common way as the default option for local storage and can be expanded to support more methods.
Tooling Support on Partition Entry
PSA FF requires each Secure Partition to have an entry point. For example:
/* The entry point function must not return. */
void entry_point(void);
Each partition has its own dedicated local_storage for heap tracking and other runtime state. The local_storage is designed to be saved at the read-write data area of a partition with a specific name. A generic entry point needs to be available to get partition local_storage and do initialization before calling into the actual partition entry. This generic entry point is defined as ‘__sprtmain’:
void __sprtmain(void)
{
/* Get current SP private data from local storage */
struct p_sp_local_storage_t *m =
(struct p_sp_local_storage_t *)tfm_sprt_local_storage;
/* Potential heap init - check later chapter */
if (m->heap_size) {
m->heap_instance = tfm_sprt_heap_init(m->heap_sa, m->heap_sz);
}
/* Call thread entry 'entry_point' */
m->thread_entry();
/* Back to tell Core end this thread */
SVC(THREAD_EXIT);
}
Since SPM is not aware of the ‘__sprtmain’ in SPRTL, it just calls into the entry point listed in partition runtime data structure. And the partition writer may be not aware of running of ‘__sprtmain’ as the generic wrapper entry, tooling support needs to happen to support this magic. Here is an example of partition manifest:
{
"name": "TFM_SP_SERVICE",
"type": "PSA-ROT",
"priority": "NORMAL",
"entry_point": "tfm_service_entry",
"stack_size": "0x1800",
"heap_size": "0x1000",
...
}
Tooling would do manipulation to tell SPM the partition entry as ‘__sprtmain’, and TF-M SPM would maintain the local storage at run time. Finally, the partition entry point gets called and run, tooling helps on the decoupling of SPM and SPRTL implementation. The pseudo code of a tooling result:
struct partition_t sp1 {
.name = "TFM_SP_SERVICE",
.type = PSA_ROT,
.priority = NORMAL,
.id = 0x00000100,
.entry_point = __sprtmain, /* Tell SPM entry is '__sprtmain' */
.local_storage = { /* struct sprt_local_storage_t */
.heap_sa = sp1_heap_buf,
.heap_sz = sizeof(sp1_heap_buf),
.thread_entry = sp1_entry, /* Actual Partition Entry */
.heap_instance = NULL,
},
}
Implementation
The SPRTL C Runtime sources are put under: ‘$TFM_ROOT/secure_fw/partitions/lib/runtime/’
The output of this folder is a static library named as ‘libtfm_sprt.a’. The code of ‘libtfm_sprt.a’ is put into a dedicated section so that a hardware protected region can be applied to contain it.
The RoT Service API are put under service interface folder. These APIs are marked with the same section attribute where ‘libtfm_sprt.a’ is put.
The Formatting API - ‘printf’ and variants
The ‘printf’ and its variants need special parameters passing mechanism. To implement these APIs, the toolchain provided builtin macro ‘va_list’, ‘va_start’ and ‘va_end’ cannot be avoided. This is because of some scenarios such as when ‘stack canaries’ are enabled, only the compiler knows the format of the ‘canary’ in order to extract the parameters correctly.
To provide a simple implementation, the following requirements are defined for ‘printf’:
Format keyword ‘xXduscp’ needs to be supported.
Take ‘%’ as escape flag, ‘%%’ shows a ‘%’ in the formatted string.
To save heap usage, 32 bytes buffer in the stack for collecting formatted string.
Flush string outputting due to: a) buffer full b) function ends.
The interface for flushing can be a logging device.
Function needs implied inputs
Take ‘malloc’ as an example. There is only one parameter for ‘malloc’ in the prototype. Heap management code is put in the SPRTL for sharing with caller partitions. The heap instance belongs to each partition, which means this instance needs to be passed into the heap management code as a parameter. For allocation API in heap management, it needs two parameters - ‘size’ and ‘instance’, while for ‘malloc’ caller it needs a ‘malloc’ with one parameter ‘size’ only. As mentioned in the upper chapter, this instance can be retrieved from the Secure Partition Local Storage. The implementation can be:
void *malloc(size_t sz)
{
struct p_sp_local_storage_t *m =
(struct p_sp_local_storage_t *)tfm_sprt_local_storage;
return tfm_sprt_alloc(m->heap_instance, sz);
}
Copyright (c) 2019-2024, Arm Limited. All rights reserved.