Bronze GC and aliasing problems

I saw a new garbage collector implementation mentioned in TWIR 411: Bronze GC along with an arXiv preprint (which is primarily focused on how much does this GC make learning rust and working with references easier). I gave it a closer look as I am interested in any new development in rust GCs (as well as GATs/HKTs which would e.g. permit us to abstract over various GC implementations and more; but that was already discussed in other threads) so I gave it a closer look and was confused by some of the design and wanted to discuss it.

The authors decided to allow to take two mutable references to one object:

With Bronze, mutation is permitted through all references to each garbage-collected object, with no extra effort. A key tradeoff is that Bronze does not guarantee thread safety; as in other garbage-collected languages, it is the programmer’s responsibility to ensure safety.

Indeed, one can do e.g. (requires nightly):

extern crate bronze_gc;
use bronze_gc::GcRef;

fn main() {
    let mut gr1 = GcRef::new(vec![1u16,2,3]);
    let mut gr2 = gr1.clone(); // clones the reference, not the Vec

    let ref1: &mut Vec<u16> = gr1.as_mut();
    let ref2: &mut Vec<u16> = gr2.as_mut();
    // ref1 and ref2 now mutably reference the same object:
    ref1.push(4);
    ref2.push(5);
    ref1.push(6);
    dbg!(&ref1, &ref2); // both are [1, 2, 3, 4, 5, 6]
}

I believe that this design decision does not only pose an issue in multi-threaded environments (which they mitigate by making GcRef<_> both !Send and !Sync) but also breaks rust safety guarantees in general. Is that correct or am I missing something? If it is unsafe, is it actually possible to use it safely, or do UBs (and actual bugs) lurk around the corners in such a situation? How could it be fixed (e.g. minimally redesigned) to make it a safe GC?

Edit: Created an issue with a use-after-free example. Also noticed that someone pointed this out on reddit earlier.

7 Likes

Yes, this us wildly unsound. Creating two unique references to the same value at the same time is instant UB

14 Likes

With Bronze, mutation is permitted through all references to each garbage-collected object, with no extra effort.

This quote is disturbing. The authors seem to think that preventing shared mutation is a problem with Rust, rather than one of its most important features.

17 Likes

The bronze code looks mostly like a copy-paste of rust-gc, with unsound methods added to create aliased mutable references..

Also, I don't know if maybe manishearth has licensed the code separately to them, but bronze released obviously copied code under a different license (BSD instead of MPL-2).

Granted, it's just a hacked together stub API (with no actual collector) in order to conduct the research study, but I find the entire premise of the study quite odd. The study seems to be trying to answer the question "is Rust easier to learn if mutable references are not unique?" Is that even worth answering? Pretty much everything about Rust would be unsound with shared mutable references.

15 Likes

Some of the code is indeed from rust-gc, with appropriate attribution. My understanding is that the licenses are compatible, but if I'm wrong about this, I could look into changing the license.

The Bronze project is exploring the usability costs of the restrictions that Rust imposes. Indeed, if one uses as_mut() and as_ref() inappropriately, one can do unsafe things. As the project proceeds, I think it will be worth considering (1) dynamic safety enforcement, via a mechanism similar to the Ref/RefMut one used by RefCell; (2) a mechanism to avoid exposing references into the interior of objects owned by the garbage collector.

IANAL;

The licenses are compatible in the sense that you may include code licensed under both in a single distribution. However the MPL is still a copy-left license, and code present in files licensed under the MPL is not allowed to be re-licensed under a more permissive license like BSD.

The files licensed under MPL and any modifications to those specific files must continue to be licensed under the MPL. You may even be required to retain the file header specifically.

Them being "compatible" just means you are allowed to include additional files licensed under BSD, it doesn't change the terms of the code from the original files.

4 Likes

Interesting, so like an automatic RefCell without the Ref(Mut)? It sounds like it'd end up basically like the stacked borrows implementation in miri..

Still, the study allowed students to create multiple live mutable references simultaneously though, which is UB in Rust and contradictory to most of the Rust code currently written in the ecosystem (including libstd), making Bronze essentially a new language. The simple Vec::push example above shows why.

1 Like

Indeed, this is language design research. But I view this as an early-stage step toward compatibility with Rust, not with the goal of developing a fundamentally different end language. But as you say, there is still substantial work to be done before something like this could be considered for adoption in Rust.

Some people have said things like: "there's no need for GC in Rust, because you can already do everything you need to do." What we see in this work is that there is a significant usability cost to that position. Can we build a GC that meets all of Rust's design requirements? That's still a work in progress.

2 Likes

Note that certain files are indeed marked with an MPL license because they are derived from corresponding projects. If I've missed something, I would be happy to fix it.

It's mostly this file which seems to be missing MPL, and I believe you're supposed to include the actual text of their copyright header somewhere.

But obviously it's your judgement call, this is just my guess!

I don't know, this Bronze idea seems a bit wrong-headed to me.

Rust offers you speed AND safety. The high performance comes largely from the efficient use of memory, avoiding garbage collection. I guess that's a good question actually, where does the performance come from?

But regardless, yes, it takes a little time to learn how Rust works, and how to use it, but I don't think Bronze is the right way forward! It's throwing all the unique advantages of Rust away. If you want a safe**, easy language, use say C#. But don't expect it to run so fast.

** Although C# doesn't give you safe concurrent programming the way Rust does.

7 Likes

There are already perfectly valid solutions to the "usability costs" of Rust: use Go or any of the adjacent languages with GC, rather than have a go at Rust :innocent:. Rust is defined by its safety and deterministic memory management.

5 Likes

I think the idea is promising and would be interested in seeing how it turns out!

However, because the ergonomics you are looking into is intrinsically linked to the way mutability and sharing interact, they shouldn't be pushed off as something that should be researched later on.

Otherwise, the research will boil down to just another Rc<T> implementation.

3 Likes

Not my place to moderate but I would like to note there seem to be (at least) two broad question directions discussed here:

  • Is the Bronze GC direction safe/viable at all and why? How is the experiment on usability (and teaching) relevant with multiple mutable refs? etc.
  • Does Rust need a GC / shoul it have a GC? Is Rust better off without GC? etc.

I would think that the firs direction may be more interesting to pursue here - is there perhaps a cleaner argument for what is definitely not possible? The second direction, while also a good question in its own right, has been asked discussed many times elsewhere and seems possibly loaded.

(My own take: Most of the Rust ecosystem avoiding GC with clever and efficient memory management is great - as well as the "virtue" of simplicity and efficiency - but there may be situations where a GC would fit the bill (e.g. complex applications now riddled with Rc<RefCell<_>>, also discussed here), so I am excited about people researching GCs in Rust.)

4 Likes

I can wrap my head around Gc<RefCell<_>>. An alternative to Rc and Arc, I get that. Trying to eliminate the RefCell, though, and do it safely? I get lost there.

3 Likes

Ah, I see that I did not write that very clearly.

I would also think that eliminating the RefCell (resp gc::GcCell) from Gc<RefCell<_>> is not possible in general (or at least not so easily/directly) and I was merely arguing for the value of GC research and development in Rust in general. (In addition to pointing out "critique of Bronze" vs "critique of GCs in Rust in general")

2 Likes

That is a reasonable justification for me, but is this discussed in the paper? The paper makes it sound like Bronze could simply make Rust easier to use, but the question that is really studied here is whether Bronze makes it easier to write Rust code at the cost of also making it easier to cause bugs and security vulnerabilities. Bronze gives up on one of the fundamental promises of Rust -- that without unsafe, the program is memory and thread safe. This is the kind of issue which is often handled via a security advisory in the Rust SecDB (there is a special category informational = "unsound" there). The crate README should at least point this out, if it is a known problem.

I searched for a discussion of this trade-off in this version of the paper and could not find one; my apologies if I just overlooked it.

6 Likes

We'll talk about this a bit in the camera-ready version of the paper. Note, however, that we believe this could be made safe via a dynamic mechanism, though for now that's future work. Do you agree that that sounds feasible?

That is good to hear, thanks. :slight_smile:

Any GcRef<T> that only supports shared references can be used to define a MutGcRef<T> := GcRef<RefCell<T>>, which provides some amount of mutation, albeit at the cost of thread safety as well as time and space overhead. (RwLock fixes thread safety but increases the overhead.) So something is possible, but it moves further away from the language Rust is meant to be by adding more overhead. It also introduces possible runtime failures on each object access if the dynamic check fails, leading to a whole new class of bugs -- bugs that can be hard to test for since triggering reentrancy might require very peculiar inputs. Also this API would be different from your as_mut since it would return some kind of guard that updates the dynamic state when it is droped. This can be a serious ergonomic downside. (Or maybe you are thinking of some other way to dynamically check this, that does not require a guard? That would be quite cool, but I am not sure how it is supposed to work. And the other issues would remain.)

So basically, this language would not quite work like the usual GC languages, and it would have new failure modes not present in other GC languages. RefCell is considered as 'avoid whenever possible' for a reason -- reliability is a core value of Rust, and having random panics interrupt the service goes against that.

All that makes me wonder what the lesson is that one would learn from a study which shows that this language is easier to learn than Rust. It is not very surprising that giving up on the zero-cost principle can help improve ergonomics. But how can that lesson help improve Rust which heavily emphasizes zero-overhead? And if we are okay with some baseline amount of overhead, we probably don't want "Rust but with a GC"; there are other design decisions of Rust that probably also should be reconsidered in that case. See for example this blog post for some musings on a simpler Rust-style language.

7 Likes

Most code is not particularly performance-sensitive; previous studies showed that GC, when applied judiciously, has minimal performance impact (see the Cyclone paper). In general, I think ruling out GC for all programs is a premature optimization IF it is possible to gradually (i.e. partially) strategically reduce the usage of GC when the necessity arises. This is what Bronze is intended to enable.

So yes, making this safe would involve additional overhead. The community has accepted that runtime overhead is worthwhile in certain cases: dynamic dispatch is a good example. And yes, there are cases where even that overhead is problematic. But then, of course one wouldn't use GC in code that is known to be performance-critical; it's better to pay the usability cost. Highly-optimized code is typically less readable, harder to write, and bug-prone anyway; we do it when it's worth it.

My vision is that typically, one would not hold dynamic borrows very long. If programming in an OOP style, one would typically pass GcRefs around, and then inside the body of mutating methods, one would borrow, do the mutation, and discard the borrow. So, if programming in that style, runtime errors due to multiple borrows should be rare. My intuition could be wrong here, I admit. Perhaps it could be possible to use a linter-style approach to identify most of the cases in which this would be likely, but I realize that we're in dynamic-land and full guarantees are not going to be possible in general.

I don't view the Bronze project as proposing a fundamental change to Rust. I view it as proposing an additional usage technique to enable gradual adoption of the faster, safer Rust memory management techniques, and as highlighting for the community the real usability costs of the current approach in Rust.