TE
TechEcho
Home24h TopNewestBestAskShowJobs
GitHubTwitter
Home

TechEcho

A tech news platform built with Next.js, providing global tech news and discussions.

GitHubTwitter

Home

HomeNewestBestAskShowJobs

Resources

HackerNews APIOriginal HackerNewsNext.js

© 2025 TechEcho. All rights reserved.

Uninitialized memory: Unsafe Rust is too hard

106 pointsby drrlvnover 3 years ago

15 comments

notpopcornover 3 years ago
Without any unsafe code this is simply:<p><pre><code> let role = Role { name: &quot;basic&quot;, flag: 1, disabled: false, }; </code></pre> The language tries to prevent you from interacting with a `Role` object that&#x27;s not fully initialized. `mem::zero()` could work, but then you&#x27;ll have to turn the `&amp;&#x27;static str` into an `Option&lt;&amp;&#x27;static str&gt;` or a raw pointer, to indicate that it might be null. You could also add `#[derive(Default)]` to the struct, to automatically get a `Role::default()` function to create a `Role` with and then modify the fields afterwards, if you want to set the fields in separate statements for some reason:<p><pre><code> let mut role = Role::default(); role.name = &quot;basic&quot;; role.flag = 1; role.disabled = false; </code></pre> And even with `MaybeUninit` you can initialize the whole struct (without `unsafe`!) with `MaybeUninit::write`. It&#x27;s just that <i>partially</i> initializing something is hard to get right, which is the point of the article I guess. But I wonder how commonly you would really want that, as it easily leads to mistakes.
评论 #30139060 未加载
评论 #30139282 未加载
评论 #30138758 未加载
评论 #30145743 未加载
jcranmerover 3 years ago
Here&#x27;s another perspective on why things are the way they are:<p>One of the central philosophies of Rust is that it should not be possible to execute undefined behavior using only safe code. Rust&#x27;s underlying core semantics end up being <i>very</i> similar to C&#x27;s semantics, at least in terms of where undefined behavior can arise, and we can imagine Rust&#x27;s references as being wrappers around the underlying pointer type that have extra requirements to ensure that they can be safely dereferenced in safe code without <i>ever</i> causing UB.<p>So consider a simple pointer dereference in C (*p)... how could that cause UB? Well, the obvious ones are that the pointer could be out-of-bounds or pointing to an expired memory location. So references (&amp; and &amp;mut) most point to a live memory location, even in unsafe code. Also pretty obviously, the pointer would be UB were it unaligned, so a Rust reference must be properly aligned.<p>Another one that should be familiar from the C context is that the memory location must be initialized. So the &amp; reference in Rust means that the memory location must also be initialized... and since &amp;mut <i>implies</i> &amp;, so must &amp;mut. This part is probably genuinely surprising, since it&#x27;s a rule that <i>doesn&#x27;t</i> apply to C.<p>The most surprising rule that applies here as well is that the memory location cannot be a trap representation (to use C&#x27;s terminology). Yes--C has the same requirement here, but most people probably don&#x27;t come across a platform that has trap representations in C. The reason why std::mem::uninitialized was deprecated in favor of MaybeUninit was that Rust has a type all of whose representations are trap representation (that&#x27;s the ! type).<p>In short, the author is discovering two related issues here. First, the design of Rust is to push all of the burden of undefined behavior into unsafe code blocks, and the downside of that is that most programmers probably aren&#x27;t sufficiently cognizant of UB rules to do that rule. Rust also pushes the UB of pointers to reference construction, whereas C makes most of its UB happen only on pointer dereference (constructing unaligned pointers being the exception).<p>The second issue is that Rust&#x27;s syntax is geared to making <i>safe</i> Rust ergonomic, not unsafe Rust. This means that using the &quot;usual&quot; syntax rules in unsafe Rust blocks is more often than not UB, even when you&#x27;re trying to avoid the inherent UB construction patterns. Struct projection (given a pointer&#x2F;reference to a struct, get a pointer&#x2F;reference to a field) is especially implicated here.<p>These combine when you deal with uninitialized memory references. This is a reasonably common pattern, but designing an always-safe abstraction for uninitialized memory is challenging. And Rust did screw this up, and the stability guidelines means the bad implementations are baked in for good (see, e.g., std::io::Read).
评论 #30140009 未加载
notpopcornover 3 years ago
&gt; So we use a &amp;&#x27;static str here instead of a C string so there are some changes to the C code.<p>&gt; [..]<p>&gt; So why does this type not support zero initialization? What do we have to change? Can zeroed not be used at all? Some of you might think that the answer is #[repr(C)] on the struct to force a C layout but that won&#x27;t solve the problem.<p>The type of the first field was switched to a type (&amp;str) that specifically promises it is never null. If the original type (a pointer) was kept, or a Option&lt;&amp;str&gt; was used, mem::zero would&#x27;ve worked fine.
dupedover 3 years ago
To the OP - why should creating uninitialized references with static lifetimes be easy? That is a recipe for undefined behavior - borrows aren&#x27;t pointers, if you want a pointer to be zero initialized, then use a pointer.<p>If you want safe access to that pointer then wrap it in a struct with an accessor method
staticassertionover 3 years ago
I think that the premise here is correct - writing unsafe Rust <i>is</i> too hard. There are lots of footguns.<p>This isn&#x27;t a very good motivating example but I suppose it does the job of showing the various hoops one has to jump through when using unsafe.<p>I think right now the approach is to make unsafe &quot;safe&quot; (ie std::mem::uninitialized -&gt; MaybeUninit) at the cost of complex, and eventually to build out improved helpers and abstractions. Obviously this is still ongoing.<p>But also, just don&#x27;t write unsafe? It&#x27;s very easy to avoid.
评论 #30139319 未加载
评论 #30140412 未加载
dathinabover 3 years ago
The scary thing is:<p>Handling uninitialized memory is hard in C++ (and C), too.<p>You just don&#x27;t notice and accidentally do it slightly wrong (mainly in C++, in C it&#x27;s harder to mess up).
评论 #30140440 未加载
jcranmerover 3 years ago
In C, when you declare &#x27;struct role r&#x27; (not as a static variable), it is not zeroed. The immediate Rust equivalent would be to use std::mem::uninitialized(), not std::mem::zeroed.
评论 #30138278 未加载
andreareinaover 3 years ago
I don&#x27;t know rust, but why isn&#x27;t the answer, don&#x27;t try to do what you&#x27;d do in C like construct uninitialized structs?
评论 #30137605 未加载
评论 #30139044 未加载
评论 #30137661 未加载
Ericson2314over 3 years ago
The write_unaligned is pure FUD. Regular unpacked structs don&#x27;t violate alignment on fields!
评论 #30140179 未加载
评论 #30140774 未加载
sharikousover 3 years ago
What is the reason for the rule objects have to be always in a good state even inside unsafe?
评论 #30138056 未加载
评论 #30138198 未加载
LinAGKarover 3 years ago
But the code isn&#x27;t equivalent. The C code just has a pointer to a manually allocated buffer, while the Rust does the equivalent of zeroing out (or leaving uninitialized) a C++ std::string. Akin to:<p>auto <i>name = reinterpret_cast&lt;std::string </i>&gt;(malloc(sizeof(std::string))); memset(name, 0, sizeof(std::string); *name = &quot;basic&quot;;<p>But on the stack.
eddybover 3 years ago
&gt; For instance `(*role).name` creates a `&amp;mut &amp;&#x27;static str` behind the scenes which is illegal, even if we can&#x27;t observe it because the memory where it points to is not initialized.<p>Where is this coming from? It&#x27;s literally not true. The MIR for this has:<p><pre><code> ((*_3).0: &amp;str) = const &quot;basic&quot;; ((*_3).2: u32) = const 1_u32; ((*_3).1: bool) = const false; </code></pre> So it&#x27;s only going to do a raw offset and then assign to it, which is identical to `*ptr::addr_of_mut!((*role).field) = value`.<p>Sadly there&#x27;s no way to tell miri to consider `&amp;mut T` valid only if `T` is valid (that choice is not settled yet, AFAIK, at the language design level), in order to demonstrate the difference (<a href="https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;miri&#x2F;issues&#x2F;1638" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;miri&#x2F;issues&#x2F;1638</a>).<p>The other claim, &quot;dereferencing is illegal&quot;, is more likely, but unlike popular misconception, &quot;dereference&quot; is a <i>syntactic</i> concept, that turns a (pointer&#x2F;reference) &quot;value&quot; into a &quot;place&quot;.<p>There&#x27;s no &quot;operation&quot; of &quot;dereference&quot; to attach <i>dynamic semantics</i> to. After all, `ptr::addr_of_mut!(*p).write(x)` has to remain as valid as `p.write(x)`, and it does literally contain a &quot;dereference&quot; operation (and so do your field projections).<p>So it&#x27;s still inaccurate. I <i>believe</i> what you want is to say that in `place = value` the destination `place` has to hold a valid value, as if we were doing `mem::replace(&amp;mut place, value)`. This is indeed true for types that have destructors in them, since those would need to run (which in itself is why `write` on pointers exists - it long existed before any of the newer ideas about &quot;indirect validity&quot; in recent years).<p>However, you have `Copy` types there, and those are <i>definitely</i> not different from `&lt;*mut T&gt;::write` to assign to, today. I don&#x27;t see us having to change that, but I&#x27;m also not seeing any references to where these ideas are coming from.<p>&gt; I&#x27;m pretty sure we can depend on things being aligned<p>What do you mean &quot;pretty sure&quot;? Of course you can, otherwise it would be UB to allow safe references to those fields! Anything else <i>would be unsound</i>. In fact, this goes hand in hand with the main significant omission of this post: this is <i>not</i> how you&#x27;re supposed to use `MaybeUninit`.<p>All of this raw pointer stuff is a distraction from the fact that what you want is `&amp;mut MaybeUninit&lt;FieldType&gt;`. Then all of the things about reference validity are <i>necessarily</i> true, and you can <i>safely</i> initialize the value. The only `unsafe` operation in this entire blog post, that isn&#x27;t unnecessarily added in, is `assume_init`.<p>What the author doesn&#x27;t mention is that Rust fails to let you convert between `&amp;mut MaybeUninit&lt;Struct&gt;` and some hypothetical `&amp;mut StructBut&lt;replace Field with MaybeUninit&lt;Field&gt;&gt;` because the language isn&#x27;t powerful enough to do it automatically. This was one of the saddest things about `MaybeUninit` (and we tried to rectify it for at least arrays).<p>This is where I was going to link to a custom derive that someone has written to generate that kind of transform manually (with the necessary check for safe field access wrt alignment). To my shock, I can&#x27;t find one. Did I see one and did it have a funny name? (the one thing I did find was a macro crate but unlike a derive those have a harder time checking everything so I had to report <a href="https:&#x2F;&#x2F;github.com&#x2F;youngspe&#x2F;project-uninit&#x2F;issues&#x2F;1" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;youngspe&#x2F;project-uninit&#x2F;issues&#x2F;1</a>)
mlindnerover 3 years ago
This author doesn&#x27;t even seem to know C properly so it&#x27;s hard to accept their reasoning.<p>This in Rust:<p><pre><code> let mut role: Role = mem::zeroed(); </code></pre> Is not the same as this in C:<p><pre><code> struct role r; </code></pre> C does not zero initialize.
sAbakumoffover 3 years ago
I am under the impression that even _safe_ Rust is really hard to learn. Several years ago I started with GoLang and it was so easy to start programming even advanced things almost instantly..Rust drives me crazy. The syntax seems overcomplicated, the compiler errors are cryptic, the IDE is not helpful.
评论 #30138240 未加载
评论 #30138168 未加载
评论 #30139122 未加载
评论 #30138315 未加载
评论 #30138252 未加载
评论 #30138158 未加载
remramover 3 years ago
&gt; Because that raw pointer does not implement deref and because Rust has no -&gt; operator we now need to dereference the pointer permanently to assign the fields with that awkward syntax.<p>Absolutely not, you can still use a mutable reference:<p><pre><code> let role = &amp;mut *uninit.as_mut_ptr(); role.name = &quot;basic&quot;;</code></pre>
评论 #30139554 未加载