doc/reference/trd-storage-permissions.md
TRD:
Working Group: Kernel
Type: Documentary
Status: Draft
Author: Brad Campbell
Draft-Created: 2024/06/06
Draft-Modified: 2024/06/06
Draft-Version: 1
Draft-Discuss: [email protected]
Tock supports storing persistent state for applications, and all persistent state in Tock is identified based on the application that stored it. Tock supports permissions for persistent state, allowing for the kernel to restrict which applications can store state and which applications can read stored state. This TRD describes the permissions architecture for persistent state in Tock. This document is in full compliance with TRD1.
Tock applications need to be able to store persistent state. Additionally, applications need to be able to keep data private from other applications. The kernel should also be able to allow specific applications to read and modify state from other applications.
This requires a method for assigning applications persistent identifiers, a mechanism for granting storage permissions to specific applications, and kernel abstractions for implementing storage capsules that respect the storage permissions.
This document only describes the permission architecture in Tock for supporting application persistent storage. This document does not prescribe specific types of persistent storage (e.g., flash, FRAM, etc.), storage access abstractions (e.g., block-access, byte-access, etc.), or storage interfaces (e.g., key-value, filesystems, logs, etc.).
All shared persistent storage implementations must store a 32 bit identifier with each stored object to mark the application that created the stored object.
When applications write data, their ShortId must be used as the identifier. When the kernel writes data, the identifier must be 0.
The security, uniqueness, mapping policy, and other properties of ShortIds are allowed to vary based on board configuration. For storage use cases which have specific concerns or constraints around the policies for storage identifiers, users should consult the properties of ShortIds afforded by AppId policy.
All persistent application data is labeled based on the application which wrote the data. Applications can read and modify data with suitable permissions.
There are three types of permissions:
Each permission type is independent. For example, an application can be given read permission for specific data but not be able to write new data itself.
Write is a boolean permission. An application either has permission to write or it does not.
Read and Modify permissions are tuples of (the permission type, stored state identifier). These permissions only exist as associated with a particular
stored state identifier. That is, a Read permission gives an application
permission to read only stored state marked with the associated stored state
identifier, and a Modify permission gives an application permission to modify
only stored state marked with the associated stored state identifier.
The Tock storage model imposes the following requirements:
ShortId::Fixed cannot access (i.e.,
read/write/modify) any persistent storage.Additionally, the kernel itself can be given permission to store state.
As all persistent state written by applications is marked with the writing application's ShortId, the assignment mechanism for ShortIds is tightly coupled with the access policies for persistent state. This coupling is intentional as AppIDs are unique to specific applications. However, as ShortIds are only 32 bits, it is not possible to assign a globally unique ShortId to all applications. Therefore, board authors should be intentional with how ShortIds are assigned when persistent storage is accessible to userspace.
In particular, two potentially problematic cases can arise:
It is not feasible to implement all persistent storage APIs through the core kernel (i.e., in trusted code). Instead, the kernel provides an API to retrieve the storage permissions for a specific process. Capsules then use these permissions to enforce restrictions on storage access. The API consists of these functions:
/// Check if these storage permissions grant read access to the stored state
/// marked with identifier `stored_id`.
pub fn check_read_permission(&self, stored_id: u32) -> bool;
/// Check if these storage permissions grant modify access to the stored
/// state marked with identifier `stored_id`.
pub fn check_modify_permission(&self, stored_id: u32) -> bool;
/// Retrieve the identifier to use when storing state, if the application
/// has permission to write. Returns `None` if the application cannot write.
pub fn get_write_id(&self) -> Option<u32>;
This API is implemented for the StoragePermissions object.
The StoragePermissions type can be stored per-process and passed in
storage APIs to express the storage permissions of the caller of any storage
operations.
When writing storage capsules, capsule authors should include APIs which include
StoragePermissions as an argument, and should check for permission before
performing any storage operation.
For example, a filing cabinet abstraction that identifies stored state based on a record name might have an (asynchronous) API like this:
pub trait FilingCabinet {
fn read(&self, record: &str, permissions: StoragePermissions) -> Result<(), ErrorCode>;
fn write(&self, record: &str, data: &[u8], permissions: StoragePermissions) -> Result<(), ErrorCode>;
}
Inside the implementation for any storage abstraction, the implementation must consider three operations and check for permissions:
ErrorCode::NOSUPPORT. If there is
stored state that matches the request, the capsule must call
StoragePermissions::check_read_permission(stored_id) with the identifier
associated with the stored record. If check_read_permission() returns
false, the capsule should return ErrorCode::NOSUPPORT. If
check_read_permission() returns true, the capsule should return the read
data.StoragePermissions::get_write_id(). If get_write_id()
returns None, the capsule should return ErrorCode::NOSUPPORT. If
get_write_id() returns Some(), the capsule should save the new data and
must use the returned u32 identifier. It should then return Ok(()).StoragePermissions::check_modify_permission(stored_id). If
check_modify_permission() returns false, the capsule should return
ErrorCode::NOSUPPORT. If check_modify_permission() returns true, the
capsule should overwrite the data while not changing this stored identifier.
The capsule should then return Ok(()).For example, with the filing cabinet example:
pub trait FilingCabinet {
fn read(&self, record: &str, permissions: StoragePermissions) -> Result<[u8], ErrorCode> {
let obj = self.cabinet.read(record);
match obj {
Some(r) => {
if permissions.check_read_permission(r.id) {
Ok(r.data)
} else {
Err(ErrorCode::NOSUPPORT)
}
}
None => Err(ErrorCode::NOSUPPORT),
}
}
fn write(&self, record: &str, data: &[u8], permissions: StoragePermissions) -> Result<(), ErrorCode> {
let obj = self.cabinet.read(record);
match obj {
Some(r) => {
if permissions.check_modify_permission(r.id) {
self.cabinet.write(record, r.id, data);
Ok(())
} else {
Err(ErrorCode::NOSUPPORT)
}
}
None => {
match permissions.get_write_id() {
Some(id) => {
self.cabinet.write(record, id, data);
Ok(())
}
None => Err(ErrorCode::NOSUPPORT),
}
}
}
}
}
StoragePermissions TypeThe kernel defines a StoragePermissions type which expresses the storage
permissions of an application. This is implemented as a definite type rather
than a trait interface so permissions can be passed in storage APIs without
requiring a static object for every process in the system.
The StoragePermissions type is capable of holding storage permissions in
different formats. In general, the type looks like:
pub struct StoragePermissions(StoragePermissionsPrivate);
enum StoragePermissionsPrivate {
SelfOnly(core::num::NonZeroU32),
FixedSize(FixedSizePermissions),
Listed(ListedPermissions),
Kernel,
Null,
}
Each variant is a different method for representing and storing storage
permissions. For example, FixedSize contains fixed size lists of permissions,
where as Null grants no storage permissions.
The StoragePermissions struct includes multiple constructors for instantiating
storage permissions. The struct wraps the enum to ensure that permissions can
only be created with those constructors. The constructors require a capability
to use so only trusted code can create storage permissions.
Different users and different kernels will use different methods for determining the persistent storage access permissions for different applications (and by extensions the running process for that application). The following are some examples of how storage permissions may be specified.
StoragePermissions TBF header allows a developer to
specify storage permissions when the app is compiled. Using this method
assumes the kernel can trust the application's headers, perhaps because the
kernel only runs apps signed by a trusted party that has verified the TBF
headers.The core kernel allows individual boards to configure how permissions are
assigned to applications. At runtime, the kernel needs to know what permissions
each executing process has. To facilitate this, Tock uses the
ProcessStandardStoragePermissionsPolicy process policy. Each process, when created,
will store a StoragePermissions object that specifies the storage permissions for
that process.
/// Generic trait for implementing a policy on how applications should be
/// assigned storage permissions.
pub trait ProcessStandardStoragePermissionsPolicy<C: Chip> {
/// Return the storage permissions for the specified `process`.
fn get_permissions(&self, process: &ProcessStandard<C>) -> StoragePermissions;
}
This trait is specific to the ProcessStandard implementation of Process to
enable policies to use TBF headers when assigning permissions.
Several examples of policies are in the capsules/system crate.
The permissions architecture is generic for storage in Tock, but this section describes some examples of how this architecture may be used for several storage abstractions. Note, these are just examples and not descriptions of actual Tock implementations nor requirements for how various storage abstractions must be implemented.
get(), the storage identifier for the key-value
pair is checked. On set(), if the key already exists the modify permission
is used, and if the key does not exist the write permission is used.Brad Campbell <[email protected]>