TEE Rust Macro System — Advanced Type System
Status
This document describes the advanced type system for #[tee_command]. Most features listed below are implemented; some remain planned.
What is already implemented
See tee-sdk-design.md for the baseline. Advanced types now fully implemented:
| Category | Implemented types |
|---|---|
| Input parameters | bool, u32, i32, u64, (u32, u32), &[u8], &mut [u8], &mut u32, &mut i32, &mut u64, T: TeeParam |
| Return / output | u32, u64, bool, Vec<u8>, T: TeeParam |
| Serialization | Pluggable codec via #[tee_command(codec = MyCodec)]; any codec implementing consortium_cfg_common::codec::CodecFor<T> |
What this document covers
i32,(u32, u32)value pairs ✅&mut u32/&mut u64output parameters (inout slots) ✅- Arbitrary serializable types via
TeeParam+ pluggable codec system ✅
Architecture
┌────────────────────────────────────────────────────────────────┐
│ Developer writes one function signature │
│ │
│ #[tee_command(codec = MyCodec)] ← required if using TeeParam │
│ fn process(ctx: &mut Ctx, cfg: MyConfig) -> Result<()> │
└────────────────────┬───────────────────────────────────────────┘
│ macro expands to
│
┌──────────┴───────────┐
│ │
▼ ▼
process_dispatched() call_process() [cfg(feature="ca")]
(TA side) (CA side)
unpack params pack params
→ call process() → invoke_command()
The original function is always emitted untouched, so internal TA-to-TA calls pay zero marshalling overhead.
The codec attribute is required when any parameter or return type implements TeeParam. It specifies which codec to use for serialization/deserialization of those types.
Parameter Classification
The macro classifies each parameter by its Rust type and maps it to a TEE parameter slot. A TEE function has at most 4 parameter slots.
Context Parameter
With #[tee_command(ctx)], the first parameter with type &mut SomePath (where SomePath is not a primitive or slice) is treated as the TA context. Explicit selectors such as ctx = name or ctx = 0 are accepted for compatibility and emit one warning; if the selected context is not the first parameter, the warning asks the user to move it first. The context is forwarded directly to the inner function and never packed into a TEE slot.
Planned Types
Additional Primitive Values (ParamValue slot)
| Rust type | Slot kind | Encoding | Status |
|---|---|---|---|
u32 | ValueInput | a = value, b = 0 | ✅ done |
bool | ValueInput | a = value as u32 (0 or 1), b = 0 | ✅ done |
u64 | ValueInput/Return | a = low 32, b = high 32 | ✅ done |
i32 | ValueInput | a = value as u32, b = 0 | ✅ done |
(u32, u32) | ValueInput | a = .0, b = .1 | ✅ done |
Output Value Parameters (ValueInout slot)
A &mut T where T is a primitive scalar is treated as an output parameter. The TA writes a result back through the pointer; the CA reads it after invoke_command returns.
| Rust type | Slot kind | TA behaviour | CA behaviour | Status |
|---|---|---|---|---|
&mut u32 | ValueInout | reads a, writes back via set_a | reads a into *ptr after call | ✅ done |
&mut i32 | ValueInout | reads a as i32, writes back cast | reads a as i32 into *ptr after call | ✅ done |
&mut u64 | ValueInout | reconstructs from a/b, writes back | reconstructs from a/b after call | ✅ done |
Example
#![allow(unused)]
fn main() {
#[tee_command(ctx)]
fn compute(ctx: &mut Ctx, input: u32, result: &mut u64) -> Result<()> {
*result = (input as u64) * 0xDEAD_BEEF;
Ok(())
}
// CA side (generated):
// #[cfg(feature = "ca")]
// fn call_compute(input: u32, result: &mut u64) -> consortium_tee::TeeResult<()> {
// let _p0 = ParamValue::new(input, 0, ParamType::ValueInput);
// let _p1 = ParamValue::new(0u32, 0u32, ParamType::ValueInout);
// let mut _params = Parameters::new(_p0, _p1, ParamNone, ParamNone);
// invoke_command(Command::Compute, &mut _params)?;
// *result = ((_params.param_1().as_value().b() as u64) << 32)
// | (_params.param_1().as_value().a() as u64);
// Ok(())
// }
}
Serialized Types (TeeParam — ParamMemref slot)
Any type implementing TeeParam can be passed through a memref slot. The macro serializes it using a pluggable codec specified via the #[tee_command(codec = ...)] attribute.
Derive TeeParam on your type to opt in:
#![allow(unused)]
fn main() {
#[derive(TeeParam, Serialize, Deserialize)]
struct MyConfig {
threshold: u32,
flags: u64,
mode: OperationMode,
label: heapless::String<32>,
}
}
The codec type must implement consortium_cfg_common::codec::CodecFor<T> for your serialization format (e.g., PostcardCodec for postcard).
| Rust type | Slot kind | TA behaviour | CA behaviour | Status |
|---|---|---|---|---|
T: TeeParam | MemrefInput | deserialize from buffer with codec | serialize to buffer, pass as memref | ✅ done |
&mut T: TeeParam | MemrefInout | deserialize, pass &mut, re-serialize | serialize, pass as memref, deserialize back | ✅ done |
The TeeParam Trait
#![allow(unused)]
fn main() {
pub trait TeeParam {
/// Maximum serialized size in bytes.
/// Used to pre-size the inout buffer on the CA side.
const MAX_SIZE: usize;
}
}
TeeParam is a codec-agnostic marker trait. Derive it on any type you want to pass through a memref slot. The actual serialization is handled by the codec you specify in #[tee_command(codec = ...)].
MAX_SIZE solves the inout buffer sizing problem: since the TA may return a value larger than what the CA sent, the CA must pre-allocate a buffer sized for the response. Defining it on the type means the size contract lives next to the type definition.
That pre-sizing has a consequence: an in/out buffer arrives at the TA zero-padded to MAX_SIZE, and a Memref’s single size field cannot say how much of it the CA’s encoding filled. Decoding the padding happens to work for postcard, which ignores trailing bytes, but not for codecs that reject them (protobuf treats a zero byte as an invalid tag). A &mut T slot therefore carries a 4-byte little-endian length ahead of the payload in both directions, and its buffer is consortium_tee::inout::capacity(MAX_SIZE) bytes. Input and output Memrefs need no frame: the CA sends exactly the encoded bytes in, and the TA’s set_output reports exactly the bytes written out.
On the TA, MAX_SIZE is also enforced on the way in: a serialized input whose Memref is larger than MAX_SIZE (or a &mut T frame larger than capacity(MAX_SIZE), or one declaring a longer payload) is BadParameters, checked before the TA copies the buffer out of shared memory.
If #[derive(TeeParam)] is used without a manual impl, the macro uses a conservative default of 1024 bytes.
Slot Limit Enforcement
TEE permits at most 4 parameter slots per call (the context never counts as a slot). This is already enforced for current types via compile_error!. The enforcement will extend to cover all planned types.
If you exceed 4 slots, restructure parameters into a serialized struct instead:
#![allow(unused)]
fn main() {
// Too many slots
#[tee_command(ctx)]
fn bad(ctx: &mut Ctx, a: u32, b: u64, c: u32, d: &[u8], e: &mut [u8]) -> Result<()> { ... }
// [0] [1] [2] [3] [4] ← 5 slots, compile error
// Pack logically related scalars into a struct
#[derive(TeeParam, Serialize, Deserialize)]
struct Params { a: u32, b: u64, c: u32 }
#[tee_command(ctx)]
fn good(ctx: &mut Ctx, params: Params, input: &[u8], output: &mut [u8]) -> Result<()> { ... }
// [0] [1] [2] ← 3 slots
}
Type Classification Priority (full order)
When the macro inspects a parameter type, it will test these rules in order:
1. &[u8] → MemrefInput
2. &mut [u8] → MemrefInout
3. &mut u32/i32/u64 → ValueInout output
4. u32 → ValueInput (a)
5. i32 → ValueInput (a, bit-cast)
6. bool → ValueInput (a, 0/1)
7. u64 → ValueInput (a=low, b=high)
8. (u32, u32) → ValueInput (a=.0, b=.1)
9. &mut T (Path) → MemrefInout (TeeParam, codec round-trip)
10. T (Path) → MemrefInput (TeeParam, codec deserialize)
11. anything else → compile error
All rules are now implemented. Rules 9–10 require #[tee_command(codec = MyCodec)] to be specified.
Codec Selection and Requirements
Pluggable Codec System
The #[tee_command(codec = MyCodec)] attribute lets you choose how TeeParam types are serialized. Your codec type must implement consortium_cfg_common::codec::CodecFor<T> for each TeeParam type you use.
Example: Using PostcardCodec
#![allow(unused)]
fn main() {
use consortium_cfg_common::codec::PostcardCodec;
#[derive(TeeParam, Serialize, Deserialize)]
struct Config {
threshold: u32,
enabled: bool,
}
#[tee_command(codec = PostcardCodec)]
fn process(ctx: &mut Ctx, cfg: Config) -> Result<(), TeeError> {
// ...
}
}
Considerations for Codec Choice
| Property | postcard | bincode | Custom |
|---|---|---|---|
no_std support | ✅ | partial | ✅ |
| Output size | minimal | small | varies |
| Allocation-free mode | ✅ | ❌ | ✅ |
| Schema evolution | manual | manual | custom |
Schema Evolution Warning
Most binary codecs (including postcard and bincode) are not self-describing. If you change a TeeParam type (add/remove/reorder fields), both CA and TA must be recompiled together to stay in sync.
Codec Implementation Reference
To implement a custom codec for your type T:
#![allow(unused)]
fn main() {
impl consortium_cfg_common::codec::CodecFor<MyType> for MyCodec {
type Error = MyError;
type Decoded<'buf> = MyType;
fn encode(msg: &MyType, buf: &mut [u8]) -> Result<usize, Self::Error> {
// Serialize msg into buf, return number of bytes written
}
fn decode(buf: &[u8]) -> Result<Self::Decoded, Self::Error> {
// Deserialize from buf
}
}
}