ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
secure.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file secure.cpp
6 * @brief Secure pool accessor - implementation.
7 *
8 * An access layer, not a second allocator: the pool mechanism is ::pc_arena and there is exactly one
9 * of it. This module owns a second set of instances over its own compile-time-sized storage, and
10 * adds the one control the plaintext side does not have - reclaiming wipes.
11 */
12
13#include "secure.h"
14#include "board_drivers/board_profiles/pc_platform.h" // pc_platform_context_id()
16#include "server/mmgr/arena.h"
17#include <assert.h>
18#include <stdint.h>
19
20namespace
21{
22// Per-slot pool instances, owned by one instance with internal linkage.
23struct SecurePoolCtx
24{
26};
27SecurePoolCtx s_secure;
28
29// Backing storage in its OWN linker symbol, named only from bind() below and therefore only from the
30// allocation path. A firmware that never borrows secure storage has the allocator garbage-collected and
31// this storage with it. pc_secure_reset() must NOT bind, for the same reason scratch_reset() must
32// not: --gc-sections is per-symbol and one always-live reference would anchor the whole block.
33struct SecurePoolStorageCtx
34{
35 alignas(32) uint8_t mem[PC_SCRATCH_SLOTS][PC_SECURE_ARENA_SIZE];
36};
37SecurePoolStorageCtx s_secure_storage;
38
39// Byte offset of @p p within the whole secure block. The slot count and slot size are compile-time,
40// so the pool is ONE region of known extent: no loop, no per-slot compare, just an unsigned
41// subtract. A pointer below the base wraps to a huge value and fails the same bound as one past the
42// end, so nullptr needs no special case and an overrun cannot read as still-inside.
43//
44// No power-of-two requirement, matching the plaintext pool: the per-die profiles size these to what
45// each part can afford (s3 12288, c6 10240), and subtract-and-compare does not care.
46inline uintptr_t secure_offset(const void *p)
47{
48 return (uintptr_t)p - (uintptr_t)s_secure_storage.mem;
49}
50
51inline int cur_worker()
52{
53 int w = pc_worker_self();
54 return (w >= 0 && w < PC_SCRATCH_SLOTS) ? w : 0;
55}
56
57// Debug tripwire: one execution context per slot, as on the plaintext side.
58inline void assert_single_owner(int w)
59{
60#if PC_DEBUG_CHECKS
61 // ~52 cycles per call on the S3, three per mark+alloc+release. Off by default and turned on
62 // deliberately; see PC_DEBUG_CHECKS. The identity comes from board_drivers/ - the core does not
63 // name an RTOS.
64 static uintptr_t s_owner[PC_SCRATCH_SLOTS] = {0};
65 const uintptr_t cur = pc_platform_context_id();
66 if (s_owner[w] == 0)
67 {
68 s_owner[w] = cur;
69 }
70 else
71 {
72 assert(s_owner[w] == cur && "secure pool borrowed from a foreign task");
73 }
74#else
75 (void)w;
76#endif
77}
78
79// Bind slot @p w to its storage on first use. The ONLY reference to s_secure_storage.
80inline pc_arena *bind(int w)
81{
82 pc_arena *a = &s_secure.pool[w];
83 if (a->base == nullptr)
84 {
85 pc_arena_init(a, s_secure_storage.mem[w], PC_SECURE_ARENA_SIZE);
86 }
87 return a;
88}
89
90// The slot WITHOUT binding it, for observers and the reset. An unbound slot has never allocated, so
91// it holds nothing to wipe.
92inline pc_arena *peek(int w)
93{
94 pc_arena *a = &s_secure.pool[w];
95 return (a->base != nullptr) ? a : nullptr;
96}
97
98// Wipe the live extent down to @p mark, then move the position.
99//
100// The order is the whole point. The scratch end grows DOWN, so the live bytes are
101// [scratch_top, mark) and reclaiming means raising scratch_top back to mark. Wiping first means the
102// region is already zero at the instant it becomes available, so there is no window - not to a
103// preempting handler, not to the very next borrow - in which memory still holding the previous
104// tenant's key material can be handed out. Reclaiming first and wiping after would leave exactly
105// that window.
106inline void wipe_down_to(pc_arena *a, size_t mark)
107{
108 const size_t top = pc_arena_scratch_mark(a); // current position (an offset from the base)
109 if (mark > top && mark <= a->size)
110 {
111 pc_secure_wipe(a->base + top, mark - top); // volatile: the compiler may not elide it
112 }
114}
115} // namespace
116
117SecureBorrow::SecureBorrow(size_t n, size_t align)
118{
119 // One boundary crossing for mark + alloc, and bind() resolved once for both.
120 int w = cur_worker();
121 assert_single_owner(w);
122 pc_arena *a = bind(w);
123 m_mark = pc_arena_scratch_mark(a);
124 m_span = pc_span_from((uint8_t *)pc_arena_scratch_alloc_aligned(a, n, align), n);
125}
126
128{
129 int w = cur_worker();
130 assert_single_owner(w);
131 wipe_down_to(bind(w), m_mark); // wipe first, then move the position
132}
133
134void *pc_secure_alloc(size_t n, size_t align)
135{
136 int w = cur_worker();
137 assert_single_owner(w);
138 assert((align & (align - 1)) == 0 && "secure alignment must be a power of two"); // GCOVR_EXCL_BR_LINE
139 return pc_arena_scratch_alloc_aligned(bind(w), n, align);
140}
141
142pc_span pc_secure_span(size_t n, size_t align)
143{
144 return pc_span_from((uint8_t *)pc_secure_alloc(n, align), n);
145}
146
147size_t pc_secure_mark(void)
148{
149 int w = cur_worker();
150 assert_single_owner(w);
151 return pc_arena_scratch_mark(bind(w));
152}
153
154void pc_secure_release(size_t mark)
155{
156 int w = cur_worker();
157 assert_single_owner(w);
158 wipe_down_to(bind(w), mark);
159}
160
162{
163 int w = cur_worker();
164 assert_single_owner(w);
165 pc_arena *a = peek(w); // must not bind: would anchor the storage into builds that never borrow
166 if (a != nullptr)
167 {
168 wipe_down_to(a, a->size); // the empty position: wipes everything live
169 }
170}
171
172size_t pc_secure_used(void)
173{
174 const pc_arena *a = peek(cur_worker());
175 return (a != nullptr) ? pc_arena_scratch_used(a) : 0;
176}
177
179{
180 size_t peak = 0;
181 for (int w = 0; w < PC_SCRATCH_SLOTS; w++)
182 {
183 const pc_arena *a = peek(w);
184 if (a != nullptr && a->scratch_hw > peak)
185 {
186 peak = a->scratch_hw;
187 }
188 }
189 return peak;
190}
191
193{
195}
196
197bool pc_secure_owns(const void *p)
198{
199 return secure_offset(p) < (uintptr_t)sizeof(s_secure_storage.mem);
200}
201
202int pc_secure_slot_of(const void *p)
203{
204 const uintptr_t off = secure_offset(p);
205 if (off >= (uintptr_t)sizeof(s_secure_storage.mem))
206 {
207 return -1;
208 }
209 // A divide by a compile-time constant, which the compiler emits as a multiply-and-shift.
210 return (int)(off / PC_SECURE_ARENA_SIZE);
211}
void pc_arena_init(pc_arena *a, void *base, size_t size)
Initialize a over the region [base, base+size).
Definition arena.cpp:29
Unified double-ended server arena (Phase 1: core allocator, one region).
size_t pc_arena_scratch_mark(const pc_arena *a)
Capture the current scratch position (a savepoint for pc_arena_scratch_release()).
Definition arena.h:146
void pc_arena_scratch_release(pc_arena *a, size_t mark)
Free every scratch allocation made since mark (a value from pc_arena_scratch_mark()).
Definition arena.h:152
void * pc_arena_scratch_alloc_aligned(pc_arena *a, size_t n, size_t align)
Bump-allocate n bytes of transient storage, aligned to align.
Definition arena.h:109
size_t pc_arena_scratch_used(const pc_arena *a)
Scratch bytes currently allocated.
Definition arena.h:198
SecureBorrow(size_t n, size_t align)
Definition secure.cpp:117
The one vendor/die selector for the whole library.
uintptr_t pc_platform_context_id(void)
#define PC_SECURE_ARENA_SIZE
#define PC_SCRATCH_SLOTS
size_t pc_secure_used(void)
Bytes currently handed out.
Definition secure.cpp:172
int pc_secure_slot_of(const void *p)
Which secure slot owns p, or -1 if p is not in the secure pool.
Definition secure.cpp:202
void pc_secure_reset(void)
Wipe and reclaim the whole slot.
Definition secure.cpp:161
size_t pc_secure_mark(void)
Capture the current position, to be handed to pc_secure_release().
Definition secure.cpp:147
bool pc_secure_owns(const void *p)
True if p points inside the secure pool.
Definition secure.cpp:197
pc_span pc_secure_span(size_t n, size_t align)
Borrow n secure bytes as a span whose capacity is bound to the allocation.
Definition secure.cpp:142
void pc_secure_release(size_t mark)
Wipe and reclaim everything borrowed since mark.
Definition secure.cpp:154
void * pc_secure_alloc(size_t n, size_t align)
Borrow n bytes of secure storage, aligned to align.
Definition secure.cpp:134
size_t pc_secure_high_water(void)
Peak bytes ever handed out, for sizing PC_SECURE_ARENA_SIZE.
Definition secure.cpp:178
size_t pc_secure_capacity(void)
Total per-slot capacity in bytes (PC_SECURE_ARENA_SIZE).
Definition secure.cpp:192
Secure pool accessor - borrows that hold key material.
pc_span pc_span_from(uint8_t *p, size_t cap)
Bind a span to memory whose extent is only known at run time.
Definition span.h:92
Double-ended arena over one region [base, base+size).
Definition arena.h:57
uint8_t * base
Region start.
Definition arena.h:58
size_t scratch_hw
High-water of scratch use (size - min scratch_top).
Definition arena.h:64
size_t size
Region length in bytes.
Definition arena.h:59
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65
int pc_worker_self(void)
Worker id [0, count) of the calling task; 0 by default / single-worker.
Definition worker.cpp:35
Layer 5 (Session) - server worker identity.