ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
scratch.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 scratch.cpp
6 * @brief Plaintext pool accessor - implementation.
7 *
8 * This file is an access layer, not a second allocator. The pool mechanism is ::pc_arena
9 * (mmgr/arena) and there is exactly one of it; this module owns one instance per worker slot over
10 * compile-time-sized storage and decides who may reach it. The secret pool is the same mechanism
11 * instantiated again behind its own accessor - identical resource, identical mechanics, different
12 * access and control.
13 *
14 * Each instance has exactly one accessor (its worker), so the lock-free guarantees in scratch.h hold
15 * per slot. With PC_WORKER_COUNT == 1 this is byte-for-byte the original single arena.
16 */
17
18#include "scratch.h"
19#include "board_drivers/board_profiles/pc_platform.h" // pc_platform_context_id()
21#include "server/mmgr/arena.h"
22#include <assert.h>
23#include <stdint.h>
24
25namespace
26{
27// Per-slot pool instances (offsets + high-water live inside pc_arena), owned by one instance with
28// internal linkage.
29struct PlainPoolCtx
30{
32};
33PlainPoolCtx s_plain;
34
35// The backing storage, in its OWN owned instance so it is a distinct linker symbol.
36//
37// Nothing outside bind() below names this symbol, and bind() is reachable only from the allocation
38// path. A firmware that never allocates plaintext scratch (a plain TLS/HTTP server - no SSH /
39// WebSocket / OIDC) therefore has the allocator garbage-collected and, with it, this storage,
40// reclaiming PC_SCRATCH_SLOTS * PC_SCRATCH_ARENA_SIZE bytes of DRAM. That is why scratch_reset() -
41// which runs every dispatch and so is always live - must NOT bind: --gc-sections is per-symbol, and
42// one always-live reference would anchor the multi-KB storage into builds that never touch it.
43struct PlainPoolStorageCtx
44{
45 // The pool aligns allocation offsets, so the base must itself satisfy the strictest alignment a
46 // caller can request. As a struct member it would only inherit 8-byte alignment; force 32.
47 alignas(32) uint8_t mem[PC_SCRATCH_SLOTS][PC_SCRATCH_ARENA_SIZE];
48};
49PlainPoolStorageCtx s_plain_storage;
50
51// Byte offset of @p p within the whole plaintext block. The slot count is compile time, so the block
52// is ONE region of known extent and the test needs no loop and no per-slot compare: a single
53// unsigned subtract. A pointer below the base wraps to a huge value and so fails the same bound as
54// one past the end, which is why nullptr needs no special case.
55//
56// Deliberately NOT keyed on a power-of-two slot size. The board profiles tune this per die and most
57// are not powers of two (s3 12288, c6 10240), so a masking form would either reject those builds or
58// round them up and waste kilobytes on the smallest parts. Subtract-and-compare works for any size.
59inline uintptr_t plain_offset(const void *p)
60{
61 return (uintptr_t)p - (uintptr_t)s_plain_storage.mem;
62}
63
64// Resolve the calling worker, clamped into range so a stray id can never index out of bounds (an
65// unbound caller is worker 0, the only worker by default).
66inline int cur_worker()
67{
68 int w = pc_worker_self();
69 return (w >= 0 && w < PC_SCRATCH_SLOTS) ? w : 0;
70}
71
72// Debug tripwire: each pool instance must only ever be touched from one execution context (its
73// worker). Record the first caller per slot and assert every later call matches. The structural
74// single-accessor-per-slot invariant is what makes this lock-free; the assert just turns a future
75// violation into an immediate failure instead of a silent cross-core race.
76inline void assert_single_owner(int w)
77{
78#if PC_DEBUG_CHECKS
79 // ~52 cycles per call on the S3, three per mark+alloc+release. Off by default and turned on
80 // deliberately; see PC_DEBUG_CHECKS. The identity comes from board_drivers/ - the core does not
81 // name an RTOS.
82 static uintptr_t s_owner[PC_SCRATCH_SLOTS] = {0};
83 const uintptr_t cur = pc_platform_context_id();
84 if (s_owner[w] == 0)
85 {
86 s_owner[w] = cur;
87 }
88 else
89 {
90 assert(s_owner[w] == cur && "plaintext pool borrowed from a foreign task");
91 }
92#else
93 (void)w;
94#endif
95}
96
97// Bind slot @p w's pool to its storage on first use. The ONLY reference to s_plain_storage - see
98// the note on PlainPoolStorageCtx for why it must stay confined to the allocation path.
99inline pc_arena *bind(int w)
100{
101 pc_arena *a = &s_plain.pool[w];
102 if (a->base == nullptr)
103 {
104 pc_arena_init(a, s_plain_storage.mem[w], PC_SCRATCH_ARENA_SIZE);
105 }
106 return a;
107}
108
109// The pool for slot @p w WITHOUT binding it: for the observers and the reset, which must not anchor
110// the storage. An unbound slot has never allocated, so it is empty by definition.
111inline pc_arena *peek(int w)
112{
113 pc_arena *a = &s_plain.pool[w];
114 return (a->base != nullptr) ? a : nullptr;
115}
116} // namespace
117
118ScratchBorrow::ScratchBorrow(size_t n, size_t align)
119{
120 // One boundary crossing for mark + alloc, and bind() resolved once for both.
121 int w = cur_worker();
122 assert_single_owner(w);
123 pc_arena *a = bind(w);
124 m_mark = pc_arena_scratch_mark(a);
125 m_span = pc_span_from((uint8_t *)pc_arena_scratch_alloc_aligned(a, n, align), n);
126}
127
129{
130 int w = cur_worker();
131 assert_single_owner(w);
132 pc_arena_scratch_release(bind(w), m_mark);
133}
134
135void *scratch_alloc(size_t n, size_t align)
136{
137 int w = cur_worker();
138 assert_single_owner(w);
139 // The false half is a caller-contract violation (align documented as a power of two) and aborts
140 // the process, so it cannot be exercised from an in-process host test without killing the whole
141 // test binary mid-run.
142 assert((align & (align - 1)) == 0 && "scratch alignment must be a power of two"); // GCOVR_EXCL_BR_LINE
143 return pc_arena_scratch_alloc_aligned(bind(w), n, align);
144}
145
146pc_span pc_scratch_span(size_t n, size_t align)
147{
148 // One argument sets both fields, so the capacity can never disagree with the allocation. On
149 // exhaustion pc_span_from() normalizes to {nullptr, 0} rather than a null with a live capacity,
150 // which is what makes an omitted pc_span_ok() check drop bytes instead of dereference null.
151 return pc_span_from((uint8_t *)scratch_alloc(n, align), n);
152}
153
155{
156 int w = cur_worker();
157 assert_single_owner(w);
158 pc_arena *a = peek(w); // must not bind: this runs every dispatch and would anchor the storage
159 if (a != nullptr)
160 {
162 }
163}
164
165size_t scratch_mark(void)
166{
167 int w = cur_worker();
168 assert_single_owner(w);
169 return pc_arena_scratch_mark(bind(w));
170}
171
172void scratch_release(size_t mark)
173{
174 int w = cur_worker();
175 assert_single_owner(w);
176 pc_arena_scratch_release(bind(w), mark);
177}
178
179size_t scratch_used(void)
180{
181 const pc_arena *a = peek(cur_worker());
182 return (a != nullptr) ? pc_arena_scratch_used(a) : 0;
183}
184
186{
187 // Peak any single slot reached - the value to size PC_SCRATCH_ARENA_SIZE by. This is the
188 // backward direction of the run-length constant: the pool reports what was actually needed.
189 size_t peak = 0;
190 for (int w = 0; w < PC_SCRATCH_SLOTS; w++)
191 {
192 const pc_arena *a = peek(w);
193 if (a != nullptr && a->scratch_hw > peak)
194 {
195 peak = a->scratch_hw;
196 }
197 }
198 return peak;
199}
200
202{
204}
205
206bool pc_scratch_owns(const void *p)
207{
208 return plain_offset(p) < (uintptr_t)sizeof(s_plain_storage.mem);
209}
210
211int pc_scratch_slot_of(const void *p)
212{
213 const uintptr_t off = plain_offset(p);
214 if (off >= (uintptr_t)sizeof(s_plain_storage.mem))
215 {
216 return -1;
217 }
218 // A divide by a compile-time constant, which the compiler emits as a multiply-and-shift. Not a
219 // hot path either way: this exists so a borrow handed back can be asserted to belong to the
220 // calling worker.
221 return (int)(off / PC_SCRATCH_ARENA_SIZE);
222}
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).
void pc_arena_scratch_reset(pc_arena *a)
Free ALL scratch allocations in O(1).
Definition arena.h:162
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
#define PC_SCRATCH_ARENA_SIZE
Definition c2_defaults.h:53
ScratchBorrow(size_t n, size_t align)
Definition scratch.cpp:118
The one vendor/die selector for the whole library.
uintptr_t pc_platform_context_id(void)
#define PC_SCRATCH_SLOTS
size_t scratch_mark(void)
Capture the current arena offset (a savepoint for scratch_release()).
Definition scratch.cpp:165
pc_span pc_scratch_span(size_t n, size_t align)
Borrow n bytes as a span whose capacity is bound to the allocation.
Definition scratch.cpp:146
bool pc_scratch_owns(const void *p)
True if p points inside the plaintext pool.
Definition scratch.cpp:206
size_t scratch_capacity(void)
Total arena capacity in bytes (PC_SCRATCH_ARENA_SIZE).
Definition scratch.cpp:201
void * scratch_alloc(size_t n, size_t align)
Borrow n bytes of scratch, aligned to align.
Definition scratch.cpp:135
int pc_scratch_slot_of(const void *p)
Which plaintext slot owns p, or -1 if p is not in the plaintext pool.
Definition scratch.cpp:211
size_t scratch_used(void)
Bytes currently handed out (0 immediately after a reset).
Definition scratch.cpp:179
size_t scratch_high_water(void)
Largest scratch_used() value seen since boot (for sizing the arena).
Definition scratch.cpp:185
void scratch_reset(void)
Reclaim the whole arena (empties it).
Definition scratch.cpp:154
void scratch_release(size_t mark)
Reclaim everything allocated since mark (LIFO).
Definition scratch.cpp:172
Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
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
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.