3. C/C++ extensions for CHERIoT
The CHERIoT platform adds a small number of C/C++ annotations to support the compartment model.
3.1. Exposing compartment entry points
Compartments are discussed in detail in Chapter 4. A compartment can expose functions as entry points via a simple attribute.
The cheri_compartment({name})
attribute specifies the name of the compartment that defines a function.
This is used in concert with the -cheri-compartment=
compiler flag.
This allows the compiler to know whether a particular function (which may be in another compilation unit) is defined in the same compartment as the current compilation unit, allowing direct calls for functions in the same compilation unit and cross-compartment calls for other cases.
This can be used on either definitions or declarations but is most commonly used on declarations.
If a function is defined while compiling a compilation unit belonging to a different compartment then the compiler will raise an error.
In CHERIoT RTOS, this attribute is always used via the __cheri_compartment({name})
macro.
This makes it possible to simply use #define __cheri_compartment(x)
when compiling for other platforms.
3.2. Exposing library entry points
Libraries are discussed in Chapter 4. Like compartments, they can export functions, via a simple annotation.
The cheri_libcall
attribute specifies that this function is provided by a library (shared between compartments).
Libraries may not contain any writeable global variables.
This attribute is implicit for all compiler built-in functions, including memcpy
and similar freestanding C environment functions.
As with cheri_compartment()
, this may be used on both definitions and declarations.
This attribute can also be used via the __cheri_libcall
macro, which allows it to be defined away when targeting other platforms.
3.3. Passing callbacks to other compartments.
The cheri_ccallback
attribute specifies a function that can be used as an entry point by compartments that are passed a function pointer to it.
This attribute must also be used on the type of function pointers that hold cross-compartment invocations.
Any time the address of such a function is taken, the result will be a sealed capability that can be used to invoke the compartment and call this function.
The compiler does not know, when calling a callback, whether it points to the current compartment. As such, calling a CHERI callback function will always be a cross-compartment call, even if the target is in the current compartment. |
This attribute can also be used via the __cheri_callback
macro, which allows it to be defined away when targeting other platforms.
3.4. Interrupt state control
The cheri_interrupt_state
attribute (commonly used as a C++11 / C23 attribute spelled cheri::interrupt_state
) is applied to functions and takes an argument that is either:
-
enabled
, to enable interrupts when calling this function. -
disabled
, to disable interrupts when calling this function. -
inherit
, to not alter the interrupt state when invoking the function.
For most functions, inherit
is the default.
For cross-compartment calls, enabled
is the default and inherit
is not permitted.
The compiler may not inline functions at call sites that would change the interrupt state and will always call them via a sentry capability set up by the loader. This makes it possible to statically reason about interrupt state in lexical scopes.
If you need to wrap a few statements to run with interrupts disabled, you can use the convenience helper with_interrupts_disabled. This is annotated with the attribute that disables interrupts and invokes the passed lambda. This maintains the structured-programming discipline for code running with interrupts disabled: it is coupled to a lexical scope.
3.5. Importing MMIO access
The MMIO_CAPABILITY({type}, {name})
macro is used to access memory-mapped I/O devices.
These are specified in the board definition file by the build system.
The DEVICE_EXISTS({name})
macro can be used to detect whether the current target provides a device with the specified name.
The type
parameter is the type used to represent the MMIO region.
The macro evaluates to a volatile {type} *
, so MMIO_CAPABILITY(struct UART, uart)
will provide a volatile struct UART *
pointing (and bounded) to the device that the board definition exposes as uart
.
3.6. Manipulating capabilities with C builtins
The compiler provides a set of built-in functions for manipulating capabilities.
These are typically of the form __builtin_cheri_{noun}_{verb}
.
You can read all of the fields of a CHERI capability with get
as the verb and the following nouns:
address
-
The current address that’s used when the capability is used a pointer.
base
-
The lowest address that this authorises access to.
length
-
The distance between the base and the top.
perms
-
The architectural permissions that this capability holds.
sealed
-
Is this a sealed capability?
tag
-
Is this a valid capability?
type
-
The type of this capability (zero means unsealed).
The verbs vary because they express the guarded manipulation guarantees for CHERI capabilities. You can’t, for example, arbitrarily set the permissions on a capability, you can only remove permissions. Capabilities can be modified with the nouns and verbs listed in Table 3.
Noun |
Modification verb |
Operation |
|
|
Set the address for the capability. |
|
|
Sets the base at or below the current address and the length at or above the requested length, as closely as possible to give a valid capability |
|
|
Sets the base to the current address and the length to the requested length or returns an untagged capability if the result is not representable. |
|
|
Clears all permissions except those provided as the argument. |
|
|
Invalidates the capability but preserves all other fields. |
Setting the object type is more complex.
This is done with __builtin_cheri_seal
, which takes an authorising capability (something with the permit-seal permission) as the second argument and sets the object type of the result to the address of the sealing capability.
Conversely, __builtin_cheri_unseal
uses a capability with the permit-unseal capability and address matching the object type to restore the original unsealed value.
3.7. Comparing capabilities with C builtins
By default, the C/C++ ==
operator on capabilities compares only the address.
This is subject to change in a future revision of CHERI C.
It makes porting some existing code easier, but breaks the substitution principle (if a == b , you would expect to be able to use b or a interchangeably).
|
You can compare capabilities for exact equality with __builtin_cheri_equal_exact
.
This returns true if the two capabilities that are passed to it are identical, false otherwise.
Exact equality means that the address, bounds, permissions, object type, and tag are all identical.
It is, effectively, a bitwise comparison of all of the bits in the two capabilities, including the tag bits.
Ordered comparison, using operators such as less-than or greater-than, always operate with the address.
There is no total ordering over capabilities.
Two capabilities with different bounds or different permissions but the same address will return false when compared with either <
or >
.
This is fine according to a strict representation of the C abstract machine because comparing two pointers to different objects is undefined behaviour.
It can be confusing but, unfortunately, there is no good alternative.
Comparison of pointers is commonly used for keying in collections.
For example, the C++ std::map
class uses the ordered comparison operators for building a tree and relies on it working correctly for keys that are pointers.
Ideally, these would explicitly operate over the address, but that would require invasive modifications when porting to CHERI platforms.
In general, in new code, you should avoid comparing pointers for anything other than exact equality, unless you are certain that they have the same base and bounds. Instead, be explicit about exactly what you are testing. Do you care if the permissions are different? Do you care about the bounds? Do you care if the value is tagged? Or do you just want to care about the address? In each case, you should explicitly compare the components of the capability that you care about.
You can also compare capabilities for subset relationships with __builtin_cheri_subset_test
.
This returns true if the second argument is a subset of the first.
A capability is a subset of another if every right that it conveys is held by the other.
This means the bounds of the subset capability must be smaller than or equal to the superset and all permissions held by the subset must be held by the superset.
3.8. Sizing allocations
CHERI capabilities cannot represent arbitrary bases and bounds. The larger the bounds, the more strongly aligned the base and bounds must be.
The current CHERIoT encoding gives byte-granularity bounds for objects up to 511 bytes, then requires one more bit of alignment for each bit needed to represent the size, up to 8 MiB. Capabilities larger than 8 MiB cover the entire address space. This is ample for small embedded systems where most compartments or heap objects are expected to be under tens of KiBs. Other CHERI systems make different trade offs. |
Calculating the length can be non-trivial and can vary across CHERI systems. The compiler provides two builtins that help.
The first, __builtin_cheri_round_representable_length
, returns the smallest length that is larger than (or equal to) the requested length and can be accurately represented.
The compressed bounds encoding requires both the top and base to be aligned on the same amount and so there’s a corresponding mask that needs to be used for alignment.
The __builtin_cheri_representable_alignment_mask
builtin returns the mask that can be applied to the base and top addresses to align them.
3.9. Manipulating capabilities with CHERI::Capability
The raw C builtins can be somewhat verbose.
CHERIoT RTOS provides a CHERI::Capability
class in cheri.hh
to simplify inspecting and manipulating CHERI capabilities.
These provide methods that are modelled to allow you to pretend that they give direct access to the fields of the capability. For example, you can write:
capability.address() += 4;
capability.permissions() &= permissionSet;
This modifies the address of capability
, increasing it by four, and removes all permissions not present in permissionSet
.
Other operations are also defined to be orthogonal.
Permissions are exposed as a PermissionSet
object.
This is a constexpr
class that provides a rich set of operations on permissions.
This can be used as a template parameter and can be used in static assertions for compile-time validation of derivation chains.
The loader makes extensive use of this class to ensure correctness.
The equality comparison for CHERI::Capability uses exact comparison, unlike raw C/C++ pointer comparison.
This is less confusing for new code (it respects the substitution principle) but users may be confused that a == b is true but Capability{a} == Capability{b} is false.
|
See cheri.hh
for more details and for other convenience wrappers around the compiler builtins.