Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

The problem here looks like it is with Rc, which does require unsafe code. I haven't checked, but I don't think the destructors in the thread::scoped issue are unsafe. Even if they are, I can easily imagine another situation where a programmer assuming a safe destructor is always called causes a bug, which is exactly the problem with thread::scoped.

And Rc only looks like it is the problem. Rc is doing exactly what it says it does, as safely as it can. The fact that you can use it to avoid having a destructor called is a side-effect of it doing what it's supposed to do.

If main is safe, mem::forget is safe (remember, Rc is just playing the part of mem::forget), and the destructor for JoinGuard is safe (which it is; I just checked), where is the unsafe code?



`unsafe` is a stateful property, not a local one. The specific invariant here is that you cannot rely on destructors for memory safety, which the old thread::scoped API was doing. The actual unsafe code is actually here: https://github.com/rust-lang/rust/blob/master/src/libstd/thr...

(I myself have been guilty of this same error, of assuming that `unsafe` regardless of its position cannot have far-reaching effects on the overall state of your code.)

The important thing is this: using safe Rust, and using any safe stdlib API, memory unsafety is impossible. This includes stdlib APIs that use `unsafe` internally, such as `Rc`. Proving those interfaces safe is the burden of the Rust developers, and if you can show that there has been an error in these proofs then it is a drop-everything sky-is-falling defcon-1 situation that they must address, up to and including unreleasing the language if no suitable solution came to mind (fortunately, this is quite unlikely).


> where is the unsafe code?

In the thread spawning. The destructor itself isn't unsafe, but it was being used as a guard to enforce the safety of the threading code.

> I can easily imagine another situation where a programmer assuming a safe destructor is always called causes a bug, which is exactly the problem with thread::scoped.

There's a difference between "a bug" and "memory unsafety". If the bug is that the value the destructor guards isn't released (for example, if a File is leaked, the associated file descriptor will never be flushed and closed), then that's not Rust's problem. The programmer probably wanted that file descriptor to be closed, but there's no memory safety violations going on there, and in fact nothing worse than simply stuffing that File into a global array/hashtable and forgetting about it.

The problem with thread::scoped is actually fairly unique. The destructor is not doing much besides just calling join() on the forked thread. The problem is that the JoinGuard object doesn't actually control any access to anything at all, it exists purely as a proxy to represent any values (particularly stack values) that are borrowed by the forked thread. Because it's only a proxy, leaking the guard does not actually make the guarded values inaccessible.

I can't think of any situation besides multithreading that has this same problem. Every other situation involving a RAII object with a lifetime being leaked (and therefore outliving its lifetime, which is technically a violation of borrowck) by definition makes the object inaccessible, which means the data it's protecting cannot be reached. For example, a struct that contains a &-ref to some value may outlive the referenced value if the struct is leaked, but this is still memory-safe because the reference will never be dereferenced. If the reference can be dereferenced, that means code exists that has visibility on the struct, which means the struct hasn't actually been leaked yet, and as long as it hasn't been leaked it's still safe (e.g. because borrowck will still enforce the lifetimes).

Multithreading is special because the forked thread is what's actually accessing the values, without ever going through the RAII object. This means that borrowck cannot guarantee that the RAII object is still alive (and therefore that the "protected" values are still valid). This was considered to be ok as the RAII object would join the thread (therefore ensuring it had finished executing) before destructing, but the ability to leak objects means the join may not happen.

Ultimately, this means that JoinGuard is the moral equivalent of a Mutex that coexists with its protected data rather than owning it (thereby making it possible to write code that accesses the data without holding the lock). It's a bit better than that, because you have to actually go to some effort to leak the JoinGuard as opposed to just doing it accidentally, but the point is that borrowck cannot guarantee that the code that accesses the data does not outlive the guard.

And all of this comes back to my original point, which is that I can't think of another situation where it's actually unsafe to leak the RAII guard (as opposed to merely being a bug) without there being `unsafe` code in the guard itself. Note that the example given for PPYP, Vec::drain_range, uses `unsafe` code in its destructor.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: