Live data from Hacker News

Unsafe Rust: An Intro and Open Questions

cglab.ca

11–20 of 34 posts

Re: Unsafe Rust: An Intro and Open Questions

#11
post #10

Earlier quoted context omitted.

Sure. First, thanks for the writeup. I had imagined that the Rust standard library used safe code all the way down. (Whatever that meant, I hadn't put all that much thought into it.) But as you state, "everything is built on top of unsafe." So I guess my understanding after reading this is that I could, using only "safe" code, accidentally manipulate the Rust standard library to cause undefined behavior, it's just mu…

One important guarantee of Rust though: If you manage to do that, this is a bug in Rust and it's not your fault . And really, that's true of any "safe" language, right? Java, Ruby, Javascript, Python, whatever -- implementation errors mean your program will do crazy bad stuff, and we all have them.

Just to add to this point, the difference is that Rust can only guarantee this for the standard library. I can similarly write a library with safe interfaces that can be (ab)used to cause UB and there's little that the Rust team can do. This is different from other "safe" languages.

This is why it's so important to establish what the responsibility and expectation is of library developers to uphold the safety guarantees that everyone else relies on. It only takes one bad library to destroy the safety guarantees everyone who is transitively using that library relies on.

Re: Unsafe Rust: An Intro and Open Questions

#12
post #6
post #4

Since you mentioned you didn't understand what LLVM's "in bounds" meant: In general, LLVM semantics are largely derived from C and C++ in terms of undefined behavior (although some aspects, like signed overflow, are merely optional). The rule for "in bounds" means that pointers can only be manipulated to point to some address within the bounds of their object, or possibly the address just after the object (which cann…

That's also this tricky issue with C that's often ignored. If you have a nested array and take a pointer &foo[0][0], the only in bounds pointers are the pointers into the 0th subarray.

That's true for C, but not for LLVM inbounds.

Re: Unsafe Rust: An Intro and Open Questions

#13
post #11
post #10

Earlier quoted context omitted.

One important guarantee of Rust though: If you manage to do that, this is a bug in Rust and it's not your fault . And really, that's true of any "safe" language, right? Java, Ruby, Javascript, Python, whatever -- implementation errors mean your program will do crazy bad stuff, and we all have them.

Just to add to this point, the difference is that Rust can only guarantee this for the standard library. I can similarly write a library with safe interfaces that can be (ab)used to cause UB and there's little that the Rust team can do. This is different from other "safe" languages. This is why it's so important to establish what the responsibility and expectation is of library developers to uphold the safety guarant…

This is not all that different from Java (or Python, etc.), where it is quite easy to hide a call to a native function behind a seemingly-safe interface. The real difference is that native methods in Java must be written in a different language (C), while Rust supports both modes in the same language. (Edit: Or, if you prefer, two different but very closely related languages.)

I would argue, at any rate, that this sort of safe/unsafe boundary is still useful for the purpose of auditing code. Conceptually, memory bugs are interactions between two points in the program: e.g. one location deallocates a pointer, then another tries to dereference it. With Rust's implementation of unsafe, you are guaranteed that any bad interactions must have at least one endpoint in an unsafe block. You still can't completely ignore the safe code, because unsafe code can reach arbitrarily far out of its box (so to speak), but in general this constraint does help significantly in limiting the amount of code that needs to be audited.

Re: Unsafe Rust: An Intro and Open Questions

#14
post #11
post #10

Earlier quoted context omitted.

One important guarantee of Rust though: If you manage to do that, this is a bug in Rust and it's not your fault . And really, that's true of any "safe" language, right? Java, Ruby, Javascript, Python, whatever -- implementation errors mean your program will do crazy bad stuff, and we all have them.

Just to add to this point, the difference is that Rust can only guarantee this for the standard library. I can similarly write a library with safe interfaces that can be (ab)used to cause UB and there's little that the Rust team can do. This is different from other "safe" languages. This is why it's so important to establish what the responsibility and expectation is of library developers to uphold the safety guarant…

Right, but if you write a library using only Rust "safe" code, the guarantee is back on the Rust team.

To me, it would be better if libraries using unsafe code were marked.

Re: Unsafe Rust: An Intro and Open Questions

#15
post #5
post #4

Since you mentioned you didn't understand what LLVM's "in bounds" meant: In general, LLVM semantics are largely derived from C and C++ in terms of undefined behavior (although some aspects, like signed overflow, are merely optional). The rule for "in bounds" means that pointers can only be manipulated to point to some address within the bounds of their object, or possibly the address just after the object (which cann…

It was more a jab at LLVM's documentation in general ;) But since you're claiming to understand LLVM docs: * How does LLVM identify that a region of memory is "allocated" per the usage in the GEP docs. In particular it may be useful to mark special addresses as "allocated" for special marker objects that don't actually exist. * Does "in bounds" extend to arrays? e.g. can I offset even further from a ptr to foo if it'…

"Allocated" is something that arbitrary functions like "malloc" can define for themselves. LLVM specifies what can be done with "allocated memory", and then it's up to the API and implementation of malloc to provide something that works and is useful within LLVM's framework. Dereferencing "allocated" bytes through a proper pointer has to work. Dereferencing "unallocated" bytes is undefined behavior (regardless of whether the deference will succeed or fail in hardware).

LLVM's type system is mostly inert in its memory semantics. There is no difference between arrays or any other type of object with respect to what addresses can be computed and dereferenced. The important things are allocations which guarantee contiguous regions of memory.

Re: Unsafe Rust: An Intro and Open Questions

#16
"Unsafe" is an escape hatch used for a number of reasons. There are good ones and bad ones. Bad ones include:

- "I'm so l33t I don't need the compiler to check me." (Don't hire those guys.)

- "Safe code is too slow". (File bugs on the compiler's optimizer.)

- "Porting this to safe code would require a redesign". (See the Rust port of DOOM.)

Most of the real needs for "Unsafe" in Rust come from

- The need to interface with external code, including system calls.

- Forced type conversion ("casting")

- Memory allocation.

The first one is mostly a problem with expressive power in the foreign function interface. Can you express what "int read(int fd, char buf[], size_t len)" means in the foreign function definition syntax? Rust's foreign function syntax isn't expressive enough to do that.[1] You can't tell Rust that "len" is the length of "buf". Being able to do that would help reduce the need for unsafe code. Most of the POSIX/Linux API can be described with relatively simple syntax that allows you to associate size info with C arrays. (I once proposed this as an extension to C. It's technically possible but politically too difficult.)[2]

If your external interface still requires unsafe code after that, you're probably talking to something that has elaborate foreign data structures visible to the caller. Those really are unsafe. They also usually need a rewrite anyway. (OpenSSL comes to mind.)

Forced type conversion, or casting, is traditionally a problem. Most of the trouble comes from C, where casts bypass all type checking. In practice, much casting is safe. If a type is fully mapped to the underlying bits (i.e. all possible bit value are valid for the type), then allowing a cast is safe. If you cast 4 bytes to a 32-bit unsigned integer, the result is always a valid 32-bit unsigned integer. Conversions like that should be explicit, but are not memory-unsafe. On the other hand, casting to a pointer is always unsafe. Again, with a bit more expressive power, the need for unsafe code can be reduced.

Memory allocation is hard. However, more of it could be done in safe code. Suppose Rust had a type "space", which is simply an array of bytes, treated as write-only. Constructors take in an array of "space" of the desired type, create a valid local structure with the initialized values, and then perform an operation which copies the structure to the array of "space" and changes its type to the type of the structure. This is safe construction. As an optimization, the compiler can observe that if no reads are made from the local structure prior to converting the "space", the extra local copy is unnecessary.

"Space" would still have Rust scope and lifetime, so all that machinery remains hidden. But it's convenient to separate it from construction. Raw memory allocation is complex and unsafe, but separated from the type system, it's a coherent closed system that doesn't get modified much. It's a good candidate for formal proof of correctness - not too big, and critical to system operation.

Operations such as expanding vectors seem to include unsafe code. That's worth a hard look. If you had the "space" concept, and the operation that moves a struct into a "safe" array and converts the type, it should be possible to do operations such as growing an array without unsafe code.

For Rust 2, it's worth looking at how the need for unsafe code can be reduced. Ultimately, everything should be either memory safe or have a machine proof of memory correctness at the instruction level.

[1] https://doc.rust-lang.org/book/ffi.html [2] http://www.animats.com/papers/languages/safearraysforc43.pdf

Re: Unsafe Rust: An Intro and Open Questions

#17
post #11

Earlier quoted context omitted.

Just to add to this point, the difference is that Rust can only guarantee this for the standard library. I can similarly write a library with safe interfaces that can be (ab)used to cause UB and there's little that the Rust team can do. This is different from other "safe" languages. This is why it's so important to establish what the responsibility and expectation is of library developers to uphold the safety guarant…

Right, but if you write a library using only Rust "safe" code, the guarantee is back on the Rust team. To me, it would be better if libraries using unsafe code were marked.

Here's[0] an interesting idea for forcing crates using unsafe code to be handled specially, while allowing some "blessed" crates through without the special handling.

[0]: https://github.com/rust-lang/cargo/issues/934#issuecomment-6...

Re: Unsafe Rust: An Intro and Open Questions

#18
post #16

"Unsafe" is an escape hatch used for a number of reasons. There are good ones and bad ones. Bad ones include: - "I'm so l33t I don't need the compiler to check me." (Don't hire those guys.) - "Safe code is too slow". (File bugs on the compiler's optimizer.) - "Porting this to safe code would require a redesign". (See the Rust port of DOOM.) Most of the real needs for "Unsafe" in Rust come from - The need to interface…

I've run into two kinds of problems that require unsafe so far.

The first is similar to the c extension you described - building data structures with dynamically-sized arrays eg a bitfield followed by population_count(bitfield) entries. It would be great to have some way to express this without having to pay a whole usize for DST eg:

    struct Node{
        keys: u8,
        vals: [ShortPointer; popcount(keys)],
    }
The second is dealing with recursive data-structures or algorithms. Even for tree-shaped stuff, if you are hanging onto state as you walk the tree there are some kinds of patterns that the borrow checker just can't deal with eg

    fn join_step(state: &mut Vec, ..) {
        ...
        for values in primitive.eval_from_join(&arguments[..], &state[..]).into_iter() {
            // promise the borrow checker that we will pop values before we exit this scope
            let values = unsafe { ::std::mem::transmute::, &'a Vec>(&values) };
            push_all(state, values);
            if join.constraints[ix].iter().all(|constraint| constraint.is_satisfied_by(&state[..])) {
                join_step(state, ...)
            }
            pop_all(state, values);
        }
    }
It would be nice to have some finer-grained way of making this promise - transmute is overkill and leaves me open to all kinds of mistakes.

Re: Unsafe Rust: An Intro and Open Questions

#19
post #18
post #16

"Unsafe" is an escape hatch used for a number of reasons. There are good ones and bad ones. Bad ones include: - "I'm so l33t I don't need the compiler to check me." (Don't hire those guys.) - "Safe code is too slow". (File bugs on the compiler's optimizer.) - "Porting this to safe code would require a redesign". (See the Rust port of DOOM.) Most of the real needs for "Unsafe" in Rust come from - The need to interface…

I've run into two kinds of problems that require unsafe so far. The first is similar to the c extension you described - building data structures with dynamically-sized arrays eg a bitfield followed by population_count(bitfield) entries. It would be great to have some way to express this without having to pay a whole usize for DST eg: struct Node{ keys: u8, vals: [ShortPointer ; popcount(keys)], } The second is dealin…

If you're just trying to demote a lifetime, you should just be able to specify the lifetime on the variable:

    let values: &'a Vec = &values;
Lifetimes have variance so that you can always put a "bigger" lifetime in a place expecting a "smaller" one safely and it will treated as the smaller one forever.

Although it can be a bit of a dangerous game trying to "set" lifetimes manually because lifetime variance is hell (at least to me).

Re: Unsafe Rust: An Intro and Open Questions

#20
post #19
post #18

Earlier quoted context omitted.

I've run into two kinds of problems that require unsafe so far. The first is similar to the c extension you described - building data structures with dynamically-sized arrays eg a bitfield followed by population_count(bitfield) entries. It would be great to have some way to express this without having to pay a whole usize for DST eg: struct Node{ keys: u8, vals: [ShortPointer ; popcount(keys)], } The second is dealin…

If you're just trying to demote a lifetime, you should just be able to specify the lifetime on the variable: let values: &'a Vec = &values; Lifetimes have variance so that you can always put a "bigger" lifetime in a place expecting a "smaller" one safely and it will treated as the smaller one forever. Although it can be a bit of a dangerous game trying to "set" lifetimes manually because lifetime variance is hell (at…

I think that I'm actually doing the opposite - the lifetime of the state vec is longer than the lifetime of values but it's ok in this case because I pop the values again before they die. What I want to do is temporarily demote the lifetime of the state vec for the recursive call.

Here is a really simplified version that illustrates the problem - http://is.gd/8AZvZe

Post reply on HN