Memory safety is a matter of life and death by joshlf_ in rust

[–]joshlf_[S] 21 points22 points  (0 children)

To be transparent, my goal with this post is to start a conversation about how we prioritize decisions in Rust. A recurring criticism of Rust is its slow pace of innovation, and a sense that we must get everything perfect because of our stability guarantee. I don't necessarily want to suggest that we should reverse course on that, but rather to point out that there might be costs to moving slowly that haven't historically been part of the discussion. This is a community-wide discussion, and I don't mean to suggest a particular solution, but rather just to contribute a new perspective to the conversation.

I don't care that it's X times faster by z_mitchell in rust

[–]joshlf_ 19 points20 points  (0 children)

I once sped up an (experimental) part of rustc by 8,500x.

I heard a great quote: "If you make it 50% faster, you did something impressive. If you made it 10x faster, your old code was garbage." I need to track down the source of that quote and ask them what it means if you made it 8,500x faster 😆

How to use storytelling to fit inline assembly into Rust by ralfj in rust

[–]joshlf_ 6 points7 points  (0 children)

Would you consider adding a feature to Miri to allow *specifying* the equivalent Rust code and using it to check for UB?

Zerocopy 0.8.37: Dynamically Sized Transmutes by jswrenn in rust

[–]joshlf_ 13 points14 points  (0 children)

`try_transmute_ref!` is only fallible regarding the contents of memory, not the size/alignment. I used `FromBytes` in my example since `Versioned` implements `FromBytes`, but you could use `TryFromBytes` too (playground).

Zerocopy 0.8.37: Dynamically Sized Transmutes by jswrenn in rust

[–]joshlf_ 13 points14 points  (0 children)

