11. C29x Security Model¶
11.1. The Safety and Security Unit (SSU)¶
The TI C29x may take advantage of the special Safety and Security Unit (SSU), which protects code and data within an application. The Implementing Run-Time Safety and Security With the C29x Safety and Security Unit (SPRADK2 <https://www.ti.com/lit/pdf/SPRADK2>__**) application note describes this unit in depth. This page provides an introduction to the SSU.
11.2. Key Terms and Constructs¶
The SSU is configured with a hierarchy of control:
An Access Protection Range (APR) is a region of memory with associated read/write permissions.
The LINK is one or more regions of executable code that control data accesses (reads and writes) to both memory and peripherals.
The STACK isolates execution contexts and the stack pointer address (A15).
The ZONE is configured to control debugging and breakpoint support, as well as firmware update permissions.
A ZONE contains one or more STACKs. A STACK contains one or more LINKs. A LINK is a member of one and only one STACK. A STACK is a member of one and only one ZONE.
APRs are granted or denied read/write access to LINKs via hardware configurations.
In this document, the all-capital APR, LINK, STACK, and ZONE terms refer to the SSU concepts instead of other definitions.
11.3. Inter-STACK Calls¶
A call from one LINK in a STACK to a different link in another STACK is called an inter-STACK call. Such calls are heavily restricted:
A special “protected” call instruction must be used to transfer control from caller to callee.
The first instruction fetched from the callee must be a special handshake instruction packet.
A special “protected” return instruction must be used to transfer control from callee to caller.
The first instruction fetched from the caller upon return must be a special handshake instruction packet.
All registers are cleared by both call and return except registers passed to or returned from the callee.
The stack pointer (A15) is not shared by the caller and callee.
Functions with variadic arguments or those that pass or return structures and/or non-fundamental types use the stack for storage according to the rules of the C29x ABI. Such functions can’t be the callee of an inter-STACK call because the stack pointer changes.
Any call whose caller and callee are in the same STACK may be referred to as an intra-STACK call. Such calls are less restricted. None of the restrictions above apply.
11.3.1. Compiler Support for Inter-STACK Calls¶
C/C++ functions can be declared/defined using the c29_protected_call
function attribute. This attribute causes the c29clang compiler to act as if calls to that function or function type are inter-STACK calls. At the assembly level, the compiler emits “protected” calls and returns with the proper registers preserved in addition to the handshake instructions at both the callee’s entry and at the return address. See Function Attributes for more about function attributes.
For example:
void my_function() __attribute__((c29_protected_call)) {
return; // The definition of my_function, along with the attribute,
// results in the correct handshake and protected return.
}
void call_my_function() {
my_function(); // my_function is declared protected, so this results in
// the correct protected call and return handshake.
}
void normal_function();
void (*protected_fn_ptr)() __attribute__((c29_protected_call)) = &normal_function; // Suppressible warning in C
void call_normal_function {
normal_function(); // Normal call instruction with no handshake
// Note: normal_function will not have the handshake and protected
// return unless its definition also has the c29_protected_call
// attribute.
protected_fn_ptr(); // Results in protected call and return handshake
}
The c29clang compiler identifies and produces an error for any protected function that cannot be called due to stack requirements from the ABI. Example causes for such errors include passing excessive numbers of integer, pointer, or floating-point arguments in registers, returning a structure type by value, and use of variadic arguments.
11.4. Security Support in the Linker¶
11.4.1. The SECURE_GROUP Memory Range Attribute¶
The linker accepts the SECURE_GROUP attribute in the MEMORY region of the linker command file. This attribute declares the type of secure behavior allowed in code placed in that memory range. The syntax is as follows:
SECURE_GROUP ( name [, (public|private)][, reads=(a, b…)][, writes=(a, b,…))
The following example assigns the .text_caller section to the CALLER_GROUP call group:
MEMORY {
RO_CODE1: origin=0x20, length=0x1000, SECURE_GROUP(CALLER_GROUP)
}
SECTIONS {
.text_caller : { *(.text.caller) } > RO_CODE1
}
A call group is a functional name for a group of code within a single secure module. The linker assumes that code placed in the same call group can freely call to and return from one another without protection, including sharing the same stack pointer.
The following example extends the above linker command file snippet by adding .text_callee in the CALLEE_GROUP call group:
MEMORY {
RO_CODE1: origin=0x20, length=0x1000, SECURE_GROUP(CALLER_GROUP)
RO_CODE2: origin=0x1020, length=0x1000, SECURE_GROUP(CALLEE_GROUP)
}
SECTIONS {
.text_caller : { *(.text.caller) } > RO_CODE1
.text_callee : { *(.text.callee) } > RO_CODE2
}
If a caller and callee might be placed in such a way that they are in different call groups, the linker assumes that it is an inter-STACK call and requires protection.
If the callee was annotated with the
c29_protected_call
attribute in the source code, the linker identifies that protection has already been added and continues.If the callee was not annotated with the
c29_protected_call
attribute in the source code, the linker will issue an error, refusing to allow insecure transfer of control.
11.4.2. Linker-generated Secure Calls (Bridging)¶
If it’s not feasible to annotate the callee with the c29_protected_call
attribute, the linker provides a way to insert the proper CALL.PROT and RET.PROT sequences at the cost of code size, stack usage, and execution cycles. The following example introduces the PUBLIC SECURE_GROUP attribute:
MEMORY {
RO_CODE1: origin=0x20, length=0x1000, SECURE_GROUP(CALLER_GROUP)
RO_CODE2: origin=0x1020, length=0x1000, SECURE_GROUP(CALLEE_GROUP, PUBLIC)
}
SECTIONS {
.text_caller : { *(.text.caller) } > RO_CODE1
.text_callee : { *(.text.callee) } > RO_CODE2
}
The default, and opposite of the PUBLIC attribute is the PRIVATE attribute, which can be specified for readability, but is not required.
A PUBLIC SECURE_GROUP instructs the linker to transform an insecure transfer of control into a secure form. This is done by inserting veneers called trampolines and landing pads.
The linker inserts a trampoline if the call site is not secure. A trampoline contains code to:
Save every register on the stack.
Securely transfer control to and receive the protected return from the callee.
Restore the saved registers.
Transfer control back to the caller.
A landing pad is inserted if the callee is not secure. A landing pad contains code to:
Receive the protected call from the caller.
Insecurely transfer control to the callee.
Securely transfer control back to the caller.
The linker generates both veneers if the call site or callee are insecure. However, if either the call site’s function pointer type or the callee’s definition are marked with the c29_protected_call
attribute, the associated veneer is not necessary, saving a portion of the overhead in cycles and memory usage. This is especially true for the call site, as the register saves take a considerable amount of time and space.
11.4.3. Memory Access Protections¶
Outside of secure calls and returns, the C29 Safety and Security Unit (SSU) provides memory access protections. While two functions might be part of the same STACK, they may not necessarily have read/write access to the same APRs. The READS and WRITES attributes of SECURE_GROUP describe an abstract form of these access permissions:
MEMORY {
RO_CODE1: origin=0x20, length=0x1000, SECURE_GROUP(SAME_GROUP, READS=(DATA1))
RO_CODE2: origin=0x1020, length=0x1000, SECURE_GROUP(SAME_GROUP, READS=(DATA2))
DATA1: origin=0x10000000, length=0x1000
DATA2: origin=0x10002000, length=0x1000
}
SECTIONS {
.text_caller : { *(.text.caller) } > RO_CODE1
.text_callee : { *(.text.callee) } > RO_CODE2
}
In this example, the caller and callee are within the same group, but the linker observes that the caller may access DATA1 while the callee may access DATA2. While a call between caller and callee need not be protected, there is an expectation that the code in callee should not be executed in caller, and vice versa. This has consequences when considering interprocedural optimization.
Note that the arguments to the READS and WRITES list are informative strings and may be any valid identifier. For simplicity, it’s often useful to define them in terms of MEMORY ranges that will be accessible.
11.4.4. SECURE_GROUP and Link-time Optimization¶
Link-time optimization is a powerful optimization strategy that allows the compiler to optimize while being aware of as much of the program as possible. This is similar to taking all source files, headers, and libraries used to build the program and creating one large source file. Definitions of functions are visible to call sites for inlining, global constants are known when used, etc.
These types of optimizations must be done carefully when security is active. Many of these optimizations move potentially proprietary code and inaccessible data through the boundary of a call site.
In the case of inter-STACK calls, no such movement should be possible because the system configuration has specifically disallowed the caller and callee from sharing data that isn’t part of the ABI.
In the case of intra-STACK calls, there is still a risk of code containing a data access moving across the boundary into an APR that does not have that permission, causing a security fault.
The linker will not move code or data accesses through the secure boundary of an inter-STACK call during link-time optimization.
The linker refers to the READS and WRITES lists of SECURE_GROUPs for intra-STACK calls, deciding to allow or prohibit optimization across the call using the following rules:
If the caller may be allocated to multiple memory ranges, the linker assumes it only has access to memory that is shared by all possibilities. That is, the assumed access is the intersection of all READS or all WRITES of associated MEMORY ranges.
If the callee may be allocated to multiple memory ranges, the linker assumes it will access all possibilities. That is, the assumed access is the union of all READS or all WRITES of associated MEMORY ranges.
Optimization is prohibited if the caller’s sum of access doesn’t contain all of the callee’s potential accesses.
In the following example from linker command file snippets, MEM_* are MEMORY regions and caller/callee are output sections, where code in .caller calls into code in .callee:
// Not optimized: MEM_A can't read MEM_2, required by MEM_B
MEM_A: SECURE_GROUP(GROUP1, READS=(MEM_1))
MEM_B: SECURE_GROUP(GROUP1, READS=(MEM_2))
.caller {} > MEM_A
.callee {} > MEM_B
// Not optimized: MEM_A can't write MEM_1, only read it
MEM_A: SECURE_GROUP(GROUP1, READS=(MEM_1))
MEM_B: SECURE_GROUP(GROUP1, WRITES=(MEM_1))
.caller {} > MEM_A
.callee {} > MEM_B
// Not optimized: if callee is placed into MEM_B2, then caller can't cover
// its READ of MEM2
MEM_A: SECURE_GROUP(GROUP1, READS=(MEM_1))
MEM_B1: SECURE_GROUP(GROUP1, READS=(MEM_1))
MEM_B2: SECURE_GROUP(GROUP1, READS=(MEM_2))
.caller {} > MEM_A
.callee {} > MEM_B1|MEM_B2
// Not optimized: if caller is placed in MEM_A1 and callee is placed in
// MEM_B2, or caller in MEM_A2 and callee in MEM_B1,
// accesses are not covered.
MEM_A1: SECURE_GROUP(GROUP1, READS=(MEM_1))
MEM_A2: SECURE_GROUP(GROUP1, READS=(MEM_2))
MEM_B1: SECURE_GROUP(GROUP1, READS=(MEM_1))
MEM_B2: SECURE_GROUP(GROUP1, READS=(MEM_2))
.caller {} > MEM_A1|MEM_A2
.callee {} > MEM_B1|MEM_B2
// Optimized: take the accesses shared by all of caller's possibilities:
// READS=(MEM_1, MEM_2, MEM_3) WRITES=(MEM_4), where
// MEM_X, MEM_Y, and MEM_Z are not shared between MEM_A1 and MEM_A2
// Take all accesses from all possible placement callee:
// READS=(MEM_1, MEM_3) WRITES=(MEM_4)
// Since the caller's READS and WRITES sets are supersets of the callees,
// optimization is allowed.
MEM_A1: SECURE_GROUP(GROUP1, READS=(MEM_1, MEM_2, MEM_3, MEM_X),
WRITES=(MEM_4))
MEM_A2: SECURE_GROUP(GROUP1, READS=(MEM_1, MEM_2, MEM_3, MEM_Y),
WRITES=(MEM_4, MEM_Z))
MEM_B1: SECURE_GROUP(GROUP1, READS=(MEM_1))
MEM_B2: SECURE_GROUP(GROUP1, READS=(MEM_3))
MEM_B3: SECURE_GROUP(GROUP1, WRITES=(MEM_4))
MEM_B4: SECURE_GROUP(GROUP1)
.caller {} > MEM_A1|MEM_A2
.callee {} > MEM_B1|MEM_B2|MEM_B3|MEM_B4
11.4.5. Optimization and Security Caveats¶
Within a single file (after preprocessing, so including headers), the only guaranteed security control is the c29_protected_call
attribute. For example, a function marked with this attribute will not be inlined at any call site.
Because there is no guarantee of security, the data and code seen within a single compilation unit (a file with all includes expanded and macros replaced) is considered visible and accessible to all code in the same unit. Projects should be structured such that sensitive or proprietary code and data is not visible to untrusted modules. For example, avoid defining sensitive or proprietary functions in header files that may be included from untrusted modules.