https://manishearth.github.io/blog/2015/09/01/designing-a-gc-in-rust/ https://github.com/Manishearth/rust-gc/
lxr: https://arxiv.org/pdf/2210.17175
other implementation:
https://github.com/fitzgen/bacon-rajan-cc
https://github.com/asajeffrey/josephine/
https://github.com/withoutboats/shifgrethor
https://gist.github.com/Manishearth/70856e2f01e18935681c
they forked the thing:
https://soft-dev.org/pubs/html/hughes_tratt__garbage_collection_for_rust_the_finalizer_frontier/
GC-fuzz: Build it as a testing harness crate, not as a GC crate. That keeps scope sane and makes the “Antithesis in spirit” part explicit: perturb, assert, replay, shrink.
Shape Call it something like gc-fuzz, gc-schedule, or gc-check.
Split it into four pieces:
-
event Defines the observable moments in a collector or mutator:
pub enum GcEvent { Alloc { bytes: usize, object_id: u64 }, RootAdded { root_id: u64 }, RootRemoved { root_id: u64 }, WriteBarrier { from: u64, to: u64 }, Safepoint, CollectionStart, CollectionEnd, }
-
schedule Decides what perturbation to apply:
pub enum Decision { Noop, CollectNow, VerifyHeap, PoisonFreed, }
pub trait Schedule { fn on_event(&mut self, event: &GcEvent) → Decision; }
-
recorder Logs decisions so failures are reproducible:
pub struct TraceStep { pub event_index: u64, pub event: GcEvent, pub decision: Decision, }
pub trait Recorder { fn record(&mut self, step: &TraceStep); }
-
oracle Verifies invariants:
pub trait HeapOracle { type Error;
fn verify_pre_gc(&self) -> Result<(), Self::Error>; fn verify_post_gc(&self) -> Result<(), Self::Error>;}
Minimal API Your collector integrates at safepoints:
pub trait HarnessHost { type Error;
fn collect_now(&mut self) -> Result<(), Self::Error>;
fn verify_heap(&mut self) -> Result<(), Self::Error>;
fn poison_freed(&mut self) -> Result<(), Self::Error>;
}
Then the harness itself is just:
pub struct Harness<S, R> { schedule: S, recorder: R, event_index: u64, }
impl<S: Schedule, R: Recorder> Harness<S, R> { pub fn on_event<H: HarnessHost>( &mut self, host: &mut H, event: GcEvent, ) → Result<(), H::Error> { let decision = self.schedule.on_event(&event); self.recorder.record(&TraceStep { event_index: self.event_index, event, decision, }); self.event_index += 1;
match decision {
Decision::Noop => Ok(()),
Decision::CollectNow => host.collect_now(),
Decision::VerifyHeap => host.verify_heap(),
Decision::PoisonFreed => host.poison_freed(),
}
}
}
That is the right abstraction boundary. The harness decides when to perturb. The GC still owns how collection works.
First three scheduler modes These are enough for v1:
- Stress Trigger CollectNow on every alloc and safepoint.
- Seeded Deterministic PRNG, decisions derived from seed plus event count.
- Replay Read a previously recorded TraceStep sequence and emit exactly the same decisions.
Do not start with “adaptive”. That is a production policy problem, not a testing tool problem.
The feature that actually matters Add a trace shrinker. Given a failing replay trace, minimize it.
Representation:
pub struct DecisionTrace(pub Vec
pub struct DecisionPoint { pub event_index: u64, pub decision: Decision, }
Shrinking strategy:
- Remove half the decision points, replay.
- If still fails, keep the smaller half.
- If not, restore and try smaller chunks.
- Once coarse minimization finishes, try dropping single points.
- Report the minimal failing schedule.
This is just delta debugging, boring in the good way. That is the part that feels most “Antithesis”.
What properties to check For a non-moving mark/sweep collector, start with these:
- Every root points to a live object after collection.
- Every reachable object remains allocated after collection.
- No edge points to freed memory.
- Running GC twice with no mutation yields the same live set.
- Freed objects are not reachable from any root.
- Finalizers run at most once.
For a moving collector, add later:
- Every forwarded pointer resolves to a valid to-space object.
- No stale from-space pointer survives in roots or fields.
- Barrier invariants hold before and after each phase.
How to test the harness Do not mock the world into nonsense. Build a tiny real heap model:
- object arena as Vec
- object fields as Vec
- real root set
- simple mark/sweep implementation
- intentionally buggy variants behind test cfg
Then write end-to-end tests:
- stress_finds_missing_root_bug Simulate a root registration bug that only fails if collection lands between alloc and root_add.
- seed_reproduces_failure Save failing seed, rerun, assert same failure.
- replay_reproduces_failure Record a trace, replay it, assert identical failure.
- shrink_reduces_trace Start from a long failing trace, shrink it, assert the smaller one still fails.
That gives you real proof, not brochureware.
Two-week plan Week 1:
- Implement GcEvent, Decision, Schedule, Recorder, HarnessHost.
- Add Stress, Seeded, and in-memory Recorder.
- Build the tiny reference mark/sweep heap.
- Write one intentionally buggy scenario and make Stress catch it.
Week 2:
- Add Replay.
- Add trace serialization with serde if you want CLI ergonomics.
- Implement delta-debug style shrinking.
- Write one post: “A failing GC schedule, recorded and minimized”.
What not to do
- Do not start with concurrent GC.
- Do not start with a brand new algorithm.
- Do not start by integrating five runtimes.
- Do not oversell “determinism” if allocator layout and threads are still nondeterministic.
Best framing Pitch it like this:
“A property-driven GC testing harness for Rust collectors. It forces collections at hostile times, records the schedule, replays failures exactly, and shrinks failing schedules to minimal repros.”