Skip to main content

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}