-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Safe memory zeroing #2626
Comments
Strong 👍 from me. Bikeshed: The naming seems a bit confusing. Maybe Edit: What's up with the placement of Also, why is this filed as an issue rather than a PR? |
I've included a couple of alternatives in the issue - I don't think there is a single best way about how to proceed. Writing an RFC and submitting a PR will be smoother when we agree on a general direction to go. For that, a pre-RFC style issue might be better.
If we implement a The main drawback of the trait method is that I prefer the simplicity of the second alternative (using a marker trait + a const fn), even if that means having to find a new name for |
Yeah, if you're going to mess around with how zeroed works you should at least make it const. If that means that the trait version can't be made until trait methods support const fn then it seems fine to wait for that. |
Note that currently |
I'm fully in favor of having a safe and const way to zero types that opt into it. |
I like the idea. Regarding the alternatives, I think the best combination is the following:
Lastly, zeroing memory should mostly matter at Drop time, instead of at init time (we have both |
This is out-of-scope for this proposal. |
I only care about zeroed memory at init time, and I care nothing about at Drop time. And using zeroed() into the needlessly more wordy MaybeUninit::zeroed().into_inner() For absolutely no reason? As to drop time zeroing, we already have a crate for that EDIT: that was at |
@Lokathor please try to keep this issue on topic. |
@gnzlbg Agreed. Maybe edit the initial post to make it extra clear this proposal has nothing to do with security? |
re: the auto trait alternative, a problem not mentioned here AFAICT (but I'm by no means the first person to point this out in general) is that edit: a more general problem is that enums implement auto traits if all variants' fields implement them, but for the purpose of zeroing we also care about the discriminant values. |
About alternatives, instead of introducing There are three issues with using
|
Of course it's possible to implement Default for |
For my purposes in |
Thus using @retep998 there will always be a generic zeroing mechanism precisely for FFI, which requires such. But it is to become a deprecated non-FFI Rust pattern, given the UB danger of using it in a generic context that could monomorphise to nonnullable types such as |
An alternative to this that I think merits mention in a potential RFC would be a trait along the same lines but which, when implemented for T, guarantees that all byte strings of the correct length and alignment are valid values of T. This would allow additional use's beyond initializing values from zeroed memory. For example, it would allow safely initializing a value from an arbitrary (but not undefined) byte sequence of the correct length and alignment, which would be useful for zero-cost serialization and deserialization. It would also allow marking types as safe to pass into safe rust code over FFI boundaries. I think the downside might be that some types could not implement Other than that, it seems like they would be around equally complex to implement. Although I could definitely be missing other downsides to |
the safe transmute wg has been developing ideas for this sort of trait, just in the initial stages, but there's a non-zero amount of progress over time. |
@Lokathor Thanks for the pointer! I found |
Note that one commonly used type in FFI is |
I'm new to rust was using an thirdparty C-library I wrapped, which uses a lot of nested structs. And after I saw the Default trait, I immediately looked for a Zeroed trait since that felt like the natural way to do things in rust. I was quite surprised that this was not available in the language, so I ended up having the structs being zero by default instead (since they didn't really have any other sane default). But I really support the general idea to have this in the language. |
Update: In the ecosystem there's now |
(note: I use the terms safe and valid below with the precise meanings specified here: https://github.com/rust-rfcs/unsafe-code-guidelines/blob/master/reference/src/glossary.md#validity-and-safety-invariant)
Motivation
See rust-lang/rust#53491. In a nutshell:
std::mem::zeroed()
is dangerous - running this on miri (playground):produces an error of the form: "type validation failed: encountered NULL reference". Obviously, in the real world, code like this will be caught in code review, but catching this stops being easy when one has a struct with multiple fields and one has to manually verify that all fields in the struct are valid when all its bits are zero. Layer a couple of user-defined types on top of each other, and a small private change to one of them down the stack can easily make code using
mem::zeroed
have instant undefined behavior.When this happens, right now, having a test suite and running it on miri is the only way we have to detect that. However, C FFI is one of the main usages of
mem::zeroed
andmiri
has very limited support for that. So even if you have a good test suite,miri
won't help you here.This RFC provides a solution that catches these errors at compile-time, allowing type users to zero-initialize types using safe Rust code reliably and allowing type authors to specify that doing this is a part of the type's API that they are committed to support (where changing this would be an API breaking change).
User-level explanation
Alternative: Zeroed trait like Default
(note: the trait name
Zeroed
is yet to be bikeshedded - I think it would be better to agree on the approach and the semantics, and when that has consensus, we can bikeshed the name at the end).(note: I think I prefer the marker trait + const fn approach explained below)
Add a
std::zeroed::Zeroed
trait, similar toDefault
, that denotes that the zero bit-pattern is a valid bit-pattern of the type and that this bit-pattern is safe to use. This trait isunsafe
to implement - implementing it for&T
would makeZeroed::zeroed
have undefined behavior.Implement
Zeroed
incore
for all libcore types for which this is the case: integers, raw pointers, etc. - do not implement it for references,NonZero{...}
, etc.Add a custom-derive
Zeroed
that can be used to manually derive this trait for user-defined types without usingunsafe
Rust (e.g. if all the fields of a struct implementZeroed
). If the struct cannot deriveZeroed
that should produce a compile-time error. Whether the all-zeros bit-pattern is valid and safe bit-pattern for a type is an API contract from the writer of the type to its users. This is why manually specifying it instead of using an auto trait feels like a better solution to the problem.To upgrade code that previously was using
mem::zeroed()
toZeroed
, one changes:to
We should probably add
Zeroed
to thestd::prelude::v1
.After this change we can deprecate
std::mem::zeroed
with a deprecation warning "usestd::zeroed::Zeroed
instead".An RFC for this feature would probably leave this as an unresolved question, but we probably should turn that deprecation message into an error in the next edition. That is, for crates using
edition = rust2021
usingstd::mem::zeroed
should error with the deprecation message instead. That is,libcore
will containmem::zeroed
forever, so that Rust code using older editions can still use it, but we probably want to add a mechanism to ban using it from code that decides to use a newer edition.Alternatives
auto trait
We could make
Zeroed
anauto
-trait, but then I don't see how it could be used to denote that a zeroed value is safe to use - we could still use it to denote that the value is valid. This has two consequences:zeroinit
would need to beunsafe
, since the resulting value might not be safe to use. That is, just because the zero bit pattern does not cause undefined behavior instantaneously does not imply that safe methods on the type might not all have a pre-condition that the bit-pattern is not all zeros. The user of the type might not want to provide a way to safely construct a value with such a bit-pattern, and it would be bad for the user to have to opt-out thisauto
trait to maintain safety.Being able to zero-initialize a type is something that users of the type should be able to rely on. Once the type author commits to providing this API, it should be at least automatically noticeable when a change in the type breaks this API. With the non-auto trait + derive this happens automatically. With an
auto
-trait type authors would need to add a test for this (e.g.fn foo() -> T { Zeroed::zeroed() }
or similar).marker trait
We could also make
Zeroed
a marker trait, and have some function like:this approach has the advantage that
zeroed2
is aconst fn
. The disadvantage is that we can't call this functionmem::zeroed
because such a function already exist, and we'd have to either put it somewhere else, or call it somewhere else (e.g.mem::zeroinit
? ).There is an RFC (rust-lang/const-eval#8) that would solve this problem by allowing us to:
to indicate that the default method impl is const, and then allowing the Zeroed derive to perform a:
to add a const impl. However, the
Zeroed::zeroed
trait+trait method approach gives users the flexibility of adding their ownzeroed
implementations, and this is not a flexibility that I do think that we want. Paying for this flexibility might not be a good idea.The simplicity of an unsafe to implement marker trait + a
const fn
somewhere in libcore is definitely appealing.The text was updated successfully, but these errors were encountered: