offline_protocol_leaf/shared.rs
1//! The store handle, and the one place that decides what it is.
2//!
3//! # One handle type per target, named in one file
4//!
5//! A [`LeafDevice`](crate::LeafDevice) and the adapters it builds share one
6//! store, so the handle is reference counted. Where the processor has atomic
7//! compare-and-swap that is `alloc::sync::Arc`. Where it has none (ESP32-C3,
8//! ESP32-C2, every Cortex-M0) `alloc::sync` does not exist, and the handle is
9//! `portable_atomic_util::Arc` instead, the same choice the pinned mls-rs makes
10//! for its own handles.
11//!
12//! The failure this file prevents is a build that works on every machine a
13//! developer owns and nowhere a device runs: a host always has atomics, so an
14//! `alloc::sync` import anywhere else in this crate compiles, passes every
15//! test, and breaks only the bare-metal targets. `only_one_file_names_the_counted_pointer`
16//! in this module refuses, on the host, any path or import through
17//! `alloc::sync` outside this file (relative to the crate root or not), any
18//! glob over `alloc`, and any rename of `alloc`. The `embedded-core` CI job
19//! refuses a direct import on the targets.
20//!
21//! The scan reads one file's tokens and cannot see three routes: an import
22//! through a module of this crate that re-exports `alloc`, a glob over such a
23//! module, and a path a macro assembles. None of them is written in this
24//! crate, and a new one would be, visibly, a module that re-exports `alloc`
25//! or a macro that pastes a path together.
26//!
27//! # Building a handle
28//!
29//! [`shared_store`] is the portable way. The replacement pointer cannot turn
30//! an `Arc<MyStore>` into an `Arc<dyn LeafStore>` on stable Rust, so the
31//! constructor goes through a `Box<dyn LeafStore>`, which costs one extra
32//! allocation and one copy of the store value, once, at boot. It takes that
33//! route on every target, so the host tests run the constructor a device runs.
34//! They do not run the replacement pointer: a host always has atomics, so its
35//! tests always get `alloc::sync::Arc`, and `portable_atomic_util::Arc` is
36//! compiled on the targets without compare-and-swap and exercised nowhere.
37//!
38//! On a target with atomics [`SharedStore`] is `alloc::sync::Arc<dyn LeafStore>`,
39//! which is `std::sync::Arc` wherever `std` exists, so code that builds one with
40//! `Arc::new` keeps compiling there. On a target without, only [`shared_store`]
41//! builds one.
42//!
43//! # What a target without compare-and-swap owes
44//!
45//! A critical-section implementation, linked by the firmware. The reference
46//! count is only as sound as it is: an implementation that does not actually
47//! exclude interrupts or other cores makes the count racy, and a racy count is
48//! a use-after-free of the store.
49
50use alloc::boxed::Box;
51
52#[cfg(target_has_atomic = "ptr")]
53pub(crate) use alloc::sync::Arc;
54#[cfg(not(target_has_atomic = "ptr"))]
55pub(crate) use portable_atomic_util::Arc;
56
57use crate::store::LeafStore;
58
59/// A shared handle to the device's store.
60///
61/// On a target with atomic compare-and-swap this is `alloc::sync::Arc`, the
62/// same type as `std::sync::Arc`. On one without, it is
63/// `portable_atomic_util::Arc`. Build one with [`shared_store`] to write code
64/// that compiles on both.
65pub type SharedStore = Arc<dyn LeafStore>;
66
67/// Wraps a store in the handle a [`LeafDevice`](crate::LeafDevice) takes.
68///
69/// ```
70/// use offline_protocol_leaf::{shared_store, LeafDevice, MemoryStore};
71///
72/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
73/// let device = LeafDevice::open(shared_store(MemoryStore::new()), "com.example.lock")?;
74/// # let _ = device;
75/// # Ok(())
76/// # }
77/// ```
78pub fn shared_store<S: LeafStore + 'static>(store: S) -> SharedStore {
79 let boxed: Box<dyn LeafStore> = Box::new(store);
80 Arc::from(boxed)
81}
82
83#[cfg(all(test, feature = "std"))]
84mod tests {
85 use super::*;
86 use crate::store::{MemoryStore, KEY_TYPE_IDENTITY};
87 use crate::LeafDevice;
88 use std::string::String;
89 use std::vec::Vec;
90 use std::{format, fs, path::PathBuf};
91
92 #[test]
93 fn a_store_built_by_the_constructor_is_the_one_the_device_writes() {
94 let store = shared_store(MemoryStore::new());
95 let reader = Arc::clone(&store);
96 let _device = LeafDevice::open(store, "com.example.lock").expect("device opens");
97
98 let identity = reader
99 .load(KEY_TYPE_IDENTITY, "signature_public")
100 .expect("load succeeds");
101 assert!(
102 identity.is_some(),
103 "provisioning must write through the handle the constructor built"
104 );
105 }
106
107 /// Splits Rust source into tokens, dropping whitespace, comments, string,
108 /// byte-string, raw-string and character literals, and lifetimes, so that
109 /// a path mentioned in prose or in a literal is never read as code. `::`
110 /// is one token; every other punctuation character is its own.
111 fn tokens(src: &str) -> Vec<String> {
112 let chars: Vec<char> = src.chars().collect();
113 let ident = |c: char| c == '_' || c.is_alphanumeric();
114 let mut out = Vec::new();
115 let mut i = 0;
116 while i < chars.len() {
117 let c = chars[i];
118 let next = chars.get(i + 1).copied();
119 if c.is_whitespace() {
120 i += 1;
121 } else if c == '/' && next == Some('/') {
122 while i < chars.len() && chars[i] != '\n' {
123 i += 1;
124 }
125 } else if c == '/' && next == Some('*') {
126 let mut depth = 0usize;
127 while i < chars.len() {
128 if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
129 depth += 1;
130 i += 2;
131 } else if chars[i] == '*' && chars.get(i + 1) == Some(&'/') {
132 depth -= 1;
133 i += 2;
134 if depth == 0 {
135 break;
136 }
137 } else {
138 i += 1;
139 }
140 }
141 } else if c == '"' {
142 i += 1;
143 while i < chars.len() && chars[i] != '"' {
144 i += if chars[i] == '\\' { 2 } else { 1 };
145 }
146 i += 1;
147 } else if c == '\'' {
148 if next == Some('\\') {
149 // Skip the quote, the backslash and the escaped character,
150 // which may itself be a quote: `'\''`.
151 i += 3;
152 while i < chars.len() && chars[i] != '\'' {
153 i += 1;
154 }
155 i += 1;
156 } else if chars.get(i + 2) == Some(&'\'') {
157 i += 3;
158 } else {
159 // A lifetime or a label: skip the quote and its name.
160 i += 1;
161 while i < chars.len() && ident(chars[i]) {
162 i += 1;
163 }
164 }
165 } else if ident(c) {
166 let begin = i;
167 while i < chars.len() && ident(chars[i]) {
168 i += 1;
169 }
170 let word: String = chars[begin..i].iter().collect();
171 let raw_prefix = word == "r" || word == "br" || word == "cr";
172 let hashes = chars[i..].iter().take_while(|&&h| h == '#').count();
173 if raw_prefix && chars.get(i + hashes) == Some(&'"') {
174 // A raw string: skip to the quote followed by as many hashes.
175 i += hashes + 1;
176 while i < chars.len() {
177 if chars[i] == '"'
178 && chars[i + 1..].iter().take_while(|&&h| h == '#').count() >= hashes
179 {
180 i += 1 + hashes;
181 break;
182 }
183 i += 1;
184 }
185 } else if word == "b" && matches!(chars.get(i), Some('"') | Some('\'')) {
186 // A byte string or byte literal: the next turn skips it.
187 } else if word == "r" && hashes == 1 && chars.get(i + 1).is_some_and(|&n| ident(n))
188 {
189 // A raw identifier, `r#sync`: the next turn reads the name.
190 i += 1;
191 } else {
192 out.push(word);
193 }
194 } else if c == ':' && next == Some(':') {
195 out.push("::".into());
196 i += 2;
197 } else {
198 out.push(c.to_string());
199 i += 1;
200 }
201 }
202 out
203 }
204
205 /// Flattens one `use` tree starting at `tokens[*at]` into full paths, each
206 /// with the name it is imported under when that is renamed. `self` inside
207 /// a group names the group's own path.
208 fn use_tree(
209 tokens: &[String],
210 at: &mut usize,
211 prefix: Vec<String>,
212 out: &mut Vec<(Vec<String>, Option<String>)>,
213 ) {
214 let mut path = prefix;
215 while let Some(token) = tokens.get(*at) {
216 match token.as_str() {
217 "::" => *at += 1,
218 "{" => {
219 *at += 1;
220 while let Some(inner) = tokens.get(*at) {
221 match inner.as_str() {
222 "}" => {
223 *at += 1;
224 break;
225 }
226 "," => *at += 1,
227 ";" => return,
228 _ => {
229 let before = *at;
230 use_tree(tokens, at, path.clone(), out);
231 if *at == before {
232 *at += 1;
233 }
234 }
235 }
236 }
237 return;
238 }
239 "*" => {
240 *at += 1;
241 path.push("*".into());
242 out.push((path, None));
243 return;
244 }
245 word if word.starts_with(|c: char| c == '_' || c.is_alphabetic()) => {
246 *at += 1;
247 if word != "self" {
248 path.push(word.into());
249 }
250 if tokens.get(*at).is_some_and(|t| t == "::") {
251 continue;
252 }
253 let rename = (tokens.get(*at).is_some_and(|t| t == "as"))
254 .then(|| tokens.get(*at + 1).cloned())
255 .flatten();
256 if rename.is_some() {
257 *at += 2;
258 }
259 out.push((path, rename));
260 return;
261 }
262 _ => return,
263 }
264 }
265 }
266
267 /// Every place in `src` that reaches the `alloc` crate's `sync` module, or
268 /// could: a path through it in code or in an import, a glob over the
269 /// `alloc` root (which imports `sync` by name), and the same through any
270 /// name `alloc` is renamed to. `std::sync` and `core::sync` are other
271 /// modules and are never reported, and neither is a mention in a comment
272 /// or a literal.
273 ///
274 /// This matches the class rather than a list of line shapes. The first
275 /// version of this guard matched shapes, and each round of review found one
276 /// it missed (`sync::Arc}`, `sync as s`, `sync::*`).
277 fn counted_pointer_paths(src: &str) -> Vec<String> {
278 let tokens = tokens(src);
279 let mut imports = Vec::new();
280 let mut aliases = vec![String::from("alloc")];
281 for (index, token) in tokens.iter().enumerate() {
282 if token == "use" {
283 use_tree(&tokens, &mut (index + 1), Vec::new(), &mut imports);
284 }
285 let rest = &tokens[index..];
286 if rest.len() >= 5
287 && rest[0] == "extern"
288 && rest[1] == "crate"
289 && rest[2] == "alloc"
290 && rest[3] == "as"
291 {
292 aliases.push(rest[4].clone());
293 }
294 }
295 // `crate::alloc`, `self::alloc` and `super::alloc` reach the same crate
296 // through the root's `extern crate alloc;`, so a relative prefix is
297 // dropped before the first segment is compared.
298 for (path, _) in imports.iter_mut() {
299 let relative = path
300 .iter()
301 .take_while(|segment| matches!(segment.as_str(), "crate" | "self" | "super"))
302 .count();
303 path.drain(..relative);
304 }
305 // `use alloc as heap;` and `use alloc::{self as heap};` rename it too,
306 // and a rename of a rename counts, so loop until nothing new appears.
307 loop {
308 let found: Vec<String> = imports
309 .iter()
310 .filter(|(path, rename)| {
311 path.len() == 1 && aliases.contains(&path[0]) && rename.is_some()
312 })
313 .filter_map(|(_, rename)| rename.clone())
314 .filter(|name| !aliases.contains(name))
315 .collect();
316 if found.is_empty() {
317 break;
318 }
319 aliases.extend(found);
320 }
321 let through_sync = |root: &str, second: &str| {
322 aliases.iter().any(|alias| alias == root) && (second == "sync" || second == "*")
323 };
324
325 let mut offenders: Vec<String> = imports
326 .iter()
327 .filter(|(path, _)| path.len() >= 2 && through_sync(&path[0], &path[1]))
328 .map(|(path, _)| format!("use {}", path.join("::")))
329 .collect();
330 // A rename of `alloc` is itself reported: made in one file it is
331 // visible in every other (through the extern prelude or a crate-level
332 // `use`), and this scan reads one file at a time, so it cannot follow
333 // the new name across files. `as _` names nothing and is fine.
334 offenders.extend(
335 aliases
336 .iter()
337 .skip(1)
338 .filter(|alias| alias.as_str() != "_")
339 .map(|alias| format!("alloc renamed to {alias}")),
340 );
341 offenders.extend(
342 tokens
343 .windows(3)
344 .filter(|w| w[1] == "::" && w[2] == "sync" && through_sync(&w[0], &w[2]))
345 .map(|w| format!("{}::sync", w[0])),
346 );
347 offenders
348 }
349
350 fn rust_files(dir: &std::path::Path, found: &mut Vec<PathBuf>) {
351 let entries = fs::read_dir(dir)
352 .unwrap_or_else(|error| panic!("{} is unreadable: {error}", dir.display()));
353 for entry in entries {
354 let path = entry
355 .unwrap_or_else(|error| {
356 panic!("an entry in {} is unreadable: {error}", dir.display())
357 })
358 .path();
359 if path.is_dir() {
360 rust_files(&path, found);
361 } else if path.extension().is_some_and(|ext| ext == "rs") {
362 found.push(path);
363 }
364 }
365 }
366
367 #[test]
368 fn the_guard_sees_every_route_to_the_counted_pointer() {
369 for code in [
370 "use alloc::sync::Arc;",
371 "use alloc::sync;",
372 "use ::alloc::sync::Weak;",
373 "use alloc::{string::String, sync::Arc};",
374 "use alloc::{sync::{Arc, Weak}, vec::Vec};",
375 "use alloc::{string::String, sync as s};",
376 "use alloc::{string::String, sync::*};",
377 "use alloc::*;",
378 "use alloc::{\n string::String,\n sync::Arc,\n};",
379 "pub(crate) use alloc::sync::Arc;",
380 "let store: alloc::sync::Arc<dyn LeafStore>;",
381 "extern crate alloc as heap;\nuse heap::sync::Arc;",
382 "use alloc as heap;\nfn f(_: heap::sync::Arc<u8>) {}",
383 "use alloc::{self as heap};\nuse heap::sync::Arc;",
384 "let quote = '\"'; use alloc::sync::Arc;",
385 "fn f<'a>(x: &'a str) -> &'a str { x }\nuse alloc::sync::Arc;",
386 "const S: &[u8] = b\"x\"; use alloc::sync::Arc;",
387 "const R: &str = r#\"a \" b\"#; use alloc::sync::Arc;",
388 "const C: &CStr = cr#\"a \" b\"#; use alloc::sync::Arc;",
389 "f('\\'','\"'); use alloc::sync::Arc; g('\"');",
390 "use crate::alloc::{string::String, sync::Arc};",
391 "use super::alloc::{sync::Arc};",
392 "use crate::alloc::*;",
393 "let store: crate::alloc::sync::Arc<dyn LeafStore>;",
394 "use crate::alloc as heap;",
395 "extern crate alloc as heap;",
396 "pub(crate) use alloc as mem;",
397 // A raw identifier is the same name.
398 "use alloc::r#sync::Arc;",
399 // A raw byte string ends at its quote and hashes, not the first quote.
400 "const B: &[u8] = br#\"a \" b\"#; use alloc::sync::Arc;",
401 ] {
402 assert!(!counted_pointer_paths(code).is_empty(), "missed: {code}");
403 }
404 for code in [
405 "use std::sync::Mutex;",
406 "use std::sync::{Mutex, MutexGuard};",
407 "use std::{sync::Mutex, vec::Vec};",
408 "use core::sync::atomic::{AtomicU32, Ordering};",
409 "use crate::shared::Arc;",
410 "use alloc::{string::String, vec::Vec};",
411 "let n = 1; // alloc::sync is gone here",
412 "/* use alloc::sync::Arc; /* nested */ still a comment */",
413 "const S: &str = \"use alloc::sync::Arc;\";",
414 "const R: &str = r#\"use alloc::sync::Arc;\"#;",
415 "let sync = true; call(a, sync); file.sync_all();",
416 "mod sync { pub struct Arc; } use self::sync::Arc;",
417 "extern crate alloc; extern crate alloc as _;",
418 "let c = '\\''; let d = '\\\\'; let e = b'\\\\';",
419 // An escaped quote does not end a string.
420 "const S: &str = \"\\\" use alloc::sync::Arc; \\\"\";",
421 // A block comment ends at its own close, not the first `*/`.
422 "/* a /* b */ use alloc::sync::Arc; */",
423 ] {
424 assert_eq!(
425 counted_pointer_paths(code),
426 Vec::<String>::new(),
427 "false alarm: {code}"
428 );
429 }
430 }
431
432 #[test]
433 fn only_one_file_names_the_counted_pointer() {
434 let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
435 let exempt = src.join("shared.rs");
436 let mut files = Vec::new();
437 rust_files(&src, &mut files);
438 assert!(
439 files.contains(&exempt),
440 "the guard did not find {}",
441 exempt.display()
442 );
443
444 // An unreadable file fails the test: a guard that skips what it cannot
445 // read reports clean for code it never saw.
446 let offenders: Vec<String> = files
447 .iter()
448 .filter(|path| **path != exempt)
449 .flat_map(|path| {
450 let text = fs::read_to_string(path)
451 .unwrap_or_else(|error| panic!("{} is unreadable: {error}", path.display()));
452 counted_pointer_paths(&text)
453 .into_iter()
454 .map(move |found| format!("{}: {found}", path.display()))
455 })
456 .collect();
457
458 assert!(
459 offenders.is_empty(),
460 "these reach alloc::sync directly, which compiles on every host and \
461 breaks every target without compare-and-swap; take `Arc` from \
462 `crate::shared` instead: {offenders:?}"
463 );
464 }
465}