Skip to content

Commit 14fad8e

Browse files
committed
zeroize: Remove scary language about undefined behavior
The documentation previously included a lengthy essay about potential caveats in what it can guarantee as mixed volatile/non-volatile writes were previously assumed to be undefined behavior due to language to that effect in the official documentation for `write_volatile`. That language has subsequently been removed after discussion about the safety implications: rust-lang/rust#60972 Though generally it's not a good idea to mix volatile and non-volatile writes when using volatile accesses to communicate with e.g. memory mapped external devices, for the particular usage pattern in this crate, i.e. writing a zero-value, it can be considered well-defined.
1 parent 56ae739 commit 14fad8e

File tree

1 file changed

+35
-118
lines changed

1 file changed

+35
-118
lines changed

zeroize/src/lib.rs

Lines changed: 35 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -109,116 +109,34 @@
109109
//!
110110
//! ## What guarantees does this crate provide?
111111
//!
112-
//! Ideally a secure memory-zeroing function would guarantee the following:
113-
//!
114-
//! 1. Ensure the zeroing operation can't be "optimized away" by the compiler.
115-
//! 2. Ensure all subsequent reads to the memory following the zeroing operation
116-
//! will always see zeroes.
117-
//!
118-
//! This crate guarantees #1 is true: LLVM's volatile semantics ensure it.
119-
//!
120-
//! The story around #2 is much more complicated. In brief, it should be true
121-
//! that LLVM's current implementation does not attempt to perform
122-
//! optimizations which would allow a subsequent (non-volatile) read to see the
123-
//! original value prior to zeroization. However, this is not a guarantee, but
124-
//! rather an LLVM implementation detail, a.k.a. *undefined behavior*.
125-
//! It provides what we believe to be the best implementation possible on
126-
//! stable Rust, but we cannot yet make guarantees it will work reliably
127-
//! 100% of the time (particularly on exotic CPU architectures).
128-
//!
129-
//! For more background, we can look to the [core::ptr::write_volatile]
130-
//! documentation:
131-
//!
132-
//! > Volatile operations are intended to act on I/O memory, and are guaranteed
133-
//! > to not be elided or reordered by the compiler across other volatile
134-
//! > operations.
135-
//! >
136-
//! > Memory accessed with `read_volatile` or `write_volatile` should not be
137-
//! > accessed with non-volatile operations.
138-
//!
139-
//! Uhoh! This crate does not guarantee all reads to the memory it operates on
140-
//! are volatile, and the documentation for [core::ptr::write_volatile]
141-
//! explicitly warns against mixing volatile and non-volatile operations.
142-
//! Perhaps we'd be better off with something like a `VolatileCell`
143-
//! type which owns the associated data and ensures all reads and writes are
144-
//! volatile so we don't have to worry about the semantics of mixing volatile and
145-
//! non-volatile accesses.
146-
//!
147-
//! While that's a strategy worth pursuing (and something we may investigate
148-
//! separately from this crate), it comes with some onerous API requirements:
149-
//! it means any data that we might ever desire to zero is owned by a
150-
//! `VolatileCell`. However, this does not make it possible for this crate
151-
//! to act on references, which severely limits its applicability. In fact
152-
//! a `VolatileCell` can only act on values, i.e. to read a value from it,
153-
//! we'd need to make a copy of it, and that's literally the opposite of
154-
//! what we want.
155-
//!
156-
//! It's worth asking what the precise semantics of mixing volatile and
157-
//! non-volatile reads actually are, and whether a less obtrusive API which
158-
//! can act entirely on mutable references is possible, safe, and provides the
159-
//! desired behavior.
160-
//!
161-
//! Unfortunately, that's a tricky question, because
162-
//! [Rust does not have a formally defined memory model][memory-model],
163-
//! and the behavior of mixing volatile and non-volatile memory accesses is
164-
//! therefore not rigorously specified and winds up being an LLVM
165-
//! implementation detail. The semantics were discussed extensively in this
166-
//! thread, specifically in the context of zeroing secrets from memory:
167-
//!
168-
//! <https://internals.rust-lang.org/t/volatile-and-sensitive-memory/3188/24>
169-
//!
170-
//! Some notable details from this thread:
171-
//!
172-
//! - Rust/LLVM's notion of "volatile" is centered around data *accesses*, not
173-
//! the data itself. Specifically it maps to flags in LLVM IR which control
174-
//! the behavior of the optimizer, and is therefore a bit different from the
175-
//! typical C notion of "volatile".
176-
//! - As mentioned earlier, LLVM does not presently contain optimizations which
177-
//! would reorder a non-volatile read to occur before a volatile write.
178-
//! However, there is nothing precluding such optimizations from being added.
179-
//! LLVM presently appears to exhibit the desired behavior for point
180-
//! #2 above, but there is nothing preventing future versions of Rust
181-
//! and/or LLVM from changing that.
182-
//!
183-
//! To help mitigate concerns about reordering potentially exposing values
184-
//! after they have been zeroed, this crate leverages the [core::sync::atomic]
185-
//! memory fence functions including [compiler_fence] and [fence] (which uses
186-
//! the CPU's native fence instructions). These fences are leveraged with the
187-
//! strictest ordering guarantees, [Ordering::SeqCst], which ensures no
188-
//! accesses are reordered. Without a formally defined memory model we can't
189-
//! guarantee these will be effective, but we hope they will cover most cases.
190-
//!
191-
//! Concretely the threat of leaking "zeroized" secrets (via reordering by
192-
//! LLVM and/or the CPU via out-of-order or speculative execution) would
193-
//! require a non-volatile access to be reordered ahead of the following:
194-
//!
195-
//! 1. before an [Ordering::SeqCst] compiler fence
196-
//! 2. before an [Ordering::SeqCst] runtime fence
197-
//! 3. before a volatile write
198-
//!
199-
//! This seems unlikely, but our usage of mixed non-volatile and volatile
200-
//! accesses is technically undefined behavior, at least until guarantees
201-
//! about this particular mixture of operations is formally defined in a
202-
//! Rust memory model.
203-
//!
204-
//! Furthermore, given the recent history of microarchitectural attacks
205-
//! (Spectre, Meltdown, etc), there is also potential for "zeroized" secrets
206-
//! to be leaked through covert channels (e.g. memory fences have been used
207-
//! as a covert channel), so we are wary to make guarantees unless they can
208-
//! be made firmly in terms of both a formal Rust memory model and the
209-
//! generated code for a particular CPU architecture.
210-
//!
211-
//! In conclusion, this crate guarantees the zeroize operation will not be
212-
//! elided or "optimized away", makes a "best effort" to ensure that
213-
//! memory accesses will not be reordered ahead of the "zeroize" operation,
214-
//! but **cannot** yet guarantee that such reordering will not occur.
215-
//!
216-
//! In the future it might be possible to guarantee such behavior using
217-
//! [LLVM's "unordered" atomic mode][unordered], which is documented as
218-
//! being free of undefined behavior. There's an open issue to
219-
//! [expose atomic memcpy/memset in core/std][llvm-atomic]
220-
//! in which case this crate could leverage them to provide well-defined
221-
//! guarantees that zeroization will always occur.
112+
//! This crate guarantees the following:
113+
//!
114+
//! 1. The zeroing operation can't be "optimized away" by the compiler.
115+
//! 2. All subsequent reads to memory will see "zeroized" values.
116+
//!
117+
//! LLVM's volatile semantics ensure #1 is true.
118+
//!
119+
//! Additionally, thanks to work by the [Unsafe Code Guidelines Working Group],
120+
//! we can now fairly confidently say #2 is true as well. Previously there were
121+
//! worries that the approach used by this crate (mixing volatile and
122+
//! non-volatile accesses) was undefined behavior due to language contained
123+
//! in the documentation for `write_volatile`, however after some discussion
124+
//! [these remarks have been removed] and the specific usage pattern in this
125+
//! crate is considered to be well-defined.
126+
//!
127+
//! To help mitigate concerns about reordering of operations executed by the
128+
//! CPU potentially exposing values after they have been zeroed, this crate
129+
//! leverages the [core::sync::atomic] memory fence functions including
130+
//! [compiler_fence] and [fence] (which uses the CPU's native fence
131+
//! instructions). These fences are leveraged with the strictest ordering
132+
//! guarantees, [Ordering::SeqCst], which ensures no accesses are reordered.
133+
//!
134+
//! All of that said, there is still potential for microarchitectural attacks
135+
//! (ala Spectre/Meltdown) to leak "zeroized" secrets through covert channels
136+
//! (e.g. the memory fences mentioned above have previously been used as a
137+
//! covert channel in the Foreshadow attack). This crate makes no guarantees
138+
//! that zeroized values cannot be leaked through such channels, as they
139+
//! represent flaws in the underlying hardware.
222140
//!
223141
//! ## Stack/Heap Zeroing Notes
224142
//!
@@ -240,18 +158,15 @@
240158
//! attempting to zeroize such buffers to initialize them to the correct
241159
//! capacity, and take care to prevent subsequent reallocation.
242160
//!
243-
//! This crate does not intend to implement higher-level abstractions to
244-
//! eliminate these risks, instead it merely makes a best effort to clear the
245-
//! memory it's aware of.
161+
//! The `secrecy` crate provides higher-level abstractions for eliminating
162+
//! usage patterns which can cause reallocations:
246163
//!
247-
//! Crates which are built on `zeroize` and provide higher-level abstractions
248-
//! for strategically avoiding these problems would certainly be interesting!
249-
//! (and something we may consider developing in the future)
164+
//! <https://crates.io/crates/secrecy>
250165
//!
251166
//! ## What about: clearing registers, mlock, mprotect, etc?
252167
//!
253-
//! This crate is laser-focused on being a simple, unobtrusive crate for zeroing
254-
//! memory in as reliable a manner as is possible on stable Rust.
168+
//! This crate is focused on providing simple, unobtrusive support for reliably
169+
//! zeroing memory using the best approach possible on stable Rust.
255170
//!
256171
//! Clearing registers is a difficult problem that can't easily be solved by
257172
//! something like a crate, and requires either inline ASM or rustc support.
@@ -276,6 +191,8 @@
276191
//! [DefaultIsZeroes]: https://docs.rs/zeroize/latest/zeroize/trait.DefaultIsZeroes.html
277192
//! [Default]: https://doc.rust-lang.org/std/default/trait.Default.html
278193
//! [core::ptr::write_volatile]: https://doc.rust-lang.org/core/ptr/fn.write_volatile.html
194+
//! [Unsafe Code Guidelines Working Group]: https://github.com/rust-lang/unsafe-code-guidelines
195+
//! [these remarks have been removed]: https://github.com/rust-lang/rust/pull/60972
279196
//! [core::sync::atomic]: https://doc.rust-lang.org/stable/core/sync/atomic/index.html
280197
//! [Ordering::SeqCst]: https://doc.rust-lang.org/std/sync/atomic/enum.Ordering.html#variant.SeqCst
281198
//! [compiler_fence]: https://doc.rust-lang.org/stable/core/sync/atomic/fn.compiler_fence.html

0 commit comments

Comments
 (0)