[HN Gopher] Safety and Soundness in Rust
___________________________________________________________________
Safety and Soundness in Rust
Author : oconnor663
Score : 64 points
Date : 2023-03-05 19:18 UTC (3 hours ago)
(HTM) web link (jacko.io)
(TXT) w3m dump (jacko.io)
| pornel wrote:
| I really like that in Rust the API safety is defined by the
| language, not by each library individually.
|
| There are no gotchas to be found in small print in the library
| documentation, no disclaimers that the code is meant only for
| single-threaded programs, or haggling over how rare the edge
| cases are. If it doesn't say `unsafe`, it has to meet Rust's
| universal requirements for safety.
| satvikpendem wrote:
| You could just use cargo-geiger and #![forbid(unsafe_code)] in
| order to only have safe code. We use this in our CI/CD in order
| to ensure that no unsafe code is used.
| oconnor663 wrote:
| I replied to a related comment here:
| https://news.ycombinator.com/item?id=35034184
| theobeers wrote:
| I have a library in which I set #![forbid(unsafe_code)]. It
| relies on a small number of carefully chosen dependencies,
| almost all of which contain unsafe code. I mean first-rate
| libraries like bincode, bstr, and once_cell. It feels almost
| like a sham for me to emphasize the safety of the top layer
| that I've written.
| lordnacho wrote:
| What happens if you find unsafe deeply nested in some
| dependency that you need? Like a web framework or maybe a
| graphics engine that's not so easy to find a replacement for?
| satvikpendem wrote:
| Well, the simple answer is you don't use it. The more complex
| answer is that you do use it but have safe guards in place,
| which `unsafe` does to some extent even more than C or C++
| code. You could also ask the creators of said dependencies
| why there is `unsafe` in their code and perhaps submit PRs to
| mitigate or remove that, as in the case of actix-web a few
| years ago.
|
| But as Rust crates become more prevalent, I do expect that
| we'll see fewer and fewer crates with `unsafe`, or have
| enough safe alternatives to `unsafe` crates.
| LegionMammal978 wrote:
| > Rust is designed around safety and soundness. Roughly speaking,
| safe code is code that doesn't use the unsafe keyword, and sound
| code is code that can't cause memory corruption or other
| undefined behavior.
|
| I'd be a bit more precise here. Safety is indeed a property of
| some particular lexical part of the code, but soundness is really
| a property of an entire _interface_. A sound interface never
| causes UB when you invoke it according to its documented
| preconditions; an unsound interface may possibly cause UB in some
| situations even when invoked according to its preconditions.
|
| Some take a narrower definition of soundness, treating it
| primarily as a property of a safe interface: a sound interface
| that can be invoked from safe code can never cause UB under any
| circumstances. But personally, I prefer the broader definition,
| since it also includes unsafe interfaces which don't document all
| the preconditions they rely on.
|
| Knowing your interface boundary is an important part of writing
| unsafe code in Rust. For instance, suppose you have a data type
| with a method that unsafely makes a certain assumption about its
| fields. Then you have to be very careful to uphold this invariant
| in every other safe method that creates or modifies those fields.
| Furthermore, you have to ensure that no language-level operations
| on the object as a whole (e.g., swapping, dropping, forgetting,
| or covariant transformations) can break this invariant. But once
| you have locally checked all of those things, then you don't need
| to check any other part of the code base to know that your data
| type is sound.
|
| As another example, contrary to what some people say, it's not
| necessarily unsound to have a safe function that can cause UB on
| some inputs. You just have to ensure that external code cannot
| call that function (without having previously unsafely promised
| not to call it incorrectly), and no internal code calls it
| incorrectly. Here, the interface boundary is the entire section
| of your code that has access to the function.
|
| In particular, you sometimes see this with data types that have
| to implement safe traits using unsafe operations (for performance
| or for other reasons). Often, the function to create an object is
| unsafe, since you have to promise that the rest of your safe code
| does not invoke those trait implementations in such a way as to
| cause UB. Practically, you can do this by limiting how much of
| your code has access to the created object.
| mlindner wrote:
| > Knowing your interface boundary is an important part of
| writing unsafe code in Rust. For instance, suppose you have a
| data type with a method that unsafely makes a certain
| assumption about its fields. Then you have to be very careful
| to uphold this invariant in every other safe method that
| creates or modifies those fields. Furthermore, you have to
| ensure that no language-level operations on the object as a
| whole (e.g., swapping, dropping, forgetting, or covariant
| transformations) can break this invariant. But once you have
| locally checked all of those things, then you don't need to
| check any other part of the code base to know that your data
| type is sound.
|
| This is simply incorrect. If it is possible to, in any safe
| code, invoke the invariants that can cause the undefined
| behavior then your code is unsound. UnSafe code _cannot_ rely
| on safe code invoking it correctly. ALL invariants must be
| covered at the boundary between safe and unsafe code, anything
| else is a soundness bug in the code.
|
| > As another example, contrary to what some people say, it's
| not necessarily unsound to have a safe function that can cause
| UB on some inputs. You just have to ensure that external code
| cannot call that function (without having previously unsafely
| promised not to call it incorrectly), and no internal code
| calls it incorrectly. Here, the interface boundary is the
| entire section of your code that has access to the function.
|
| This is again completely incorrect. If it is possible to invoke
| unsound behavior from safe code then the code is fundamentally
| unsound. This is the problem that the Actix developer kept
| trying to justify that got him in a lot of hot water with the
| community. Please never write code this way. If you have any
| public repositories please mention them so bugs can be filed
| against them.
|
| Put another way, a bug in any piece of safe code must never
| cause any piece of unsafe code to commit undefined behavior.
| That's the fundamental guarantee that the entire language is
| based on. Please don't violate that by lying to developers that
| code is safe when it'a not.
| LegionMammal978 wrote:
| > This is simply incorrect. If it is possible to, in any safe
| code, invoke the invariants that can cause the undefined
| behavior then your code is unsound. UnSafe code _cannot_ rely
| on safe code invoking it correctly. ALL invariants must be
| covered at the boundary between safe and unsafe code,
| anything else is a soundness bug in the code.
|
| That's roughly what I'm saying? The point is that you have to
| carefully design your interface so that safe code from the
| outside is unable to break your invariants, no matter how it
| invokes your interface.
|
| > This is again completely incorrect. If it is possible to
| invoke unsound behavior from safe code then the code is
| fundamentally unsound. This is the problem that the Actix
| developer kept trying to justify that got him in a lot of hot
| water with the community. Please never write code this way.
| If you have any public repositories please mention them so
| bugs can be filed against them.
|
| If you have a public safe function that can cause UB, then it
| is indeed unsound. But it is possible to have a private safe
| function that can only possibly be accessed from a small part
| of your code base. If you are careful in preserving this
| invariant within the parts of the code that can directly
| access the function, then creating a sound interface without
| direct access to the function is entirely possible. (But this
| obviously gets more risky, the more code that has access to
| the function. I'll grant that it's not an uncommon pitfall to
| assume a crate-wide invariant somewhere that you later forget
| about.)
| codeflo wrote:
| You even have this phenomenon inside single functions.
| Let's say you write a function that's supposed to be a
| safe/sound abstraction. You might be setting up a pointer,
| maybe determined by a dozen lines of codes, that is then
| finally dereferenced. You'd typically only put the unsafe
| block around this final pointer access, but its safety
| depends on the whole calculation being correct.
| zozbot234 wrote:
| Unsafe _blocks_ are very different from unsafe-marked
| _functions_. A function that can indirectly cause UB when
| called in safe code _must_ be marked as unsafe, so that
| the invariants it relies on can be properly documented in
| a Safety: comment, and manually checked by callers.
| LegionMammal978 wrote:
| Only if it's public, and you don't know who the callers
| are. If you do know exactly who the callers are (since
| it's a private function), and you have internal
| documentation that lets contributors know of the proper
| invariants, then it's entirely possible to prevent UB
| from ever occurring even if the function is marked as
| safe.
|
| By itself, this is usually not a good idea. But in some
| cases, it's necessary, e.g., an unsafe implementation of
| an external safe trait on an internal type that only your
| internal code will ever see.
| zozbot234 wrote:
| Why? If you have a non-public function and all its
| potential callers are known, what's the harm in marking
| it as unsafe and documenting its invariants in the
| idiomatic way (a Safety comment)? Your case of
| implementing a safe trait via unsafe code seems like it
| should be quite rare, and even then the trait
| implementation should document the assumptions it's
| relying on as part of an unsafe block. If you neglect to
| mark other possibly-UB functions as unsafe, this becomes
| harder to ensure.
| LegionMammal978 wrote:
| I agree that it's rarely _a good idea_ to do this; it 's
| a red flag at minimum when you see it. I'm just trying to
| reject the notion that _it 's categorically unsound_ to
| do this. I've personally seen the safe-trait example
| while reviewing a crate, and there were indeed plenty of
| comments documenting the assumptions. As long as there's
| absolutely no way for anyone to cause these assumptions
| to be broken from the outside, there's no way to cause
| UB, and it's not really useful to call the interface
| unsound.
| humanrebar wrote:
| > ...soundness is really a property of an entire interface.
|
| Not really. I mean, maybe... if you limit the scope of your
| consideration to language design itself.
|
| If Rust call out to C code and they were using incompatible
| understanding of an array length due to mismatching build rules
| (like preprocessor definitions), you have a soundness bug it
| won't show up obviously in any interfaces anywhere.
|
| Soundness is a property of a single build of a program. You
| cannot generalize it. That is why in safety critical software,
| you cannot buy "safe" libraries or tools. You can buy libraries
| and tools with certification packets but context is very
| important when it comes to program correctness in all its
| forms.
| oconnor663 wrote:
| > Soundness is a property of a single build of a program. You
| cannot generalize it.
|
| I think it's important to be able to talk about sound
| functions, without needing to know anything about their
| callers, other than what's expressed in the function
| signature.
| humanrebar wrote:
| I'm saying even for that case, you have to know how the
| implementation of that function was compiled, linked, and
| possibly even deployed. You cannot promise an interface is
| "sound". You can promise certain expectations were
| communicated clearly within some parameters (like object
| types and lifetimes assuming a coherent build process).
| LegionMammal978 wrote:
| > If Rust call out to C code and they were using incompatible
| understanding of an array length due to mismatching build
| rules (like preprocessor definitions), you have a soundness
| bug it won't show up obviously in any interfaces anywhere.
|
| In that scenario, I'd say that the build rules are a poorly-
| documented part of the C code's interface, and by calling out
| to it with mismatching build rules the Rust code is unsound.
|
| In general, C interfaces tend to have a lot of poorly-
| documented or undocumented preconditions, which is one of the
| things that make Rust-to-C FFI so tricky. One of Rust's big
| ideas is to make all these preconditions fully explicit,
| either through typestate or through "Safety" sections on
| unsafe functions and traits.
| humanrebar wrote:
| Bad docs and brittle build expectations make C to C "ffi"
| tricky as well. But it's realistically how this kind of
| programming, generally speaking, tends to happen.
|
| I don't expect we'll have a great end to end solution until
| there are language agnostic build and dependency management
| systems available. As much as I like cargo, I don't expect
| porting everything to cargo will cut it. At least not in
| its current form.
| Animats wrote:
| > Then you have to be very careful
|
| The problem.
| LegionMammal978 wrote:
| Well, in principle, you could place each field in its own
| wrapper type that is unsafe to access. But you'd still have
| to be nearly as careful: there's no way around that, short of
| formal verification. Rust is ultimately a pragmatic language,
| and it lets the user pick their preferred balance between
| decreased performance, extra care, or extra line noise that
| people will get tired of reading. At best, it can give
| suggestions in the design of its standard library of where
| that balance ought to be.
| oconnor663 wrote:
| I think what you're describing might be similar to
| footnote/marginnote #6?
| LegionMammal978 wrote:
| Sorry, I was reading the article from my phone and didn't
| notice that footnote. It definitely ties in to the point I'm
| making here, but I still feel that the distinction is
| important enough to elaborate on a bit.
| dataflow wrote:
| I feel like #![forbid(unsafe_code)] deserves a mention?
| oconnor663 wrote:
| This starts to open up a whole topic that I though about
| including but ultimately decided not to: How do we deal with
| unsafe code in our dependencies? This gets into all sorts of
| tools like cargo-geiger and blessed.rs and things like that.
|
| Luckily, I think the Python/Java metaphors work well here. Rust
| apps have to worry about unsafe code in their dependencies,
| like Python and Java apps have to worry about C code in their
| dependencies. So hopefully not _too_ much is lost by leaving
| those questions for another article. Maybe I should add another
| footnote though.
| Animats wrote:
| Oh, if only that worked in practice.
|
| There's far too much use of "unsafe" in Rust crates. Sometimes,
| for really good reasons. Too often, because someone can't figure
| out a safe way to do something.
|
| Here's a thread I started on Reddit on this subject: "We're not
| really game yet".[1]
|
| Someone commented there: _" There are people here who would
| volunteer to help you out, dig up the internals and address those
| issues, given you provide them with sufficient information."_
|
| My reply:
|
| I've done that three times now:
|
| * jpeg2000-decoder -- JPEG 2000 decoder test fixture. This
| exercises jpeg2k->jpeg2000-sys->OpenJPEG. The first two are in
| Rust, the last one is in C, and valgrind shows it referencing un-
| initialized memory. It randomly segfaults. OpenJPEG has a long
| history of doing this, and has been the subject of several CERT
| security advisories. The author of jpeg2k has managed to contain
| the the problem by running OpenJPEG in a WASM sandbox. This keeps
| the program from crashing, but there is a 2.6x performance
| penalty. A bug report has been submitted to the OpenJPEG
| maintainers, who are funded by universities and companies but
| over 200 issues behind.
|
| * ui-mock -- game GUI test fixture This exercises
| rfd->egui->rend3->wgpu. It's a game GUI with menus and dialogs,
| but no game behind it, just a 3D drawing of a cube. It's useful
| for making bugs in that stack repeatable. That's been helpful in
| wringing out obscure bugs in egui.
|
| * render-bench -- scene update performance test fixture. This
| exercise rend3->wgpu->vulkan. It draws a city of identical
| buildings, then, from a second thread, periodically deletes half
| of them and re-creates them. If the stack is performing as
| intended, the updates from the second thread should not impact
| the frame rate from the main thread. But due to lock problems at
| the WGPU level, the frame time goes from 16ms to 700ms when the
| update happens.
|
| (Those are all projects on my Github [2], by the way, if anyone
| wants them.)
|
| Each time I have to do one of those bug-reproduction test fixture
| projects, it costs me substantial time not spent on the main
| project.
|
| Right now, I'm totally stopped by a race condition crash bug not
| in the list above, one for which I don't have a standalone
| project which can duplicate the bug. They're trying to fix it,
| but for now I'm stuck. I may have to build a custom test project.
| But it will be tough, because it's a timing-dependent bug that
| only appears under load.
|
| This is why no one has successfully done a major game on Rust.
| The foundations are too weak to support it. The right stuff is
| there, but much of it is stuck at "sort of works". It's a real
| question whether game development in Rust will ever have tools
| that Just Work, or whether parts of the ecosystem will go
| directly from "sort of works" to "abandoned" without ever
| reaching Just Works.
|
| Safe Rust really works. My own code is 100% safe Rust, 36,000
| lines of it. No obscure crashes in my own code. When I've needed
| gdb or valgrind, it's always been due to a problem in someone's
| unsafe Rust or C code.
|
| There are people who think they're so good they can write unsafe
| code.
|
| They're not.
|
| [1]
| https://www.reddit.com/r/rust_gamedev/comments/11b0brr/were_...
|
| [2] https://github.com/John-Nagle
| nindalf wrote:
| I might have misunderstood what you've written, but is there a
| reason you can't use https://github.com/etemesi254/zune-image
| as a JPEG decoder? It has minimal use of unsafe and is
| performant.
| erk__ wrote:
| Jpeg 2000 is not the same codec as Jpeg, it was an attempt to
| make a more modern image format, but it never really caught
| on outside of science and medical imaging. For example
| satalite images are often jpeg 2000
| Animats wrote:
| That's a classic JPEG decoder, not a JPEG 2000 decoder.
| They're completely different compression systems. JPEG is
| discrete cosine transform, while JPEG-2000 is wavelet. JPEG
| 2000 never really caught on; it's a good compression
| algorithm but has too many features. JPEG 2000 is used mostly
| for medical imagery, because you can efficiently zoom in on
| an area and the most detailed level can be compressed
| losslessly. It's also used for some virtual world content,
| because you can read the beginning of the file and get a
| lower-rez version.
| nindalf wrote:
| Yeah fair enough. It might be worth requesting the
| maintainer of zune-image to add a decoder for jpeg2000. The
| repo already supports a whole bunch of formats already.
| Ygg2 wrote:
| > This is why no one has successfully done a major game on
| Rust. The foundations are too weak to support it.
|
| That's patently false. https://veloren.net/
|
| People have made games in it. And in Zig, and C, and C++ and
| so. And assembly. And machine code. Unsafety isn't a problem
| it's lack of libs and integration.
|
| If you refer to AAA the issue is lack of engines where Rust is
| a must. I.e. network effect, you need engines in Rust to make
| games in Rust. And to make engines in Rust, more games need to
| require Rust.
| Thaxll wrote:
| Veloren is not a major game, it's not even considered a
| successful indy game.
| olah_1 wrote:
| > This is why no one has successfully done a major game on
| Rust. The foundations are too weak to support it.
|
| The Finals open beta is happening on the 6th. Check it out
| https://www.reachthefinals.com/
|
| Here is a blog post about their usage of Rust:
| https://medium.com/embarkstudios/inside-rust-at-embark-b82c0...
| Thaxll wrote:
| This game uses Unreal engine so it's most likely all in C++
| or most of it.
|
| We don't really know about the state or what game is built in
| Rust a Embark, but 2 majors games they work on are in C++.
| ridiculous_fish wrote:
| > Instead, the only way I can think of to make foo1 commit UB is
| to give it an uninitialized index
|
| Nah a `fclose(stderr)` before calling it will do it!
|
| > A safe caller can't be "at fault" for memory corruption or
| other UB.
|
| An example of where a safe caller can be at fault is after fork,
| or in a signal handler. Functions like malloc must not be called,
| but Rust does not model this and freely invokes malloc from safe
| functions.
|
| In these regimes safety is inverted: File::open is NOT safe, and
| may invoke UB; while the "unsafe" libc::open IS safe and should
| be used instead.
| burntsushi wrote:
| I wouldn't agree with this. In order to register a signal
| handler in Rust, you have to utter unsafe somewhere. That's the
| point at which you form the contract that your signal handler
| is correct. The fault is not with the safe code that allocates.
| The fault is with the creation of the signal handler.
|
| It is true that Rust could model signal safety in some richer
| way, but that's just a matter of shifting where the contract is
| formed.
| codeflo wrote:
| This is a very good explanation.
|
| It's sad that these terms are so counterintuitive. I still
| regularly have to clarify the confusion between undefined and
| unspecified behavior. And to be fair, "define" and "specify" are
| basically synonyms in everyday language, it's evil that these
| terms have so wildly different meanings. Quite a bit of confusion
| is caused by people not fully understanding that "unsafe" in Rust
| actually just means "unchecked", i.e. not verified by the
| compiler, and not necessarily "insecure".
|
| Now we add "unsound" to the mix, and I fear that all of this is
| simply too hard to learn.
|
| Maybe we should reverse all those terms to be positive, and also
| use words in their intuitive meaning, and also avoid confusing
| almost-synonyms. Here's a quick attempt:
|
| 1. "Well-behaved" code would be code that only accesses memory
| correctly. (That's not fully precise enough. What I actually mean
| of course is "no UB", but that's hard to put into words if you
| don't already know what UB is. Let's say well-behaved code only
| does "allowed stuff".)
|
| 2. "Checked" code is code verified by Rust's compiler to be well-
| behaved if certain invariants are fulfilled. Code is always
| checked by default unless you opt out with a special keyword
| (confusingly called "unsafe").
|
| 3. "Sound" code is well-behaved code (that may or may not be
| checked by the compiler) with the additional restriction that
| will remain well-behaved no matter how it's embedded into checked
| code. This is what enables "safe abstractions". Checked code is
| automatically sound by this definition, but alternatively,
| unchecked code could also be sound, which would have to be
| manually verified somehow. To do this, you obviously need to know
| which assumptions the checker makes in its reasoning.
|
| (Edit: Reworded a bit to address an unexpected source confusion.
| What's now called "well-behaved" was called "safe", but that has
| multiple meanings. Some of the responses in the thread apply to
| the old version.)
| hitekker wrote:
| To follow your recommendation would require defining Rust as a
| memory-safer language, not as the language of memory safety.
| That will take time due to history.
|
| Defining safety was stonewalled by the previous maintainers of
| Rust who enshrined their nebulous idea of safety as the source
| of the language's identity. These maintainers focused their
| efforts on evangelizing and expanding Rust, at the expense of
| governing it. They believed they could enforce their idea of
| safety by arguing with people on Twitter or HN, instead of
| writing a formal specification or a constitution. The latter is
| hard work, the former is easy and also has the added benefit of
| the motte-and-bailey game, i.e. talking out of both sides of
| the mouth.
|
| Following highly dramatic people problems, those maintainers
| have since been ejected[1][2] and replaced with people who
| seems to actually care about the governance of Rust[3][4].
|
| [1]
| https://www.theregister.com/2022/02/01/rust_core_team_depart...
|
| [2] https://www.rust-lang.org/governance/teams/core
|
| [3] https://github.com/rust-lang/rfcs/pull/3355,
| https://blog.m-ou.se/rust-standard/
|
| [4] https://thenewstack.io/rust-project-reveals-new-
| constitution...
| burntsushi wrote:
| As a former member of the Rust mod team, and a continuing
| member of the Rust project, this narrative is complete
| bullshit.
| codeflo wrote:
| > To follow your recommendation would require defining Rust
| as a memory-safer language, not as the language of memory
| safety.
|
| I'm actually suggesting changes in explanation only, hoping
| to make these terms less confusing for beginners. Rust
| veterans know that "safe" code just means "code not in an
| unsafe block", beginners are often confused by the more
| common meaning of the word.
| LegionMammal978 wrote:
| I don't think your attempt would be quite accurate: safe code,
| even though it is checked, can cause UB. This can only occur if
| some earlier unsafe code was unsound, by the current
| definition.
|
| For instance, suppose you have an ordinary Box<i32>. Then, you
| dereference it to create a local &i32 reference, and use unsafe
| code to turn it into a &'static i32 reference. Then, you drop
| the Box<i32>, which deallocates the backing memory. Finally,
| you attempt to read the value from the &'static i32.
|
| The UB occurs only at the last step, when you read from
| deallocated memory. But reading from a &'static i32 reference
| is completely allowed in safe, checked code. That's why we use
| the term "unsound" to cast fault on the earlier unsafe code
| which allowed us to do this.
| skitter wrote:
| Just holding an invalid reference is UB, even if you don't do
| anything with it (the example still works as the reference
| gets invalidated by safe code).
| oconnor663 wrote:
| I think there are some aspects of this rule that are still
| undecided. See for example:
|
| - https://github.com/rust-lang/unsafe-code-
| guidelines/issues/8...
|
| - https://github.com/rust-lang/miri/issues/2732
| LegionMammal978 wrote:
| A reference is never really "held": from a language-
| semantics standpoint, it only exists when it is actually
| used. In this example, copying, reborrowing, or accessing
| the reference would be UB, but simply letting it fall out
| of scope would not be UB (modulo the UCG issue oconnor
| mentioned; but I personally doubt that this status quo will
| change). You can try this yourself with Tools > Miri on the
| Playground (https://play.rust-
| lang.org/?version=stable&mode=debug&editio...). The
| distinction is far more relevant for unsafe code than for
| safe code.
| oconnor663 wrote:
| Yeah this is a great example of why formally defining
| "soundness" is so tricky. Not only do we have to worry about
| unsafe code committing UB, we also have to worry about it
| setting up a situation where safe code might commit UB later.
| Soundness is a property of functions and modules, but that
| property is a statement about the entire program that
| contains them, or actually about _any_ program that _could_
| contain them.
|
| In retrospect, I kind of wish I had asked somebody like Ralf
| Jung to review this post before I published it. On the other
| hand, the fastest way to get an answer on the internet is to
| post the wrong answer :-D
| codeflo wrote:
| That's a very good example. I think that's what I meant by:
|
| > "Sound" code is safe code (that may or may not be checked
| by the compiler), that will remain safe no matter how it's
| called by checked code.
|
| Maybe "called" was too specific, it could be embedded in
| other ways. But it's clear in your example that there's a way
| of integrating the unsafe cast into otherwise fully checked
| code that leads to UB. Hence, it is not sound.
___________________________________________________________________
(page generated 2023-03-05 23:01 UTC)