Skip to main content

hardware_enclave/memory/
memory_enclave.rs

1// Copyright 2026 Jay Gowdy
2// SPDX-License-Identifier: MIT
3
4// aes-gcm's Nonce::from_slice still works but triggers a deprecation on
5// the underlying generic_array usage in some versions.
6#![allow(deprecated)]
7
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use aes_gcm::aead::Aead;
11use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
12use rand::TryRngCore;
13
14use super::pool::{hot_cache_evict, hot_cache_get, hot_cache_insert, pool_acquire, PoolSlot};
15use super::secure_buffer::SecureBuffer;
16use crate::error::{Error, Result};
17
18const NONCE_LEN: usize = 12;
19const TAG_LEN: usize = 16;
20
21/// Generates a fresh 12-byte random nonce for each seal operation.
22///
23/// A full random nonce is used (rather than a counter) to ensure nonce uniqueness
24/// is preserved across `fork()` — a counter-based scheme with a cached prefix would
25/// produce identical nonces in parent and child processes after fork. At the seal
26/// volumes expected for in-process use (thousands per process lifetime, not billions),
27/// the collision probability of random 96-bit nonces is negligible (~2^{-80} after
28/// 2^8 seals).
29fn fresh_nonce() -> Result<[u8; NONCE_LEN]> {
30    let mut nonce = [0_u8; NONCE_LEN];
31    rand::rngs::OsRng
32        .try_fill_bytes(&mut nonce)
33        .map_err(|e| Error::Memory(format!("MemoryEnclave: OsRng nonce failure: {e}")))?;
34    Ok(nonce)
35}
36
37/// An in-memory AES-256-GCM sealed secret.
38///
39/// Plaintext is encrypted under the process-global Coffer master key.
40/// `open()` returns the plaintext in a `PoolSlot` (slab-backed if the
41/// plaintext fits in the smallest tier's slot size, otherwise standalone).
42/// A hot cache in the slab avoids decryption when the same `MemoryEnclave`
43/// is opened multiple times in quick succession.
44///
45/// When dropped, the hot cache entry for this enclave is evicted.
46///
47/// # Security note: hot cache
48/// After the first successful `open()`, the plaintext is cached in the locked slab
49/// until this `MemoryEnclave` is dropped (or until LRU pressure evicts it). The
50/// cached copy lives in a guard-paged, mlock'd slab slot — but it is present for
51/// the lifetime of this value. For secrets that should not persist in memory,
52/// drop the `MemoryEnclave` promptly after use.
53pub struct MemoryEnclave {
54    id: u64,
55    /// [nonce (12 bytes)] [ciphertext + GCM tag]
56    ciphertext: Vec<u8>,
57    plaintext_len: usize,
58}
59
60static SEAL_ID: AtomicU64 = AtomicU64::new(1);
61
62impl std::fmt::Debug for MemoryEnclave {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("MemoryEnclave")
65            .field("id", &self.id)
66            .field("plaintext_len", &self.plaintext_len)
67            .finish()
68    }
69}
70
71impl MemoryEnclave {
72    fn do_seal(plaintext: &[u8]) -> Result<Self> {
73        // Get the master key from the slab-backed coffer.
74        let key_slot = super::pool::global_pool().coffer_view()?;
75        let nonce_bytes = fresh_nonce()?;
76        let nonce = Nonce::from_slice(&nonce_bytes);
77
78        let cipher = Aes256Gcm::new_from_slice(key_slot.as_slice())
79            .map_err(|e| Error::Memory(format!("MemoryEnclave::seal cipher init: {e}")))?;
80
81        let ct = cipher
82            .encrypt(nonce, plaintext)
83            .map_err(|e| Error::Memory(format!("MemoryEnclave::seal encrypt: {e}")))?;
84
85        // Erase the round-key schedule before returning the coffer key slot.
86        // SAFETY note: Aes256Gcm implements ZeroizeOnDrop when compiled with the
87        // "zeroize" cargo feature (enabled in the workspace Cargo.toml). The expanded
88        // AES round-key schedule is therefore zeroed when `cipher` drops.
89        drop(cipher); // erase round-key schedule before returning key slot
90        drop(key_slot); // wipe coffer key from slab slot
91
92        let mut blob = Vec::with_capacity(NONCE_LEN + ct.len());
93        blob.extend_from_slice(&nonce_bytes);
94        blob.extend_from_slice(&ct);
95
96        let id = SEAL_ID.fetch_add(1, Ordering::Relaxed);
97        Ok(Self {
98            id,
99            ciphertext: blob,
100            plaintext_len: plaintext.len(),
101        })
102    }
103
104    /// Seal `plaintext` under the Coffer key.
105    pub fn seal(plaintext: &[u8]) -> Result<Self> {
106        Self::do_seal(plaintext)
107    }
108
109    /// Seal a `SecureBuffer`'s contents (melt → read → re-freeze).
110    pub fn seal_buffer(buf: &mut SecureBuffer) -> Result<Self> {
111        buf.melt()?;
112        let result = Self::do_seal(buf.as_slice());
113        drop(buf.freeze());
114        result
115    }
116
117    /// Seal a `PoolSlot`'s contents.
118    /// The caller is responsible for dropping the slot (which zeroizes it).
119    pub fn seal_slot(slot: &PoolSlot) -> Result<Self> {
120        Self::do_seal(slot.as_slice())
121    }
122
123    /// Decrypt and return the plaintext in a `PoolSlot`.
124    ///
125    /// Hot cache fast path: if this enclave was recently opened, the plaintext
126    /// is copied from the slab cache into a new transient `PoolSlot` without
127    /// AES-GCM decryption.
128    pub fn open(&self) -> Result<PoolSlot> {
129        // Hot cache lookup (slab-backed copy).
130        if let Some(cached) = hot_cache_get(self.id) {
131            return Ok(cached);
132        }
133
134        // Cold path: decrypt.
135        if self.ciphertext.len() < NONCE_LEN + TAG_LEN {
136            return Err(Error::Memory(
137                "MemoryEnclave::open: ciphertext too short".into(),
138            ));
139        }
140
141        let key_slot = super::pool::global_pool().coffer_view()?;
142        let nonce = Nonce::from_slice(&self.ciphertext[..NONCE_LEN]);
143
144        let cipher = Aes256Gcm::new_from_slice(key_slot.as_slice())
145            .map_err(|e| Error::Memory(format!("MemoryEnclave::open cipher init: {e}")))?;
146
147        // Wrap the decrypted plaintext in Zeroizing immediately so it is
148        // scrubbed when it goes out of scope — even on error paths below.
149        let plaintext = zeroize::Zeroizing::new(
150            cipher
151                .decrypt(nonce, &self.ciphertext[NONCE_LEN..])
152                .map_err(|_| Error::DecryptFailed {
153                    detail: "MemoryEnclave::open: authentication failed".into(),
154                })?,
155        );
156
157        // SAFETY note: Aes256Gcm implements ZeroizeOnDrop when compiled with the
158        // "zeroize" cargo feature (enabled in the workspace Cargo.toml). The expanded
159        // AES round-key schedule is therefore zeroed when `cipher` drops.
160        drop(cipher); // erase round-key schedule before returning key slot
161        drop(key_slot); // wipe coffer key from slab slot
162
163        // Cache the plaintext in the slab (only if it fits: exact slot_size match).
164        // If pool_acquire fails, evict the cache entry so we don't leave a
165        // cached plaintext that the caller has no slot to receive.
166        hot_cache_insert(self.id, &plaintext);
167
168        // Return plaintext in a fresh PoolSlot.
169        let mut out_slot = pool_acquire(plaintext.len()).map_err(|e| {
170            hot_cache_evict(self.id);
171            e
172        })?;
173        let copy_len = plaintext.len().min(out_slot.size());
174        out_slot.bytes()[..copy_len].copy_from_slice(&plaintext[..copy_len]);
175        Ok(out_slot)
176    }
177
178    pub fn plaintext_len(&self) -> usize {
179        self.plaintext_len
180    }
181
182    pub fn id(&self) -> u64 {
183        self.id
184    }
185}
186
187impl Drop for MemoryEnclave {
188    fn drop(&mut self) {
189        // Capture `id` by value (Copy) so the closure is UnwindSafe.
190        let id = self.id;
191        // catch_unwind prevents a double-panic during stack unwind.
192        drop(std::panic::catch_unwind(move || {
193            hot_cache_evict(id);
194        }));
195    }
196}
197
198#[cfg(test)]
199#[allow(clippy::unwrap_used, clippy::panic)]
200mod tests {
201    use std::sync::Mutex;
202
203    use super::*;
204    use crate::memory::pool::{coffer_view, hot_cache_get};
205
206    /// Serializes tests that touch the global TieredPool to prevent interference.
207    static TEST_LOCK: Mutex<()> = Mutex::new(());
208
209    #[test]
210    fn seal_and_open_roundtrip() {
211        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
212        let secret = b"my secret data 1234";
213        let enc = MemoryEnclave::seal(secret).unwrap();
214        let slot = enc.open().unwrap();
215        assert_eq!(&slot.as_slice()[..secret.len()], secret.as_ref());
216    }
217
218    #[test]
219    fn open_twice_uses_hot_cache() {
220        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
221        let secret = b"cached secret";
222        let enc = MemoryEnclave::seal(secret).unwrap();
223        let s1 = enc.open().unwrap();
224        // Drop s1 before opening again (releases slab slot for second open).
225        let bytes1 = s1.as_slice()[..secret.len()].to_vec();
226        drop(s1);
227        let s2 = enc.open().unwrap();
228        assert_eq!(bytes1, secret.as_ref());
229        assert_eq!(&s2.as_slice()[..secret.len()], secret.as_ref());
230    }
231
232    #[test]
233    fn drop_evicts_hot_cache() {
234        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
235        let secret = b"evicted secret";
236        let id = {
237            let enc = MemoryEnclave::seal(secret).unwrap();
238            let slot = enc.open().unwrap(); // populate cache
239            drop(slot);
240            enc.id()
241        }; // enc dropped here — should evict
242        assert!(hot_cache_get(id).is_none());
243    }
244
245    #[test]
246    fn different_enclaves_are_independent() {
247        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
248        let enc1 = MemoryEnclave::seal(b"secret one").unwrap();
249        let enc2 = MemoryEnclave::seal(b"secret two").unwrap();
250        assert_ne!(enc1.id(), enc2.id());
251        let s1 = enc1.open().unwrap();
252        let s2 = enc2.open().unwrap();
253        assert_eq!(&s1.as_slice()[..10], b"secret one");
254        assert_eq!(&s2.as_slice()[..10], b"secret two");
255        drop(s1);
256        drop(s2);
257    }
258
259    #[test]
260    fn seal_empty_slice() {
261        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
262        let enc = MemoryEnclave::seal(b"").unwrap();
263        assert_eq!(enc.plaintext_len(), 0);
264        let slot = enc.open().unwrap();
265        drop(slot);
266    }
267
268    #[test]
269    fn coffer_view_returns_key_sized_slot() {
270        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
271        let slot = coffer_view().unwrap();
272        assert_eq!(slot.size(), 32);
273    }
274
275    #[test]
276    fn pool_acquire_small_uses_slab() {
277        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
278        use crate::memory::pool::pool_acquire;
279        let slot = pool_acquire(16).unwrap();
280        assert!(slot.slab_index().is_some());
281    }
282
283    #[test]
284    fn pool_acquire_large_uses_standalone() {
285        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
286        use crate::memory::pool::pool_acquire;
287        let slot = pool_acquire(8192).unwrap();
288        assert!(slot.slab_index().is_none());
289    }
290
291    #[test]
292    fn seal_open_large() {
293        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
294        let plaintext = vec![0xAB_u8; 4096];
295        let enc = MemoryEnclave::seal(&plaintext).unwrap();
296        let slot = enc.open().unwrap();
297        assert_eq!(&slot.as_slice()[..4096], plaintext.as_slice());
298    }
299
300    #[test]
301    fn tampered_ciphertext_fails() {
302        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
303        let plaintext = b"tamper test";
304        let mut enc = MemoryEnclave::seal(plaintext).unwrap();
305        enc.ciphertext[NONCE_LEN] ^= 0xFF;
306        let result = enc.open();
307        assert!(
308            matches!(result, Err(Error::DecryptFailed { .. })),
309            "expected DecryptFailed, got {result:?}"
310        );
311    }
312
313    #[test]
314    fn truncated_ciphertext_fails() {
315        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
316        let enc = MemoryEnclave::seal(b"short").unwrap();
317        let truncated = MemoryEnclave {
318            id: enc.id,
319            ciphertext: vec![0_u8; NONCE_LEN + TAG_LEN - 1],
320            plaintext_len: 5,
321        };
322        let result = truncated.open();
323        assert!(
324            matches!(result, Err(Error::Memory(_))),
325            "expected Memory error, got {result:?}"
326        );
327    }
328
329    #[test]
330    fn unique_ids() {
331        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
332        let a = MemoryEnclave::seal(b"a").unwrap();
333        let b = MemoryEnclave::seal(b"b").unwrap();
334        assert_ne!(a.id(), b.id());
335    }
336
337    #[test]
338    fn seal_buffer_roundtrip() {
339        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
340        let secret = b"buffered secret";
341        let mut sbuf = SecureBuffer::new(secret.len()).unwrap();
342        sbuf.bytes().copy_from_slice(secret);
343        let enc = MemoryEnclave::seal_buffer(&mut sbuf).unwrap();
344        assert!(sbuf.is_alive());
345        let slot = enc.open().unwrap();
346        assert_eq!(&slot.as_slice()[..secret.len()], secret);
347    }
348
349    #[test]
350    fn seal_slot_roundtrip() {
351        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
352        let secret = b"slot secret data";
353        let mut slot = pool_acquire(secret.len()).unwrap();
354        slot.bytes()[..secret.len()].copy_from_slice(secret);
355        let enc = MemoryEnclave::seal_slot(&slot).unwrap();
356        drop(slot);
357        let out = enc.open().unwrap();
358        assert_eq!(&out.as_slice()[..secret.len()], secret);
359    }
360
361    #[test]
362    fn debug_does_not_leak_plaintext() {
363        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
364        let enc = MemoryEnclave::seal(b"top secret").unwrap();
365        let debug = format!("{enc:?}");
366        assert!(!debug.contains("top secret"));
367        assert!(debug.contains("MemoryEnclave"));
368    }
369
370    #[test]
371    fn same_plaintext_different_nonces() {
372        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
373        let a = MemoryEnclave::seal(b"same").unwrap();
374        let b = MemoryEnclave::seal(b"same").unwrap();
375        assert_ne!(a.ciphertext, b.ciphertext);
376    }
377
378    // ── New tests for review findings ────────────────────────────────
379
380    #[test]
381    fn plaintext_is_zeroized_after_open() {
382        // This test verifies that the intermediate plaintext Vec is Zeroizing-wrapped.
383        // We can't directly inspect the heap, but we verify the open() succeeds and
384        // returns correct data — the Zeroizing wrapper is a compile-time guarantee.
385        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
386        let secret = b"zeroize test secret";
387        let enc = MemoryEnclave::seal(secret).unwrap();
388        let slot = enc.open().unwrap();
389        assert_eq!(&slot.as_slice()[..secret.len()], secret.as_ref());
390    }
391
392    #[test]
393    fn open_cache_evicted_on_drop() {
394        // BLK-8: verify hot cache is evicted when MemoryEnclave is dropped.
395        // After a successful open and drop of the enclave, cache must be clear.
396        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
397        let enc = MemoryEnclave::seal(b"test").unwrap();
398        let id = enc.id();
399        let slot = enc.open().unwrap(); // populates cache
400        drop(slot);
401        drop(enc); // should evict
402        assert!(hot_cache_get(id).is_none());
403    }
404
405    #[test]
406    fn nonce_prefix_is_nonzero() {
407        // Probabilistically verifies OsRng ran (not a PID fallback).
408        // All-zero prefix would be astronomically unlikely with a real OsRng.
409        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
410        let enc1 = MemoryEnclave::seal(b"a").unwrap();
411        let enc2 = MemoryEnclave::seal(b"b").unwrap();
412        // Different ciphertexts implies different nonces (nonce uniqueness).
413        assert_ne!(enc1.ciphertext, enc2.ciphertext);
414    }
415
416    #[test]
417    fn fresh_nonce_never_returns_all_zeros() {
418        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
419        // Statistically impossible for a real OsRng to return all zeros.
420        // If this fails, OsRng is broken and nonces are predictable.
421        let enc1 = MemoryEnclave::seal(b"probe1").unwrap();
422        let enc2 = MemoryEnclave::seal(b"probe2").unwrap();
423        // Different ciphertexts (different nonces) — random nonce scheme is working.
424        assert_ne!(enc1.ciphertext, enc2.ciphertext);
425        // Neither ciphertext starts with 12 zero bytes (nonce portion).
426        assert!(
427            enc1.ciphertext[..NONCE_LEN].iter().any(|&b| b != 0),
428            "nonce is all zeros — OsRng may be broken"
429        );
430    }
431
432    #[test]
433    fn seal_and_open_boundary_sizes() {
434        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
435        for size in [
436            0_usize, 1, 15, 16, 31, 32, 33, 63, 64, 1023, 1024, 4095, 4096, 65535,
437        ] {
438            let plaintext = vec![0x5A_u8; size];
439            let enc = MemoryEnclave::seal(&plaintext).unwrap();
440            assert_eq!(
441                enc.plaintext_len(),
442                size,
443                "size {size}: plaintext_len mismatch"
444            );
445            let slot = enc.open().unwrap();
446            assert_eq!(
447                &slot.as_slice()[..size],
448                &plaintext[..],
449                "size {size}: roundtrip mismatch"
450            );
451        }
452    }
453
454    #[test]
455    fn nonce_tampering_fails_authentication() {
456        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
457        let mut enc = MemoryEnclave::seal(b"tamper nonce test").unwrap();
458        // Flip a bit in the nonce (first 12 bytes of ciphertext).
459        enc.ciphertext[0] ^= 0x01;
460        let result = enc.open();
461        assert!(
462            matches!(result, Err(Error::DecryptFailed { .. })),
463            "nonce tampering must fail authentication: {result:?}"
464        );
465    }
466
467    #[test]
468    fn tag_tampering_fails_authentication() {
469        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
470        let mut enc = MemoryEnclave::seal(b"tamper tag test").unwrap();
471        // Flip a bit in the GCM tag (last 16 bytes of ciphertext).
472        let len = enc.ciphertext.len();
473        enc.ciphertext[len - 1] ^= 0x01;
474        let result = enc.open();
475        assert!(
476            matches!(result, Err(Error::DecryptFailed { .. })),
477            "tag tampering must fail authentication: {result:?}"
478        );
479    }
480
481    #[test]
482    fn seal_ids_are_unique_and_increasing() {
483        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
484        let encs: Vec<_> = (0..10)
485            .map(|_| MemoryEnclave::seal(b"x").unwrap())
486            .collect();
487        let ids: Vec<u64> = encs.iter().map(|e| e.id()).collect();
488        // All distinct.
489        let mut sorted = ids.clone();
490        sorted.dedup();
491        assert_eq!(sorted.len(), ids.len(), "all IDs must be unique");
492        // Strictly increasing.
493        for w in ids.windows(2) {
494            assert!(
495                w[0] < w[1],
496                "IDs must be strictly increasing: {} < {}",
497                w[0],
498                w[1]
499            );
500        }
501    }
502
503    #[test]
504    fn different_plaintexts_produce_different_ciphertexts() {
505        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
506        let enc1 = MemoryEnclave::seal(b"first plaintext").unwrap();
507        let enc2 = MemoryEnclave::seal(b"second plaintext").unwrap();
508        assert_ne!(enc1.ciphertext, enc2.ciphertext);
509    }
510
511    #[test]
512    fn open_after_drop_uses_cold_path() {
513        // Seal → open (populates cache) → drop enclave (evicts cache) →
514        // seal again with same data → open (cold path, new ciphertext).
515        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
516        let secret = b"cold path test";
517        let enc1 = MemoryEnclave::seal(secret).unwrap();
518        let id1 = enc1.id();
519        let slot1 = enc1.open().unwrap();
520        drop(slot1);
521        drop(enc1); // evicts cache
522                    // Cache must be gone.
523        assert!(hot_cache_get(id1).is_none());
524        // Second seal/open works (cold path only).
525        let enc2 = MemoryEnclave::seal(secret).unwrap();
526        let slot2 = enc2.open().unwrap();
527        assert_eq!(&slot2.as_slice()[..secret.len()], secret);
528    }
529
530    #[test]
531    fn seal_slot_does_not_consume_slot() {
532        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
533        let secret = b"slot test value";
534        let mut slot = pool_acquire(secret.len()).unwrap();
535        slot.bytes()[..secret.len()].copy_from_slice(secret);
536        let enc = MemoryEnclave::seal_slot(&slot).unwrap();
537        // slot is still usable (seal_slot takes &PoolSlot, not by value).
538        assert_eq!(&slot.as_slice()[..secret.len()], secret);
539        // But the sealed data is independent.
540        drop(slot); // zeroizes original
541        let recovered = enc.open().unwrap();
542        assert_eq!(&recovered.as_slice()[..secret.len()], secret);
543    }
544
545    #[test]
546    fn ciphertext_nonce_is_twelve_bytes() {
547        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
548        let enc = MemoryEnclave::seal(b"nonce len test").unwrap();
549        // First 12 bytes are the nonce; verify they're not all zeros.
550        assert!(
551            enc.ciphertext[..12].iter().any(|&b| b != 0),
552            "nonce (first 12 bytes) must not be all zeros"
553        );
554        // Total length: 12 (nonce) + plaintext_len + 16 (GCM tag).
555        assert_eq!(enc.ciphertext.len(), 12 + enc.plaintext_len() + 16);
556    }
557
558    #[test]
559    fn coffer_slot_released_after_seal() {
560        // Verify the coffer slot is returned to the pool after seal completes.
561        // If it weren't, multiple seals would exhaust the slab.
562        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
563        for _ in 0..20 {
564            MemoryEnclave::seal(b"coffer slot release test").unwrap();
565        }
566        // If coffer slots leaked, the pool would be exhausted and the loop would fail.
567    }
568
569    #[test]
570    fn concurrent_seal_and_open() {
571        use std::sync::Arc;
572        use std::thread;
573        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
574        // 4 threads each seal a secret and immediately open it, independently.
575        let barrier = Arc::new(std::sync::Barrier::new(4));
576        let handles: Vec<_> = (0..4_u8)
577            .map(|i| {
578                let b = Arc::clone(&barrier);
579                thread::spawn(move || {
580                    let secret = vec![i; 16];
581                    let enc = MemoryEnclave::seal(&secret).unwrap();
582                    b.wait();
583                    let slot = enc.open().unwrap();
584                    assert_eq!(
585                        &slot.as_slice()[..16],
586                        &secret[..],
587                        "thread {i}: roundtrip mismatch"
588                    );
589                })
590            })
591            .collect();
592        for h in handles {
593            h.join().expect("thread panicked");
594        }
595    }
596}