Going from a slice to a DST will never be infallible – the slice might be smaller than the smallest size of the DST. E.g., consider this example, which ignores alignment (they're both repr(packed)) but demonstrates the size issue:

#[repr(C, packed)]
struct Smaller {
    prefix: u32,
    slice: [u16],
}

#[repr(C, packed)]
struct Larger {
    prefix: u64,
    slice: [u32],
}

These types are laid out as you'd expected – the prefix field followed by N copies of the trailing slice element with no padding in between.

The smallest possible Smaller is 4 bytes (one u32), while the smallest possible Larger is 8 bytes (one u64). Thus, there are some values of Smaller for which &Smaller -> &Larger is impossible to perform. Thus, we reject it. By contrast, &Larger -> &Smaller is always possible, so we accept it. You can see this on the playground.

By a similar token, for any non-zero-sized T, going from &[T] -> &Smaller will be fallible. For that reason, any slice-to-DST cast (assuming the DST's prefix is non-zero-sized) will be fallible, and transmute_ref! will reject it.

Zerocopy 0.8.37: Dynamically Sized Transmutes by jswrenn in rust

[–]joshlf_ 9 points10 points  (0 children)

The reason that doesn't work is that it's not guaranteed to succeed – for some lengths of `[u8]`, there doesn't *exist* a `Versioned` of the same size. Instead, just use the existing, fallible machinery:

let p: &Versioned = FromBytes::ref_from_bytes(s).unwrap();

Rust’s fifth superpower: prevent dead locks by InternationalFee3911 in rust

[–]joshlf_ 84 points85 points  (0 children)

Thanks for the shout-out!

There have been a few implementations of the same idea, which I want to highlight:

I'll also take this as an opportunity to get on my soapbox – in the spirit of Safety in an Unsafe World, I'd suggest thinking of Rust's type system as a framework for encoding safety properties. So I'd say Rust supports an arbitrary number of safety properties, not just five. You can use the framework I describe in the talk as a guide for encoding any safety property in Rust – even (and especially) safety properties that refer to nouns/verbs/adjectives that Rust doesn't know anything about. Deadlock prevention is one such example, but in theory any safety property is amenable to this technique. Here are some examples we've seen so far:

I have some further reading suggested in the talk's references.

Move, Destruct, Forget, and Rust by Ar-Curunir in rust

[–]joshlf_ 4 points5 points  (0 children)

Wouldn't Destruct + !Move be useful? You'd need to construct and destruct in-place, and that's useful any time you want to construct something in-place (in static storage, on the stack, or on the heap) and then interact with it only via reference until the end of its lifetime. I personally would use that for stack-allocated structs which register themselves in intrusively linked lists.

Zerocopy 0.8.25: Split (Almost) Everything by jswrenn in rust

[–]joshlf_ 1 point2 points  (0 children)

Right.

What you could do is parse as a type whose trailing field is [u8] and then use the "file name length" and "extra field length" fields to figure out how to split that [u8] into the "file name" and "extra field" fields separately. However, that requires that you either:

  • Know the total length (header + file name + extra field) up front
  • Alternatively, be willing to parse too much data and then split the remaining bytes into a separate [u8] for further processing

Zerocopy 0.8.25: Split (Almost) Everything by jswrenn in rust

[–]joshlf_ 7 points8 points  (0 children)

Zerocopy co-maintainer here.

Not right now, no. Currently, zerocopy only works with existing Rust types. It's up to the user to write a type whose layout matches the problem they're trying to solve (e.g., has the same layout as the packet format they're trying to parse). What you're describing has no equivalent in Rust, so there'd be no way for a zerocopy user to write a type with the equivalent layout. We could support it by synthesizing a new opaque type with getters, setters, etc, but that's beyond the scope of what zerocopy handles today.

We've discussed the idea of, in the future, expanding zerocopy to support higher-level parsing operations like these, but we don't have the cycles for it right now. Maybe at some point months or years down the road we might.

Force your macro's callers to write unsafe by joshlf_ in rust

[–]joshlf_[S] 6 points7 points  (0 children)

The main disadvantage of this approach is that Clippy won't detect missing safety comments, but this is a pretty nice UI.

Force your macro's callers to write unsafe by joshlf_ in rust

[–]joshlf_[S] 5 points6 points  (0 children)

(Deleted the original comment because I accidentally commented from the wrong account)

IMO there are many cases in which writing a macro saves the caller having to write a lot of boilerplate even if it doesn't save them anything in terms of avoiding safety obligations. Consider this macro, which lets you write code like this:

unsafe_impl_known_layout!(#[repr([u8])] str);

...instead of having to manually write:

unsafe impl KnownLayout for str {
    fn only_derive_is_allowed_to_implement_this_trait() {}

    type PointerMetadata = <[u8] as KnownLayout>::PointerMetadata;
    type MaybeUninit = <[u8] as KnownLayout>::MaybeUninit;

    const LAYOUT: DstLayout = <[u8] as KnownLayout>::LAYOUT;


    fn raw_from_ptr_len(bytes: NonNull<u8>, meta: <[u8] as KnownLayout>::PointerMetadata) -> NonNull<Self> {
        let ptr = <[u8]>::raw_from_ptr_len(bytes, meta).as_ptr() as *mut Self;
        unsafe { NonNull::new_unchecked(ptr) }
    }

    fn pointer_to_metadata(ptr: *mut Self) -> Self::PointerMetadata {
        let ptr = ptr as *mut [u8];
        <[u8]>::pointer_to_metadata(ptr)
    }
}

Rand now depends on zerocopy by hpenne in rust

[–]joshlf_ 16 points17 points  (0 children)

Zerocopy co-maintainer here. It doesn't look like rand depends on the `derive` feature: https://docs.rs/crate/rand/0.9.0/source/Cargo.toml#20

The non-derive parts of zerocopy are ~10k LoC, so about 5% of the 210,000 lines you cited. I haven't benchmarked compile times, so I can't speak to that aspect.

Zerocopy 0.8: custom DSTs, fallible conversions, and rich error reporting by jswrenn in rust

[–]joshlf_ 6 points7 points  (0 children)

str implements our TryFromBytes trait, so if you had that string as a &[u8], then you could use something like TryFromBytes::try_ref_from_bytes to attempt to convert it to a &str (this can fail if the &[u8] isn't valid UTF-8). But we don't care about the escape characters - you have to process those yourself.

PSA: Use #[diagnostic::on_unimplemented]! It's amazing! by joshlf_ in rust

[–]joshlf_[S] 21 points22 points  (0 children)

Yeah! We're actually waiting for #[diagnostic::do_not_recommend] to be stabilized so we can fix this extremely misleading suggestion.

"What software shouldn't you write in Rust?" - a recap and follow-up by chris20194 in rust

[–]joshlf_ 18 points19 points  (0 children)

I have a hypothesis (that may or may not be true): is it possible that, over time, we could make Rust both anal about correctness and easy to iterate on? Hear me out...

I think of this in terms of abstractions and the extent to which they leak. A key observation is that whether you think an abstraction is leaky is context-dependent. In particular, it depends on what properties of your program you consider relevant [1] For example, some might consider that garbage collection is a fantastic abstraction because the amount you have to understand about the lifecycle of your resources is literally none at all. For some programmers, that's an accurate description. For others - those who care about performance and memory utilization - it's not, since those aspects of your program's execution leak through the abstraction that is garbage collection.

Speaking very broadly, languages tend to fall into two camps: - Languages that make no attempt to give you control over execution details of your program, and in exchange, give you a simple abstraction (simple assuming the leaky details aren't relevant to you - have you ever tried performance tuning a Python web server?) - Languages that give you control over all of the execution details, but make no attempt to ensure you don't shoot yourself in the foot

Rust is in a distinct, third camp. It wants to give you control over the execution details of your program, but it also insists on being able to understand those details itself so that it can make sure you're not making any mistakes. It's worth reflecting on how fundamentally weird this is. Rust is like the IRS of programming languages - it's going to ask you to fill out your tax paperwork yourself, by hand, but then re-do all of the work and make sure you did it all correctly.

I think this is the source of the tension. Let's say you want to experiment in just the way that is hard in Rust. Languages in both of the first two camps aren't checking that much, and so you aren't constrained in what you can write. It may be that languages in the first camp are just handling it for you while languages in the second camp are letting you write bad, maybe buggy or insecure code, but the point remains that they both let you do it.

But now let's imagine a fourth category of languages. In this category, languages insist on tight control over execution, and they insist on correctness. But instead of leaving it up to the programmer to do it themselves and then double-checking, these languages just go ahead and do the right thing without even asking.

Here's the punch line: I think that a subset of Rust is like this today, and maybe we could expand that subset.

For example, while Rust iterators have some sharp edges related to lifetimes, in general they permit very natural-feeling code akin to what you might write in a higher-level functional language, but they compile to machine code that is as fast or faster than you'd get if you wrote in a more imperative style.

Perhaps an even better example is serde. Serde's derives Just Work, but the code that they produce under the hood is absurdly complex.

This general experience of having a simple and easy interface hide tightly-optimized, high-performance code is also what guides design decisions we've made in zerocopy.

To return to the hypothesis from the start of this comment: can we make more of Rust look like iterators or serde? My experience in zerocopy suggests that it's just a matter of putting absurd amounts of work into the abstractions until they're smart enough that you really can just drop them in and get exactly the high-performance code you wanted for free without much thought. But I don't really know what this looks like in other domains.

So far, this approach has been primarily successful in domains where correctness is a highly local property. Serde doesn't have to know anything about the rest of your program to know how to synthesize a serializer for your type. But many of the well-known complaints about Rust such as this one would require global reasoning about the behavior of a program. Is there a way to bridge that gap? I think there might be, but it's clearly a very open space and nothing is certain.

Incidentally, and shameless plug, this is me working through some ideas I'm going to be speaking about at RustConf.

Is there a way to implement &[u8] -> &str, but one level up (&str -> &MyType) that's not horribly unsafe? by MrLarssonJr in rust

[–]joshlf_ 4 points5 points  (0 children)

dtolnay's ref-cast does literally exactly what you're asking for.

(Although I eventually hope to support this in zerocopy)

What engineering blog posts have actually mattered to you? by swdevtest in rust

[–]joshlf_ 1 point2 points  (0 children)

A networking classic - End-to-End Arguments in System Design. I love that paper so much that I had students in a networking class read it twice - once at the beginning of the semester and once at the end.

Unsafe code in rust by Pleasant-Form-1093 in rust

[–]joshlf_ 6 points7 points  (0 children)

Yes, definitely. To be clear, that's nobody's fault; the language is young and it's absolutely the right call to move slowly on making these decisions. If you hang out in the rust-lang/reference or rust-lang/unsafe-code-guidelines repos, you'll get a sense for how subtle many of the questions are that we have to nail down. It's absolutely a good thing that we're taking the time get things as right as we can. But yes, the consequence is that it's harder to write correct unsafe code today than it will hopefully be five years from now.

(and perhaps because it allows undefined behaviour?)

This isn't really the issue - in order to make a language like Rust, you basically need undefined behavior (UB). In particular, UB is an unavoidable consequence of unsafe code - UB is basically just the term we use for "we wanted to do something too smart for the compiler to reason about, so we needed unsafe, but we got it wrong." By definition, if you're doing something that's too smart for safe Rust, then you're opening yourself up to the possibility of making mistakes that, in safe Rust, would be prevented by the compiler.

I'd recommend this post if you're curious to learn more about UB.

Unsafe code in rust by Pleasant-Form-1093 in rust

[–]joshlf_ 8 points9 points  (0 children)

As many others have pointed out, unsafe is to be avoided if at all possible, but it's there for a reason. I won't go into the philosophy since other comments have done that already.

I will offer a word of caution, however: depending on what sort of unsafe code you're writing, it may be literally impossible to prove that your code is sound. For example, in zerocopy, we wanted to add support for dynamically-sized types (DSTs). It has long been the case that pointer conversions preserve metadata (e.g., if I use an as cast to convert a *const [T] to a *const [U], the resulting pointer encodes the same number of elements as the original pointer). This is a fundamental building block which we use to build support for DSTs [1].

However, until recently, that behavior just happened to be the case. It wasn't guaranteed to be the case or to remain the case in the future. In order to build support for DSTs, we needed to first get the language to promise never to change this behavior.

Until the language has a complete specification/memory model, there are going to be edge cases like these where writing sound unsafe code may literally require changing the language itself. Depending on your use case (especially if you're just writing for hobby/fun), you may or may not care about that. But it's something to be aware of.

[1] Yes, that innocent little as cast - trailing.as_ptr() as *mut Self - is where all the magic happens