[HN Gopher] Writing into Uninitialized Buffers in Rust
___________________________________________________________________
Writing into Uninitialized Buffers in Rust
Author : luu
Score : 117 points
Date : 2025-05-19 17:56 UTC (2 days ago)
(HTM) web link (blog.sunfishcode.online)
(TXT) w3m dump (blog.sunfishcode.online)
| 90s_dev wrote:
| Write into uninit'd buffers was one of the pain points of Rust
| for the creator of the new open source "edit" program for
| Windows[1]. I wonder what he thinks of this article.
|
| > Another thing is the difficulty of using uninitialized data in
| Rust. I do understand that this involves an attribute in clang
| which can then perform quite drastic optimizations based on it,
| but this makes my life as a programmer kind of difficult at
| times. When it comes to `MaybeUninit`, or the previous
| `mem::uninit()`, I feel like the complexity of compiler
| engineering is leaking into the programming language itself and
| I'd like to be shielded from that if possible. At the end of the
| day, what I'd love to do is declare an array in Rust, assign it
| no value, `read()` into it, and magically reading from said array
| is safe. That's roughly how it works in C, and I know that it's
| also UB there if you do it wrong, but one thing is different: It
| doesn't really ever occupy my mind as a problem. In Rust it does.
| [https://news.ycombinator.com/item?id=44036021]
| electrograv wrote:
| > That's roughly how it works in C, and I know that it's also
| UB there if you do it wrong, but one thing is different: It
| doesn't really ever occupy my mind as a problem. In Rust it
| does.
|
| UB doesn't occupy the author's mind when writing C, when it
| _really_ should. This kind of lazy attitude to memory safety is
| precisely why so much C code is notoriously riddled with memory
| bugs and security vulnerabilities.
| mk12 wrote:
| There is an important difference for this case though. It C
| it's fine to have pointers into uninitialized memory as you
| as you don't read them until after initializing. You can
| write through those pointers the same way you always do. In
| Rust it's UB as soon as you "produce" an invalid value, which
| includes references to uninitialized memory. Everything uses
| references in Rust but when dealing with uninitialized memory
| you have to scrupulously avoid them, and instead write
| through raw pointers. This means you can't reuse any code
| that writes through &mut. Also, the rules change over time.
| At one point I had unsafe code that had a Vec of
| uninitialized elements, which was ok because I never produced
| a reference to any element until after I had written them
| (through raw pointers). But they later changed the Vec docs
| to say that's UB, I guess because they want to reserve the
| right to use references even if you never call a method that
| returns a reference.
| Arnavion wrote:
| This stopped being much of a problem when MaybeUninit was
| stabilized. Now you can stick to using &MaybeUninit<T> /
| &mut MaybeUninit<T> instead of needing to juggle *T / *mut
| T and carefully track converting that to &T / &mut T only
| when it's known to be initialized, and you can't
| accidentally use a MaybeUninit<T> where you meant to use a
| T because the types are different.
|
| It's not as painless as it could be though, because many of
| the MaybeUninit<T> -> T conversion fns are unstable. Eg the
| code in TFA needs `&mut [MaybeUninit<T>] -> &mut [T]` but
| `[T]::assume_init_mut()` is unstable. But reimplementing
| them is just a matter of copying the libstd impl, that in
| turn is usually just a straightforward reinterpret-cast
| one-liner.
| nemothekid wrote:
| Bizarre. I think I've been writing broken Rust code for a
| couple years. If I understand you correctly something like:
| let mut data = Vec::with_capacity(sz); unsafe {
| data.set_len(sz) };
| buf.copy_to_slice(data.as_mut_slice());
|
| is UB?
| vgatherps wrote:
| Yes, this is the case that I ran into as well. You have
| to zero memory before reading and/or have some crazy
| combination of tracking what's uninitialized capacity or
| initialized len, I think the rust stdlib write trait for
| &mut Vec got butchered over this concern.
|
| It's strictly more complicated and slower than the
| obvious thing to do and only exists to satisfy the
| abstract machine.
| Arnavion wrote:
| No. The correct way to write that code is to use
| .spare_capacity_mut() to get a &mut [MaybeUninit<T>],
| then write your Ts into that using
| .write_copy_of_slice(), then .set_len(). And that will
| not be any slower (though obviously more complicated)
| than the original incorrect code.
| vgatherps wrote:
| Oh this is very nice, I think it was stabilized since I
| wrote said code.
| nemothekid wrote:
| write_copy_of_slice doesn't look to be stable. I'll mess
| around with godbolt, but my hope that whatever
| incantation is used compiles down to a memcpy
| Arnavion wrote:
| As I wrote in
| https://news.ycombinator.com/item?id=44048391 , you have
| to get used to copying the libstd impl when working with
| MaybeUninit. For my code I put a "TODO(rustup)" comment
| on such copies, to remind myself to revisit them every
| time I update the Rust version in toolchain.toml
| nemothekid wrote:
| In other words the """safe""" stable code looks like
| this: let mut data =
| Vec::with_capacity(sz); let mut dst_uninit =
| data.spare_capacity_mut(); let uninit_src:
| &[MaybeUninit<T>] = unsafe { transmute(buf) };
| dst_uninit.copy_from_slice(uninit_src); unsafe {
| data.set_len(sz) };
| Arnavion wrote:
| That's correct.
| eptcyka wrote:
| Valgrind it :)
| vlovich123 wrote:
| Valgrind doesn't tell you about UB, just if the code did
| something incorrect with memory and that depends on what
| the optimizer did if you did write UB code. You'll need
| Miri to tell you if this kind of code is triggering UB
| which works by evaluating and analyzing the mid level of
| compiler output to check if Rust rules about safety are
| followed.
| ironhaven wrote:
| That's right. Line 3 is undefined behaviour because you
| are creating mutable references to the uninit spare
| capacity of the vec. copy_to_slice only works with
| writing to initialized slices. The proper way for you
| example to mess with the uninitialized memory on a vec
| would be only use raw pointers or calling the newly added
| Vec::spare_capacity_mut function on the vec that returns
| a slice of MaybeUninit
| NobodyNada wrote:
| It's an open question whether creating a reference to an
| uninitialized value is instant UB, or only UB if that
| reference is misused (e.g. if copy_to_slice reads an
| uninitialized byte). The specific discussion is whether
| the language requires "recursive validity for
| references", which would mean constructing a reference to
| an invalid value is "language UB" (your program is not
| well specified and the compiler is allowed to
| "miscompile" it) rather than "library UB" (your program
| is well-specified, but functions you call might not
| expect an uninitialized buffer and trigger language UB).
| See the discussion here: https://github.com/rust-
| lang/unsafe-code-guidelines/issues/3...
|
| Currently, the team is leaning in the direction of _not_
| requiring recursive validity for references. This would
| mean your code is not language UB as long as you can
| assume `set_len` and `copy_to_slice` never read from
| 'data`. However, it's still considered library UB, as
| this assumption is not documented or specified anywhere
| and is not guaranteed -- changes to safe code in your
| program or in the standard library can turn this into
| language UB, so by doing something like this you're
| writing fragile code that gives up a lot of Rust's safety
| by design.
| bombela wrote:
| Why not simply: let mut data =
| Vec::with_capacity(sz); data.extend(&buf[..sz]);
|
| Vec::extend extends a container from an iterable. A
| Vec/slice is iterable.
|
| And from the doc:
|
| > This implementation is specialized for slice iterators,
| where it uses copy_from_slice to append the entire slice
| at once.
|
| Of course this trivial example could also be written as:
| let mut data = buf.clone();
| uecker wrote:
| It is also not UB to read uninitialized values through a
| pointer in C for types that do not have non-value
| representations.
| codeflo wrote:
| I don't get the difference. In both C and Rust you can have
| pointers to uninitialized memory. In both languages, you
| can't use them except in very specific circumstances (which
| are AFAIK identical).
|
| There are two actual differences in this regard: C pointers
| are more ergonomic than Rust pointers. And Rust has an
| additional feature called references, which enable a lot
| more aggressive compiler optimizations, but which have the
| restriction that you can't have a reference to
| uninitialized memory.
| mk12 wrote:
| I agree with you. My point is that the additional feature
| (references) creates a new potential for UB that doesn't
| exist in C, and that justifies the "doesn't really ever
| occupy my mind as a problem" statement being criticized
| upthread. You can't compare C to Rust-without-references
| because no one writes Rust that way. It's not like
| C++-without-exceptions which is a legitimate subset that
| people use.
| o11c wrote:
| The reason _this_ particular UB doesn 't need mindspace for C
| programmers is because it's not even _meaningful_ to do
| anything with the parts of the buffer beyond the written
| length.
|
| Most other UBs related to datums that you _think_ you can do
| something with.
| usefulcat wrote:
| I suspect that the main reason it doesn't really occupy the
| author's mind is that even though it's _possible_ to misuse
| read(), it 's really not that hard to actually use it safely.
|
| It sounds like the more difficult problem here has to do with
| explaining to the compiler that read() is not being used
| unsafely.
| lhecker wrote:
| What I meant is that if I write a UTF8 --> UTF16 conversion
| function for my editor in C I can write
| size_t convert(state_t* state, const void* inp, void* out)
|
| This function now works with both initialized and
| uninitialized data in practice. It also is transparent over
| whether the output buffer is an `u8` (a byte buffer to write
| it out into a `File`) or `u16` (a buffer for then using the
| UTF16). I've never had to think about whether this doesn't
| work (in this particular context; let's ignore any alignment
| concerns for writes into `out` in this example) and I don't
| recall running into any issues writing such code in a long
| long time.
|
| If I write the equivalent code in Rust I may write
| fn convert(&mut self, inp: &[u8], out: &mut
| [MaybeUninit<u8>]) -> usize
|
| The problem is now obvious to me, but at least my intention
| is clear: "Come here! Give me your uninitialized arrays! I
| don't care!". But this is not the end of the problem, because
| writing this code is theoretically unsafe. If you have a
| `[u8]` slice for `out` you have to convert it to
| `[MaybeUninit<u8>]`, but then the function could
| theoretically write uninitialized data and that's UB isn't
| it? So now I have to think about this problem and write this
| instead: fn convert(&mut self, inp: &[u8],
| out: &mut [u8]) -> usize
|
| ...and that will also be unsafe, because now I have to
| convert my actual `[MaybeUninit<u8>]` buffer (for file
| writes) to `[u8]` for calls to this API.
|
| Long story short, this is a problem that occupies my mind
| when writing in Rust, but not in C. That doesn't mean that
| C's many unsafeties don't worry me, it just means that this
| _particular_ problem type described above doesn't come up as
| an issue in C code that I write.
|
| Edit: Also, what usefulcat said.
| ninkendo wrote:
| Why wouldn't you accept a &mut [MaybeUninit<T>] and return
| a &mut [u8], hiding the unsafe bits that transmute the
| underlying reference?
|
| Something like: fn convert<'i, 'o>(inp: &'i
| [u8], buf: &'o mut MaybeUninit<u8>) -> &'o mut [u8]
|
| (Honest question, actually... because the above may be
| impossible to write and I'm on my phone and can't try it.)
|
| Edit: it works: https://play.rust-
| lang.org/?version=stable&mode=debug&editio...
| lhecker wrote:
| That's a fair workaround for my specific example. But I
| believe it's possible to contrive a different example
| where such a solution would not be possible. Put
| differently, I only tried to convey the overall idea of
| what I think is a shortcoming in Rust at the moment.
|
| Edit: Also, I believe your code would fail my second
| section, as the `convert` function would have difficulty
| accepting a `[u8]` slice. Converting `[u8]` to
| `[MaybeUninit<u8>]` is not safe per se.
| ii41 wrote:
| I think this solves his problem. He said he wants a read
| function that turns the unsafe buffer into a safe buffer, and
| this API does that.
|
| IIRC it's not that hard to convince the compiler to give you a
| safe buffer from a MaybeUninit. However, this type has really
| lengthy docs and makes you question everything you do with it.
| Thinking through all this is painful but it's not like you
| don't have to it with C.
| jcranmer wrote:
| The basic problem with uninitialized buffers is that they
| effectively require write-only references to exist, and Rust's
| type system doesn't have (and doesn't easily support) write-
| only references, only read-only and read-write. MaybeUninit is
| a partial solution to the problem, but since it's a library
| solution and not a language solution, it suffers from a lack of
| integration with the language, e.g., getting MaybeUninit fields
| from a MaybeUninit struct is challenging.
|
| And the most aggravating part of all of this is that the most
| common use case for uninitialized memory (the scenario being
| talked about both in the article here and the discussion you
| quote) is actually pretty easy to have a reasonable, safe
| abstraction for, so the fact that the current options requires
| both use of unsafe code and also potentially faulty duplication
| of value calculations doesn't make for a fun experience. (Also,
| the I/O traits predate MaybeUninit, which means the most common
| place to want to work with uninitialized memory is one where
| you can't do it properly.)
| 90s_dev wrote:
| Then is the solution to have write-only be the default for
| muts, so that they start out write-only _at least_ , but can
| _also_ be made to be read-write either (a) in given
| circumstances of creation, or (b) after certain operations on
| them, or (c) if created by certain APIs?
| lhecker wrote:
| Abstracting away the `assume_init` is a great idea! I think I
| could use something like that for the editor. The only concern
| I have is that the `read` function is templated on the
| parameter type. I'd ideally _really_ prefer it if I didn't need
| two copies of the same function to switch over `[u8]` and
| `[MaybeUninit<u8>]` due to different return types. [^1] I guess
| the approach could be tuned to avoid this?
|
| Personally, I also like the simpler approach overall, compared
| to the `BorrowedBuf` trait, for the same reasons outlined in
| the article.
|
| While this possibly solves parts of pain points that I had,
| what I meant to write is that in an ideal world I could write
| Rust while mostly not thinking about this issue much, if at
| all. Even with this approach, I'd still need to decide whether
| my API needs to take a `[u8]` or a `Buffer`, just in the mere
| off-chance that a caller may want to pass an uninitialized
| array further up in the call chain. This then requires making
| the call path generic for the buffer parameter which may end up
| duplicating any of the functions along the path, even though
| that's not really my intention by marking it as `Buffer`.
|
| I think if there was a way to modify Rust so we can boldly
| state in writing "You may cast a `[MaybeUninit<T>]` into a
| `[T]` and pass it into a call _if_ you're absolutely certain
| that nothing reads from the slice", it would already go a long
| way. It may not make this more comfortable yet, but it would
| definitely take off a large part of my worries when writing
| such unsafe casts. That's basically what I meant with "occupy
| my mind": It's not that I wouldn't think about it at all,
| rather it just wouldn't be a larger concern for me anymore, for
| code where I know for sure that this requirement is fulfilled
| (i.e. similar to how I know it when writing equivalent C code).
|
| Edit: jcranmer's suggestion of write-only references would
| solve this, I think?
| https://news.ycombinator.com/item?id=44048450
|
| [^1]: This is of course not a problem for a simple `read`
| syscall, but may be an issue for more complex functions, e.g.
| the UTF8 <> UTF16 converter API I suggested elsewhere in this
| thread, particularly if it's accelerated, the way simdutf is.
| vgatherps wrote:
| I wish that there was a useful "freeze" intrinsic exposed, even
| if only for primitive types and not for generic user types, where
| the values of the frozen region become unspecified instead of
| undefined. I believe llvm has one now?
|
| Iirc the work on safe transmute also involves a sort of "any bit
| pattern" trait?
|
| I've also dealt with pain implementing similar interfaces in
| Rust, and it really feels like you end up jumping through a ton
| of hoops (and in some of my cases, hurting performance) all to
| satisfy the abstract machine, at no benefit to programmer or
| application. It's really a case where the abstract machine cart
| is leading the horse
| CJefferson wrote:
| I agree, I'd go further and say I wonder why primitive types
| aren't "frozen" by default.
|
| I totally understand not wanting to promise things get zeroed,
| but I don't really understand why full UB, instead of just
| "they have whatever value is initially in memory / the register
| / the compiler chose" is so much better.
|
| Has anyone ever done a performance comparison between UB and
| freezing I wonder? I can't find one.
| wrs wrote:
| That assumes the compiler reserves one continuous place for
| the value, which isn't always true (hardly ever true in the
| case of registers). If the compiler is required to make all
| code paths result in the _same_ uninitialized value, that can
| limit code generation options, which might reduce performance
| (and performance is the whole reason to use uninitialized
| values!).
|
| Also, an uninitialized value might be in a memory page that
| gets reclaimed and then mapped in again, in which case
| (because it hasn't been written to) the OS doesn't guarantee
| it will have the same value the second time. There was
| recently a bug discovered in one of the few algorithms that
| uses uninitialized values, because of this effect.
| CJefferson wrote:
| But, I wonder how much it would reduce performance, if we
| only have to pick a value the first time the memory is
| read?
|
| I would imagine there isn't that many cases where we are
| reading uninitalised memory and counting on that reading
| not saving a value. It would happen when reading in 8-byte
| blocks for alignment, but does it happen that much
| elsewhere?
| ablob wrote:
| if you pick a value you have to store it, and if you have
| to store it it might spill into memory when register
| allocation fails. Moving from register-only to stack/heap
| usage easily slows down your program by an order of
| magnitude or two. If this is in a hot path, which I'd
| argue it is since using uninitialized values seems
| senseless otherwise, it might have a big impact.
|
| The only way to really know is to test this. Compilers
| and their optimizations depend on a lot of things. Even
| the order and layout of instructions can matter due to
| the instruction cache. You can always go and make the
| guarantee later on, but undoing it would be impossible.
| dathinab wrote:
| > same uninitialized value, that can limit code generation
| options
|
| it pretty much requires the compiler to initialize all
| values when they first "appear"
|
| except that this is impossible and outright hazardous if
| pointers are involved
|
| But doable for a small subset like e.g.
|
| - stack values (but would inhibit optimizations,
| potentially pretty badly)
|
| - some allocations e.g. I/O buffers, (except C alloc has no
| idea that you are allocating an I/O buffer)
| dooglius wrote:
| > If the compiler is required to make all code paths result
| in the same uninitialized value, that can limit code
| generation options
|
| Can you provide (on say x86_64) an example of this, _other
| than_ the case where the compiler prunes cases based on
| characterizing certain paths as UB? In other words, a case
| where "an uninitialized value is well-defined but can be
| different on each read" allows more performance
| optimization than "the value will be the same on each
| read".
|
| > Also, an uninitialized value might be in a memory page
| that gets reclaimed and then mapped in again, in which case
| (because it hasn't been written to) the OS doesn't
| guarantee it will have the same value the second time.
| There was recently a bug discovered in one of the few
| algorithms that uses uninitialized values, because of this
| effect.
|
| This does not sound correct to me, at least for Linux
| (assuming one isn't directly requesting such behavior with
| madvise or something). Do you have more information?
| vgatherps wrote:
| Uninitialized memory being UB isn't an insane default imo
| (although it makes masked simd hard), nor is most UB. But the
| lack of escape hatches can be frustrating
| adastra22 wrote:
| Anything being UB is insane to me...
| IshKebab wrote:
| Nah it makes some sense for portability between
| architectures. Or at least it _did_ back when C was
| invented and there were some wild architectures out
| there.
|
| And it definitely _does_ allow some optimisation. But
| probably nothing significant on modern out-of-order
| machines.
| dathinab wrote:
| > there were some wild architectures out there.
|
| what is out there is still pretty wield
|
| just slightly less
|
| > probably nothing significant on modern out-of-order
| machines.
|
| having no UB at all will kill a lot of optimizations
| still relevant today (and won't match anymore to hardware
| as some UB is on hardware level)
|
| out of order machines aren't magically fixing that, just
| makes some less optimized code work better, but not all
|
| and a lot of low energy/cheap hardware does have no or
| very very limited out of order capabilities so it's still
| very relevant and likely will stay very relevant for a
| very long time
| dathinab wrote:
| only until you get deeper into how the hardware actually
| work (and OS to some degree)
|
| and realize sometimes the UB is even in the hardware
| registers
|
| and that the same logical memory address might have 5
| different values in hardware at the same time without you
| having a bug
|
| and other fun like that
|
| so the insanity is reality not the compiler
|
| (through IMHO in C and especially C++ the insanity is how
| easily you might accidentally run into UB without doing
| any fancy trickery but just dumb not hot every day code)
| uecker wrote:
| I do not find it so easy to accidentally run into UB in C
| if you follow some basic rules. The exceptions are null
| pointer dereferences, out-of-bounds accesses for arrays,
| and signed overflow, all those can be turned into run-
| time traps. The rules include no pointer arithmetic, no
| type casts, and having some ownership strategy. None of
| those is difficult to implement and where exceptions are
| made, one should treat it carefully similar to using
| "unsafe" in Rust.
| dooglius wrote:
| There is no UB in hardware registers or physical DRAM, I
| don't think you actually have familiarity with how the
| hardware works if you make this claim. (Or perhaps you
| aren't familiar with how crazy "UB" in the sense of the
| ISO C documentation is)
|
| EDIT: one could see "apparent" violation of memory
| consistency if say the cache subsystem or memory
| controller were misconfigured, however this would require
| both (1) you are running in kernel mode, not user-space
| (2) you have a bug, so GP's claim is not supported that
| bug-free code could encounter such a state.
| addaon wrote:
| > There is no UB in hardware registers or physical DRAM
|
| This seems very sensitive to specific definitions that
| others might not share. DRAM is provided with a spec
| sheet that defines its behavior (if you write to an
| address, you'll read back the same value from the same
| address in the future) under certain conditions. If you
| violate those conditions, the behavior is... undefined.
| If you operate DRAM with the wrong refresh timing, or
| temperature, or voltage, or ionizing radiation level, you
| may see strange behavior. Even non-local behavior, where
| the value read from one cell depends on other cells
| (RowHammer). How is this not UB?
| dooglius wrote:
| I've edited my claim to be a bit more clear, however in
| the context of parent's claim we are talking about bug-
| free code on an non-buggy physical processor, and I think
| implicitly we are talking about user-mode code where one
| does not have the ability to alter any DRAM timing
| configuration registers anyway.
| adastra22 wrote:
| If a C program accesses uninitialized memory (UB), it is
| perfectly compliant with the language spec for the
| compiler to reformat your hard drive and replace your OS
| code with crypto mining.
|
| I'm not exaggerating by a legalistic interpretation, and
| I'm only slightly exaggerating in practice. UB can do
| some really weird, unintuitive stuff in practice on real
| hardware:
|
| https://mohitmv.github.io/blog/Shocking-Undefined-
| Behaviour-...
|
| The point is that this extreme UB should never happen. It
| was a choice of the compiler implementors, and rather
| than fix this, they allowed the escape hatch of UB in the
| spec. It would be more sensible for the compiler to say
| that, e.g., accessing uninitialized memory results in a
| nonspecified value, or even possibly multiple different
| nonspecified values if accessed from different threads.
| That captures what we expect to happen, but would be
| (according to C language spec lawyers) defined behavior.
|
| In practice, it would mean that compliant compilers will
| ensure that any situation in which uninitialized memory
| could be accessed would not result in weird edge case
| page faults on certain architectures or whatever that
| could in fact lead to wacky UB situations.
|
| This is not an unreasonable ask.
| addaon wrote:
| > This is not an unreasonable ask.
|
| But isn't this exactly parallel to the Rowhammer case in
| DRAM? When operating at the edge of the spec, the
| behavior of DRAM becomes undefined. (And, of course, one
| challenge with Rowhammer was about /which/ edge of the
| spec this happened on.) In this case, writing one
| physical address altered the contents of other physical
| addresses. This is "really weird, unintuitive stuff ...
| on real hardware." And of course we can (and do) ask DRAM
| vendors to not take advantage of this undefined behavior;
| but they do so as an optimization, allowing slightly
| smaller and more closely spaced DRAM cells, and thus
| higher density DRAM dice for the same price. Just like
| it's possible to work with a language with fully-defined
| semantics at the cost of performance, it's possible to
| buy DRAM with much wider specifications and more clearly
| defined behavior up to the edges of those
| specifications... at the cost of performance.
|
| Extreme UB in both hardware and software is a choice of
| priorities. You may favor giving up performance
| capabilities to achieve a more understandable system (so
| do I! I do most of my work on in-order lockstep
| processors with on-die ECC SRAM to maximize
| understandability of failure modes), but the market as a
| whole clearly does not, in both hardware and software.
| adastra22 wrote:
| Legitimate bugs in hardware is probably out of scope for
| compilers. That would be an unreasonable ask, yes. But I
| believe it should be the job of the compiler to make sure
| that a correctly executed program on a reference machine
| strictly adhering to the architecture specs should not
| result in UB.
|
| I'm not saying that a computer architecture should be UB-
| free. That would be awesome if it could be done, but in
| practice probably a bridge too far. But a compiler should
| map high-level directives into low-level implementations
| on a specific architecture using constructs that do not
| result in UB. _This_ is not too much to ask.
|
| A compiler can't reasonably protect you from rowhammer
| attacks. But it should guarantee that, barring hardware
| errors, accessing uninitialized memory has no effect
| other than something sensible like returning unspecified
| contents, or causing a memory access exception, or
| whatever. It should be defined up front what the
| _behavior_ is, even if some of the runtime values are
| unpredictable.
|
| As a more concrete example, most languages these days
| clearly define what happens in signed integer overflow:
| the thing that you expect to happen in any two's
| complement machine (char)127 + (char)1 == -128. C treats
| this as undefined behavior, and as mentioned in my link
| above that can cause what _should_ be a finite loop (with
| or without overflow) to compile as an infinite loop. This
| "optimization" step by the compiler should never have
| happened. C resists changing this because C compilers
| exist for non-twos-complement architectures where the
| behavior would be different. IMHO the correct approach
| would be to require THOSE weird esoteric architectures to
| compile in extra overflow checks (possibly disabled with
| opt-in compiler flags that explicitly violate the
| standard), rather than burden every C developer
| everywhere with the mess that is signed arithmetic
| overflow UB.
|
| It's a matter of expectations. Any but the most junior
| programmers expect that signed integer overflow will not
| be portable. That's fine. But they expect a sensible
| result (e.g. wrap around on two's complement machines),
| even if it is non-portable. They don't expect the
| compiler to silently and sneakily change the logic of
| their program to something fundamentally different,
| because UB means the compiler can do whatever tf it
| wants.
| addaon wrote:
| > Legitimate bugs in hardware is probably out of scope
| for compilers.
|
| But that's exactly the point. The "bug" of RowHammer was
| that it occurred slightly on the "allowed" side of the
| envelope, at acceptably-low refresh rates. The "UB" of
| RowHammer and a hundred other observable effects is that,
| on the "disallowed" side of the envelope, the behavior is
| undefined. The system designer gets to choose at what
| probability they are on each side of the envelope, and
| the trade-offs are very much optimization opportunities.
|
| Writing software in C that may exhibit undefined behavior
| is exactly this -- it's choosing, as a software engineer,
| to be on the far side of the specification envelope. In
| exchange, you get access to several powerful
| optimizations, some at the compiler level, and some at
| the career level (if you think that not needing to learn
| to use understand your language properly is at time
| optimization, at least).
| Aurornis wrote:
| > There is no UB in hardware registers
|
| There most definitely is.
|
| In the ARM documentation this is referred to as
| "UNPREDICTABLE". The outcome is not defined. It may work.
| It may not. It may put garbage data in a register.
| dooglius wrote:
| I've edited/clarified the claim above. Parent did say
| "without you having a bug" and entering an UNPREDICTABLE
| state would be buggy code. Also, "maybe puts garbage data
| in a register" is not UB, it's a much more reasonable
| thing in UB (the definition of UNPREDICTABLE prior to
| ARMv8 does seem to allow for UB, however). I don't
| believe that such an UNPREDICTABLE state is reachable
| from user-space/unprivileged code (or else it would
| violate the chip's security properties) -- but if I'm
| wrong on that I'd be interested in an example.
| adastra22 wrote:
| Here are just a few examples:
|
| https://mohitmv.github.io/blog/Shocking-Undefined-
| Behaviour-...
| dooglius wrote:
| Those are examples regarding UB in the C++ language not
| the UNPREDICTABLE state in the ARM ISA which is what I
| asked for examples of.
| jcranmer wrote:
| How would you implement integer-to-pointer conversions
| without UB?
| adastra22 wrote:
| What is UB about integer-to-pointer conversions?
| jcranmer wrote:
| Plenty of things! The resulting pointer may not be
| pointing to an object that is currently live, for
| example. It may not even be pointing to an object that
| makes any sense in the language's object model. It might
| be pointing to a return address on the stack, for
| example. Or a constant in the constant pool. Or a saved
| register in the middle of a computation that corresponds
| to no variables in the original program.
|
| In short, the moment you enable integer-to-pointer
| conversions (assuming your target has a flat address
| space), you create pointer provenance problems whose only
| resolution is that some things have to be UB.
| adastra22 wrote:
| I don't think any of those are undefined behavior in the
| strict sense in which the term is defined in the C/C++
| standards. Pointer casts are defined behavior. I believe
| the things you point to are either implementation-defined
| or unspecified, which is different from UB.
|
| It may seem nitpicky, but the downside of relying on
| implementation defined or unspecified behavior is largely
| boxed and contained. E.g you might get a memory access
| error. UB is, in principle, completely unlimited in
| downside. And because of that, it often interacts badly
| with optimization passes, resulting in very strange bugs.
| pcwalton wrote:
| jcranmer is correct and pointer provenance-related issues
| are not "boxed and contained". Start here:
| https://www.ralfj.de/blog/2020/12/14/provenance.html
| dathinab wrote:
| > why primitive types aren't "frozen" by default.
|
| it kills _a lot_ of optimizations leading to problematic
| perf. degredation
|
| TL;DR: always freezing I/O buffers => yes no issues (in
| general); freezing all primitives => perf problem
|
| (at lest in practice in theory many might still be possible
| but with a way higher analysis compute cost (like exponential
| higher) and potentially needing more high level information
| (so bad luck C)).
|
| still for I/O buffers of primitive enough types `frozen` is
| basically always just fine (I also vaguely remember some
| discussion about some people more involved into rust core
| development to probably wanting to add some functionality
| like that, so it might still happen).
|
| To illustrate why frozen I/O buffers are just fin: Some
| systems do already anyway always (zero or rand) initialize
| all their I/O buffers. And a lot of systems reuse I/O
| buffers, they init them once on startup and then just
| continuously re-use them. And some OS setups do (zero or
| rand) initialize all OS memory allocations (through that is
| for the OS granting more memory to your in process memory
| allocator, not for every lang specific alloc call, and it
| doesn't remove UB for stack or register values at all (nor
| for various stations related to heap values either)).
|
| So doing much more "costly" things then just freezing them is
| pretty much normal for I/O buffers.
|
| Through as mentioned, sometimes things are not frozen
| undefined on a hardware level (things like every read might
| return different values). It's a bit of a niche issue you
| probably won't run into wrt. I/O buffers and I'm not sure how
| common it is on modern hardware, but still a thing.
|
| But freezing primitives which majorly affect control flows is
| both making some optimizations impossible and other much
| harder to compute/check/find, potentially to a point where
| it's not viable anymore.
|
| This can involve (as in freezing can prevent) some forms of
| dead code elimination, some forms of inlining+unrolling+const
| propagation etc.. This is mostly (but not exclusively) for
| micro optimizations but micro optimizations which sum up and
| accumulate leading to (potentially but not always) major
| performance regressions. Frozen also has some subtle
| interactions with floats and their different NaN values (can
| be a problem especially wrt. signaling NaNs).
|
| Through I'm wondering if a different C/C++ where arrays of
| primitives are always treated as frozen (and no signaling
| NaNs) would have worked just fine without any noticeable
| perf. drawback. And if so, if rust should adopt this...
| Arnavion wrote:
| >I've also dealt with pain implementing similar interfaces in
| Rust, and it really feels like you end up jumping through a ton
| of hoops (and in some of my cases, hurting performance) all to
| satisfy the abstract machine, at no benefit to programmer or
| application.
|
| I've implemented what TFA calls the "double cursor" design for
| buffers at $dayjob, ie an underlying (ref-counted)
| [MaybeUninit<u8>] with two indices to track the filled,
| initialized and unfilled regions, plus API to split the buffer
| into two non-overlapping handles, etc. It certainly required
| wrangling with UnsafeCell in non-trivial ways to make miri
| happy, but it doesn't have any less performance than the
| equivalent C code that just dealt with uint8_t* would've had.
| lilyball wrote:
| What is the reason for explicitly tracking the initialized-
| but-unfilled portion? AIUI there's no harm in treating
| initialized bytes as uninitialized since you're working with
| u8, so what do you actually gain by knowing the initialized
| portion? I mean, at an API level you gain the ability to
| return a &mut [u8] for the initialized portion, but
| presumably anyone actually trying to write to this buffer
| either wants to copy in from a &[u8] or write to a &mut
| [MaybeUninit<u8>].
| jcranmer wrote:
| This is already discussed for Rust: https://github.com/rust-
| lang/rfcs/pull/3605. TL;DR: it's not as easy as it looks to
| just add "freeze."
| orlp wrote:
| It is as easy as it looks to add `freeze`. That is, value-
| based `freeze`, reference-based `freeze` while seemingly
| reasonable is broken because of MADV_FREE.
|
| Some people simply aren't comfortable with it.
|
| Currently sound Rust code does not depend on the value of
| uninitialized memory whatsoever. Adding `freeze` means that
| it can. A vulnerability similar to heartbleed to expose
| secrets from free'd memory is impossible in sound Rust code
| without `freeze`, but theoretically possible with `freeze`.
|
| Whether you consider this a realistic issue or not likely
| determines your stance on `freeze`. I personally don't think
| it's a big deal and have several algorithms which are
| fundamentally being slowed down by the lack of `freeze`, so
| I'd love it if we added it.
| dathinab wrote:
| also to be realistic in a lot of practical situations I/O
| buffers are reused so at least for the I/O buffer use-case
| it can be very viable (perf wise, in most use cases) to
| just zero or rand initialize it once on alloc and then
| treat it as frozen in all repeated usages (through it does
| open the issue of bug now potentially leaking previous
| content of the I/O buffer).
|
| but I guess this isn't just about I/O buffers ;)
| LegionMammal978 wrote:
| > Currently sound Rust code does not depend on the value of
| uninitialized memory whatsoever. Adding `freeze` means that
| it can.
|
| Arguably, the existence of "asm!() freeze" has already
| broken this idea. Of course, you nominally don't get any
| guarantees about the stability of data that asm!() code
| reads from uninitialized bytes, yet you can do it
| nonetheless.
|
| And it's not like it's practical to say "asm!() code is
| always unsound if it uses uninitialized bytes like they're
| numbers!", since lots of it does useful stuff like
| communicating with the kernel with structs that get
| serialized, and it can also open up interfaces like mmap()
| which translate possibly-uninitialized virtual-memory bytes
| into definite kernel bytes.
|
| Not to mention /proc/self/mem and similar kernel-provided
| debugging utilities that can peek into memory as serialized
| data.
| jcranmer wrote:
| > Some people simply aren't comfortable with it.
|
| Some people--especially when those people are closest to
| the workings of the operational semantics--not being
| comfortable is a sign that it is in fact harder than it
| looks.
|
| The problems with "freeze" are in the same vein as integer-
| to-pointer semantics: it's a change which turns out to have
| implications for things not closely related to the
| operation itself, giving it a spooky-action-at-a-distance
| effect that is hard to tame.
|
| The deeper issue is that, while there is clearly a use for
| some sort of "garbage value" semantics in a high-level
| language (that supports things like uninitialized I/O
| buffers, excessive reads for vectorization, padding bytes
| within structures), it's not clear which of the subtly
| different variants of garbage value works the best for all
| of the use cases.
| JoshTriplett wrote:
| This isn't just about the abstract machine. This is also about
| making it hard to end up _using_ uninitialized memory, which is
| a security hole.
|
| Abstractions like ReadBuf allow safe code to efficiently work
| with uninitialized buffers without risking exposure of random
| memory contents.
| leoh wrote:
| Just dropping to C for a smallish segment of a rust program kind
| of makes sense if you want to eke out performance here, no?
| bjackman wrote:
| You can always just use unsafe. This is about how to allow code
| to do this without unsafe blocks.
| Arnavion wrote:
| It does, though even for a small segment you end up needing a
| lot of boilerplate - a dependency on the cc crate, a build.rs
| that invokes the cc crate, extern declarations to tell the Rust
| caller about the C function, potentially some bindgen if your
| small segment is not so small ... and you still end up having
| to do some amount of thunking between & / * / MaybeUninit
| because of that anyway. So if there is a "pure Rust" way to do
| it with `unsafe`, writing that is often easier. The pure Rust
| impl also has the advantage that you can validate it with Miri,
| unlike the C impl case because Miri cannot emulate arbitrary C
| code.
| IshKebab wrote:
| Yeah but the C toolchain is a huge pain, and makes things like
| cross-compiling and compiling to WASM harder. It's really nice
| if you can keep your program pure Rust.
|
| Go has similar characteristics.
| nottorp wrote:
| > compiling to WASM
|
| I thought Rust was supposed to be a systems language?
| IshKebab wrote:
| It is. And you can compile it to WASM.
| treyd wrote:
| Go is in a much worse situation here since the toolchain
| doesn't give you _any_ help with C dependencies.
|
| In Rust you can mostly just wrap the C library as a crate and
| the end developer doesn't need to care very much. If you want
| to use their system headers then you need to let them know,
| but that's not an issue if the headers are bundled. Cargo
| knows how to absorbs any C-built object files into the final
| binary and it just works.
|
| In Go, in _every scenario_ , the end developer needs to go
| through the effort of managing all the necessary the cgo
| calls when any of their dependencies use C libraries, the
| whole way up the dependency tree. It's really really not
| supported.
| briansm wrote:
| Pardon my ignorance, but I thought the whole point of Rust was to
| be a 'safe' modern alternative to C, so all new buffers would be
| zero'd at a neglible-these-days cost. Why is rust half-assing
| this?
| vlovich123 wrote:
| It's not. That is the case. But in cases where "negligible-
| these-days" isn't quite negligible enough, this still matters
| and unsafe + MaybeUninit is the escape hatch to accomplish it.
| Arnavion wrote:
| Also not every type has a valid "all zeroes" value in the
| first place.
| briansm wrote:
| Yuck. In my mind, 'using C and not using Rust in the first
| place' is the escape hatch and Rust shouldn't even go there.
| Jeez, what a mess.
| JoshTriplett wrote:
| This is why we're spending substantial energy building
| better abstractions that don't require you to write any
| unsafe code.
| vlovich123 wrote:
| Rust provides all the safety guarantees of managed
| languages with none of the performance drawbacks. It's
| precisely intended to replace C/C++ because the unsafe
| parts of Rust are used very sparingly and result in
| significantly fewer bugs and security vulnerabilities.
|
| Safe abstractions for dealing with uninitialized memory
| efficiently are important in very niche scenarios to get
| optimal code out of the compiler and for reducing the
| ability to make a mistake when writing such code.
|
| Reaching for C to do this is an emotional overreaction
| instead of calmly dealing with a small corner case that
| already has workarounds even if it does involve using
| unsafe
| cozzyd wrote:
| what if your buffer is 64 GiB? (ok, in practice it will be
| zero'd on demand by the OS but still)
| josephg wrote:
| I'd love to know the actual performance impact of zeroing out
| memory. I bet the performance cost of zeroing out memory
| ahead of time is negligible in almost all programs.
| cozzyd wrote:
| Here is a really dumb example: $ cat
| bigbuf.c #include <stdlib.h> #include
| <stdio.h> #include <string.h> #define
| DEFINITELY_BIG_ENOUGH 2U*1024*1024*1024 int main(int
| nargs, char ** args) { char *
| definitely_big_enough = malloc(DEFINITELY_BIG_ENOUGH);
| if (nargs > 1) {
| memset(definitely_big_enough,0, DEFINITELY_BIG_ENOUGH);
| } sprintf(definitely_big_enough,"%s",args[0]);
| return 0; } $ /usr/bin/time ./bigbuf
| 0.00user 0.00system 0:00.00elapsed 100%CPU
| (0avgtext+0avgdata 1356maxresident)k 0inputs+0outputs
| (0major+80minor)pagefaults 0swaps $
| /usr/bin/time ./bigbuf 1 0.05user 1.25system
| 0:01.31elapsed 99%CPU (0avgtext+0avgdata
| 2098468maxresident)k 0inputs+0outputs
| (0major+524369minor)pagefaults 0swaps
|
| YMMV on different operating systems. Of course this is a
| program only an idiot would write, but things like caches
| are often significantly bigger than the median case,
| especially on Linux where you know there is overcommit.
| Arnavion wrote:
| A non-idiot would use calloc(DEFINITELY_BIG_ENOUGH), and
| that will likely erase the difference because the impl
| will be able to rely on mmap(ANONYMOUS) creating zero
| pages for such a large allocation. A more realistic test
| would be to have a large number of small allocations that
| get calloc'd and then free'd repeatedly, because a)
| free'ing a small allocation will not free the underlying
| page and thus reallocation will not be able to rely on it
| already being zero, and b) zeroing small allocations
| doesn't amortize the cost of zeroing as well as zeroing
| large allocations does.
| delamon wrote:
| c) SIMD won't be as helpful when you are zeroing small
| buffers of non-round sizes
| Arnavion wrote:
| That's what (b) is about.
| briansm wrote:
| Surely the point of Rust is 'safety at the price of
| performance' and if extra performance is required, don't use
| Rust. Don't bodge the language to accommodate!
| int_19h wrote:
| It is not. If it were, it wouldn't even have raw pointers,
| for example.
| briansm wrote:
| and yet an article about a 'safe' language has code
| examples full of 'unsafe'... wut.
| panstromek wrote:
| `unsafe` is just a barrier between what compiler proves
| and what programmer proves.
|
| People should really read more on safety semantics in
| Rust before making comments like this, it's quite
| annoying to bump into surface level misunderstandings
| everytime Rust is mentioned somewhere.
| hmry wrote:
| In normal safe code, the compiler ensures your code has
| no UB. In an unsafe block, the programmer ensures the
| code has no UB.
|
| Safe abstractions are created out of unsafe operations by
| making sure it's impossible to violate the operations'
| preconditions. Either by checking at runtime and
| returning an error or aborting the program if they're
| violated, or by using the type system and borrow checker
| to verify them at compile time (writing the code such
| that any program that could violate the preconditions
| must have a type error.)
|
| If Rust didn't have unsafe, the only way to access the
| underlying unsafe operations would be dropping down to
| C/C++/Assembly, or hardcoding them in the compiler. This
| is what other languages do, and it's ergonomically worse
| because the barrier to adding a whole new language and
| build system to your project is quite high.
| panstromek wrote:
| The point of rust is explicitly not 'safety at the price of
| performance', quite the opposite. The whole point was to
| create language where safety doesn't cost performance like
| it does in most other languages.
| arlort wrote:
| The cost might not be negligible for everyone?
|
| Rust is being used and is designed to be able to be used
| everywhere from top of the line PCs, to servers to
| microcontrollers to virtual machines in the browser.
|
| Not all tradeoffs are acceptable to everyone all of the time
| mrkeen wrote:
| Other languages can easily achieve this kind of safety. What
| makes Rust different is that it tries to provide this level of
| safety without doing extra work at runtime (because otherwise
| people will put it in the same pile as Java/C#, and continue
| using C/C++ for speed).
| sqrt_1 wrote:
| Related to unspecified vs undefined. I recall some C code was
| trying to be tricky and read from just allocated memory.
| Something like:
|
| int* ptr = malloc(size); if(ptr[offset] == 0) { }
|
| The code was assuming that the value in an allocated buffer did
| not change.
|
| However, it was pointed out in review that it could change with
| these steps:
|
| 1) The malloc allocates from a new memory page. This page is
| often not mapped to a physical page until written to.
|
| 2) The reads just return the default (often 0 value) as the page
| is not mapped.
|
| 3) Another allocation is made that is written to the same page.
| This maps the page to physical memory which then changes the
| value of the original allocation.
| Arnavion wrote:
| A read from an unmapped page producing a different value than
| reading from that same page after it's mapped is an OS bug (*).
| If this was an already allocated page that had something
| written to it, reading from it would page it back in and then
| produce the actual content. If this was a new page and the OS
| contract was to provide zeroed pages, both the read before it
| was mapped and the read after it was mapped would produce zero.
|
| What could happen is that the UB in that code could result in
| it being compiled in a way that makes the comparison non-
| deterministic.
|
| (*): ... or alternatively, we're not talking about regular
| userspace program but a higher privilege layer that is doing
| direct unpaged access, but I assume that's not the case since
| you're talking about malloc.
| sqrt_1 wrote:
| It was from C++con 2016 - Facebook take on small strings
| https://www.youtube.com/watch?v=kPR8h4-qZdk&t=1343s I believe
| it is about a page that was conditionally returned to the
| kernel.
| Arnavion wrote:
| The speaker was mistaken / misspoke.
|
| The closest thing to "conditionally returned to the kernel"
| is if the page had been given to madvise(MADV_FREE), but
| that would still not have the behavior they're talking
| about. Reading and writing would still produce the same
| content, either the original page content because the
| kernel hasn't released the page yet, or zero because the
| kernel has already released the page. Even if the order of
| operations is read -> kernel frees -> write, then that
| still doesn't match their story, because the read will
| produce the original page content, not zero.
|
| That said, the code they're talking about is different from
| yours in that their code is specifically doing an out-of-
| bounds read. (They said "If you happen to allocate a string
| that's 128 bytes, and malloc happens to return an address
| to you that's 128 bytes away from the end of the page,
| you'll write the 128 bytes and the null terminator will be
| the first byte on the next page. So they're very clearly
| talking about the \0 being outside the allocation.)
|
| So it is absolutely possible to have this setup: the
| string's allocation happens to be followed by a different
| allocation that is currently 0 -> the `data[size()] !=
| '\0'` check is performed and succeeds -> `data` is returned
| to the caller -> whoever owns that following allocation
| writes a non-zero value to the first byte -> whoever called
| `c_str()` will now run off the end of the 128B string. This
| doesn't have anything to do with pages; it can happen
| within the bounds of a single page. It is also such an
| obvious out-of-bounds bug that it boggles my mind that it
| passed any sort of code review and required some sort of
| graybeard to point out.
| sqrt_1 wrote:
| I don't believe they are allocating 128 bytes, or
| accessing out of bounds memory.
|
| He explicitly states 128byte filename allocates 129
| bytes.
| https://www.youtube.com/watch?v=kPR8h4-qZdk&t=1417s
| Arnavion wrote:
| In that case the bug he described simply does not exist.
| sqrt_1 wrote:
| This is well outside my expertise, but some discussion
| happened at the time https://www.reddit.com/r/programming
| /comments/56xxmb/the_str...
|
| Some people suggest that maybe Facebook runs with
| MAP_UNINITIALIZED
| Arnavion wrote:
| Unfortunately this hypothesis is also wrong.
| MAP_UNINITIALIZED can only be enabled in the kernel when
| there is no MMU, and in that case the page will already
| be in physical memory, so the very first pointer
| dereference will read the correct byte, not a fake zero
| because it's "uninitialized".
| mleonhard wrote:
| > without doing anything hugely inefficient, such as initializing
| the full buffer
|
| Is this so inefficient? If your code is very sensitive to IO
| throughput, then it seems preferable to re-use buffers and pay
| the initialization once at startup.
|
| Some years ago, I needed a buffer like this and one didn't exist,
| so I wrote one: https://crates.io/crates/fixed-buffer . I like
| that it's a plain struct with no type parameters.
| MaulingMonkey wrote:
| > Is this so inefficient?
|
| It can be. If you have large buffers (tuned for throughput)
| that end up fulfilling lots of small requests for whatever
| reason, for example. And there's always the occasional article
| when someone rediscovers that replacing malloc + memset with
| calloc can have massive performance savings thanks to zeroing
| by the OS only occuring on first page fault (if it ever
| occurs), instead of an O(N) operation on the whole buffer up
| front.
|
| Which, if in the wrong loop, can quickly balloon from O(N) to
| O(scary).
|
| https://github.com/PSeitz/lz4_flex/issues/147
|
| https://github.com/rust-lang/rust/issues/117545
|
| If I'm reading that log-log plot right, that looks like a
| significantly worse than 100x slowdown on 1GB data sets.
| Avoiding init isn't the only solution, of course, but it was
| _a_ solution.
|
| > then it seems preferable to re-use buffers
|
| Buffer reuse may be an option, but in code with complicated
| buffer ownership (e.g. transfering between threads, with the
| thread of origination not necessarily sticking around, etc.),
| one of the sanest methods of re-use may be to return said
| buffer to the allocator, or even OS.
|
| > and pay the initialization once at startup.
|
| Possibly a great option for long lived processes, possibly a
| terrible one for something you spawn via xargs.
| pornel wrote:
| Couple of things that are commonly misunderstood/unappreciated
| about this:
|
| * Uninitialized bytes are not just some garbage random values,
| they're a safety risk. Heartbleed merely exposed unitialized
| buffers. Uninit buffers can contain secrets, keys, and pointers
| that help defeat ASLR and other mitigations. As usual, Rust sets
| the bar higher than "just be careful not to have this bug", and
| therefore the safe Rust subset requires making uninit
| _impossible_ to read.
|
| * Rust-the-language can already use uninitialized buffers
| efficiently. The main issue here is that the Rust standard
| library doesn't have APIs for I/O using custom uninitialized
| buffers (only for the built-in Vec, in a limited way). These are
| just musings how to design APIs for custom buffers to make them
| the most useful, ergonomic, and interoperable. It's a debate,
| because it could be done in several ways, with or without
| additions to the language.
| ajross wrote:
| > Uninitialized bytes are not just some garbage random values,
| they're a safety risk.
|
| Only when _read_. _Writing_ to "uninitialized" memory[1] and
| reading it back is provably secure[2], but doesn't work in safe
| Rust as it stands. The linked article is a proposal to address
| that via some extra complexity that I guess sounds worth it.
|
| [1] e.g. using it as the target of a read() syscall
|
| [2] Because it's obviously isomorphic to "initialization"
| pornel wrote:
| Obviously, initialized memory isn't an uninitialized memory
| any more.
|
| There are fun edge cases here. Writing to memory through
| `&mut T` makes it initialized for T, but its padding bytes
| become de-initialized (that's because the write can be a
| memcpy that also copies the padding bytes from a source that
| never initialized them).
| lilyball wrote:
| Note that if you have a `&mut T` then the memory must
| _already_ be initialized for T, so writing to that pointer
| doesn 't initialize anything new (although as you say it
| can deinitialize bytes, but that only matters if you use
| transmute or pointer casting to get access to those padding
| bytes somehow).
___________________________________________________________________
(page generated 2025-05-21 23:02 UTC)