Migrating TA Code to the Typed Parameter API
This guide explains how to migrate OP-TEE TA code from the deprecated
optee_utee::Parameters API to the typed parameter API. The public types are
re-exported from optee_utee and optee_utee::prelude.
The typed API keeps the same OP-TEE parameter model: every entry point receives
up to four parameters, and each slot has one runtime type tag. The difference is
that TA code now represents those tags with Rust types such as
ParameterMemrefInput<'_>, ParameterValueOutput<'_>, and ParameterNone.
The Normal World client application may be implemented with any OP-TEE client
stack; this document only describes the Secure World TA side.
Why migrate
The old API exposed each slot as a generic Parameter and required unsafe
accessors:
let mut p0 = unsafe { params.0.as_memref()? };
let buf = p0.buffer();
That code allowed TA-side code to request a value or memref dynamically at each use site. A mismatch was discovered only when that unsafe accessor ran, or worse, hidden behind mutable access to a buffer whose intended direction was not visible in the Rust type.
The new API makes the expected direction explicit:
let input = p0.as_memref_input()?.read_to_vec();
let output = p1.as_memref_output()?;
output.set_output(bytes)?;
This does not make the client application and TA parameter layouts a compile-time contract. They are built as separate OP-TEE components, and the TEE still delivers parameter type tags at runtime. The typed API improves the TA side: it records the TA's expected layout in Rust types, performs consistent runtime validation at the entry point or branch accessor, and removes most call-site unsafe code.
For commands with a fixed parameter layout, the entry-point signature can use a
concrete 4-tuple. For commands where different command IDs use different
layouts, use ParametersAny<'_> and validate each slot inside the command
branch.
Imports
Most TA code should import the prelude:
use optee_utee::prelude::*;
use optee_utee::{ErrorKind, Result};
The prelude exports the TA entry-point macros, ParametersAny,
ParametersNone, all typed parameter wrappers, and the read/write traits needed
for methods such as read_to_vec, read_at, get_a, and set_output.
Parameter Directions
Choose typed wrappers from the TA's perspective:
| Data flow | TA typed parameter |
|---|---|
| No parameter in this slot | ParameterNone |
Client application provides two u32 values for the TA to read | ParameterValueInput |
TA writes two u32 values back to the client application | ParameterValueOutput<'_> |
TA reads and writes two u32 values | ParameterValueInout<'_> |
| Client application provides a buffer for the TA to read | ParameterMemrefInput<'_> |
| TA writes bytes into a client-provided output buffer | ParameterMemrefOutput<'_> |
| TA reads and writes the same buffer | ParameterMemrefInout<'_> |
The direction names are strict. If the TA writes to a buffer, use
ParameterMemrefOutput<'_> or ParameterMemrefInout<'_>. If the TA only reads
from a buffer, use ParameterMemrefInput<'_> or ParameterMemrefInout<'_>.
Entry-Point Signatures
The #[ta_open_session] and #[ta_invoke_command] macros accept any parameter
type that implements FromRawParameters<'_>. In normal code this means either:
ParametersNoneParametersAny<'_>- A concrete 4-tuple of typed wrappers
No Parameters
Use ParametersNone when no parameters are expected.
#[ta_open_session]
fn open_session(_params: &mut ParametersNone) -> Result<()> {
Ok(())
}
#[ta_invoke_command]
fn invoke_command(cmd_id: u32, _params: &mut ParametersNone) -> Result<()> {
match Command::from(cmd_id) {
Command::Ping => Ok(()),
_ => Err(ErrorKind::BadParameters.into()),
}
}
Fixed Layout
Use a concrete tuple when every supported command for an entry point uses the
same parameter layout. The macro converts the raw OP-TEE parameters into the
tuple before your function runs, and returns TEE_ERROR_BAD_PARAMETERS if any
runtime slot type does not match the tuple.
#[ta_invoke_command]
fn invoke_command(
cmd_id: u32,
(input, output, _, _): &mut (
ParameterMemrefInput<'_>,
ParameterMemrefOutput<'_>,
ParameterNone,
ParameterNone,
),
) -> Result<()> {
let bytes = input.read_to_vec();
let response = handle_request(Command::from(cmd_id), &bytes)?;
output.set_output(response)
}
Use this style for examples such as a single request buffer plus a single response buffer.
Command-Dependent Layout
Use ParametersAny<'_> when different command IDs use different layouts. Each
slot is first decoded into ParameterAny, and command branches then request the
specific type they expect.
#[ta_invoke_command]
fn invoke_command(cmd_id: u32, (p0, p1, p2, _): &mut ParametersAny<'_>) -> Result<()> {
match Command::from(cmd_id) {
Command::Update => {
let input = p0.as_memref_input()?.read_to_vec();
digest_update(&input);
Ok(())
}
Command::DoFinal => {
let input = p0.as_memref_input()?.read_to_vec();
let output = p1.as_memref_output()?;
let mut out_buf = vec![0u8; output.buffer_len()];
let size = digest_final(&input, &mut out_buf)?;
p2.as_value_output()?.set_a(size as u32);
output.set_output(&out_buf[..size])
}
_ => Err(ErrorKind::BadParameters.into()),
}
}
This is the right choice for cryptographic examples where Prepare, Update,
and Final commands use different parameter directions.
Replacing Value Access
Old code:
let value = unsafe { params.0.as_value()? };
let mode = value.a();
let flags = value.b();
New code for an input value:
let value = p0.as_value_input()?;
let mode = value.get_a();
let flags = value.get_b();
New code for an output value:
let value = p0.as_value_output()?;
value.set_a(result_len as u32);
value.set_b(status);
New code for an in/out value:
let value = &mut params.0;
value.set_a(value.get_a() + 1);
The trait split is intentional:
| Operation | Trait | Implemented by |
|---|---|---|
get_a, get_b | ParameterValueRead | ParameterValueInput, ParameterValueInout |
set_a, set_b | ParameterValueWrite | ParameterValueOutput, ParameterValueInout |
Replacing Memref Access
Old code:
let mut p0 = unsafe { params.0.as_memref()? };
let input = p0.buffer();
New code for an input memref:
let input = p0.as_memref_input()?.read_to_vec();
Use read_at when only part of the buffer is needed. It returns the number
of bytes actually read, truncated at the end of the buffer (BadParameters
if offset is beyond the end):
let mut header = [0u8; 8];
let n = p0.as_memref_input()?.read_at(0, &mut header)?;
read_to_vec allocates a host-sized heap buffer, so prefer read_at
into a stack array for fixed-size fields, and bound buffer_len() before
copying variable-size input you are not willing to allocate in full.
Old code for output:
let mut p1 = unsafe { params.1.as_memref()? };
let output = p1.buffer();
output[..bytes.len()].copy_from_slice(&bytes);
p1.set_updated_size(bytes.len());
New code:
let output = p1.as_memref_output()?;
output.set_output(bytes)?;
Use set_output for the common case where the output starts at offset 0. Use
write_at when appending or writing at a specific offset:
let output = p0.as_memref_output()?;
output.write_at(0, header)?;
output.write_at(header.len(), body)?;
When an API needs a scratch buffer it can write into, copy the result out afterwards:
let input = p0.as_memref_input()?.read_to_vec();
let output = p1.as_memref_output()?;
let mut tmp = vec![0u8; output.buffer_len()];
let written = cipher.update(&input, &mut tmp)?;
output.set_output(&tmp[..written])
set_output copies and updates the size in one step. set_updated_size
should only be used after write_at if manual size handling is needed.
The memref traits are:
| Operation | Trait | Implemented by |
|---|---|---|
buffer_len | ParameterMemref | ParameterMemrefInput, ParameterMemrefOutput, ParameterMemrefInout |
read_to_vec, read_at | ParameterMemrefRead | ParameterMemrefInput, ParameterMemrefInout |
set_updated_size, set_output, write_at | ParameterMemrefWrite | ParameterMemrefOutput, ParameterMemrefInout |
Open Session Parameters
Session opening is migrated the same way as command invocation.
If the TA expects no parameters during session opening, use ParametersNone:
#[ta_open_session]
fn open_session(_params: &mut ParametersNone) -> Result<()> {
Ok(())
}
If the TA expects open-session parameters, express that expected layout with a
concrete tuple or ParametersAny<'_>. For example, this TA expects slot 0 to be
a client-provided input buffer containing an f64 learning rate:
#[ta_open_session]
fn open_session(
(p0, _, _, _): &mut (
ParameterMemrefInput<'_>,
ParameterNone,
ParameterNone,
ParameterNone,
),
) -> Result<()> {
// Single TA-owned copy, then validate/use it to avoid TOCTOU.
let buf = p0.read_to_vec();
let lr_bytes: [u8; 8] = buf
.try_into()
.map_err(|_| ErrorKind::BadParameters)?;
let learning_rate = f64::from_le_bytes(lr_bytes);
init(learning_rate)
}
When the runtime parameters do not match this tuple, the generated entry point
returns TEE_ERROR_BAD_PARAMETERS before calling open_session.
Session Context
The parameter migration does not change session-context handling. If the old entry point had a context parameter, keep it and only change the parameter type:
#[ta_open_session]
fn open_session(_params: &mut ParametersNone, ctx: &mut AesCipher) -> Result<()> {
*ctx = AesCipher::default();
Ok(())
}
#[ta_invoke_command]
fn invoke_command(
ctx: &mut AesCipher,
cmd_id: u32,
params: &mut ParametersAny<'_>,
) -> Result<()> {
dispatch(ctx, cmd_id, params)
}
The context type must still implement Default for #[ta_open_session] with a
context parameter.
Complete Migration Example
Old code:
#[ta_invoke_command]
fn invoke_command(cmd_id: u32, params: &mut Parameters) -> Result<()> {
match Command::from(cmd_id) {
Command::Serialize => {
let mut p0 = unsafe { params.0.as_memref()? };
let output = p0.buffer();
let bytes = serde_json::to_vec(&Point { x: 1, y: 2 })
.map_err(|_| ErrorKind::BadParameters)?;
if bytes.len() > output.len() {
p0.set_updated_size(bytes.len());
return Err(ErrorKind::ShortBuffer.into());
}
output[..bytes.len()].copy_from_slice(&bytes);
p0.set_updated_size(bytes.len());
Ok(())
}
_ => Err(ErrorKind::BadParameters.into()),
}
}
New code:
#[ta_invoke_command]
fn invoke_command(cmd_id: u32, (p0, _, _, _): &mut ParametersAny<'_>) -> Result<()> {
match Command::from(cmd_id) {
Command::Serialize => {
let output = p0.as_memref_output()?;
let bytes = serde_json::to_vec(&Point { x: 1, y: 2 })
.map_err(|_| ErrorKind::BadParameters)?;
output.set_output(bytes)
}
_ => Err(ErrorKind::BadParameters.into()),
}
}
If Serialize is the only command and the layout is fixed, the TA entry-point
validation can be made stricter:
#[ta_invoke_command]
fn invoke_command(
cmd_id: u32,
(p0, _, _, _): &mut (
ParameterMemrefOutput<'_>,
ParameterNone,
ParameterNone,
ParameterNone,
),
) -> Result<()> {
match Command::from(cmd_id) {
Command::Serialize => {
let bytes = serde_json::to_vec(&Point { x: 1, y: 2 })
.map_err(|_| ErrorKind::BadParameters)?;
p0.set_output(bytes)
}
_ => Err(ErrorKind::BadParameters.into()),
}
}
Migration Checklist
- Replace
use optee_utee::{..., Parameters, ...}withuse optee_utee::prelude::*. - For each
#[ta_open_session], write down the TA's expected four-slot parameter layout. UseParametersNoneonly when all four slots are expected to beNone. - For each
#[ta_invoke_command], write down the TA's expected four-slot layout for each command ID. - Use a concrete 4-tuple when every command in the entry point uses the same layout.
- Use
ParametersAny<'_>when command IDs use different layouts. - Replace
as_value()?.a()andas_value()?.b()withget_a()andget_b()on the correct value wrapper. - Replace
set_a()andset_b()on oldParamValuewith the same methods onParameterValueOutputorParameterValueInout. - Replace
as_memref()?.buffer()reads withread_to_vec()orread_at(). - Replace manual output-buffer copies with
set_outputorwrite_at. - Ensure every unused slot is represented as
ParameterNone, not omitted.
Common Errors
TEE_ERROR_BAD_PARAMETERS During Open Session
This means the runtime open-session parameters did not match the TA
open-session signature. If the TA expects any value or memref slot, do not use
ParametersNone; use a concrete tuple or ParametersAny<'_> that describes
the expected runtime layout.
TEE_ERROR_BAD_PARAMETERS During Invoke Command
For ParametersAny<'_>, this usually means a branch called the wrong accessor:
for example as_memref_input() when the runtime slot is an output memref.
For a concrete tuple, the macro validates all four slots before entering the function. Check the complete tuple, including the unused slots.
Output Size Is Wrong for the Client Application
Prefer set_output/write_at because they copy and update the size in one
step. If set_updated_size is used, ensure it is called with the exact number
of bytes produced.
Input/Output Direction Is Ambiguous
Choose the wrapper from the TA's perspective:
| Data flow | TA wrapper |
|---|---|
| Client application writes, TA reads | ParameterMemrefInput<'_> |
| TA writes, client application reads | ParameterMemrefOutput<'_> |
| Both read and write | ParameterMemrefInout<'_> |
Legacy Compatibility
The deprecated API is still available as optee_utee::deprecated, and it
implements FromRawParameters<'_> for compatibility with the entry-point
macros. New and migrated code should use the typed wrappers instead.
Keeping the deprecated API in new code should be limited to transitional patches, because it keeps the unsafe raw-pointer access pattern and does not express parameter direction in the function signature.