resetConcentrations() (and any other caller of System::destroyAllMolecules(),
e.g. the .rnf reset_system command) crashed the process. MoleculeList is a
fixed-capacity object pool: create() hands back a pre-allocated Molecule from
its array and bumps a counter, remove() only unbinds/decrements, and
~MoleculeList() owns and frees every slot in [0, capacity).
MoleculeType::removeAllMolecules() additionally called `delete mol` on each
pooled molecule. That left a dangling pointer in the pool, so the next
genDefaultMolecule()/create() returned freed memory (use-after-free) and
~MoleculeList() then freed it again (double free). In parallel,
destroyAllMolecules() called clearAllComplexes(), deleting the Complex objects
the recycled pool molecules still referenced by ID_complex.
Fix: removeAllMolecules() tears each molecule down (unbind, drop from
observables/reactions, mark dead, decrement) without deleting it, leaving the
object in the pool for reuse. destroyAllMolecules() no longer deletes the
complex list — unbinding already routes through
Complex::updateComplexMembership(), which splits each former complex back into
singletons, so the pooled molecules stay paired with valid complex IDs and the
available-complex queue stays consistent across the destroy/recreate cycle.
After this, save_concentrations -> mutate -> reset_concentrations -> simulate
round-trips correctly on models with bonded complexes (verified with and
without complex bookkeeping) and the System tears down without a double free.
Problem
resetConcentrations()(and any other path throughSystem::destroyAllMolecules(), e.g. the.rnfreset_systemcommand)crashes the process — typically a SIGSEGV inside
SystemSnapshot::restore(),or a double free at teardown. It reproduces on any model: save state, then
reset, with no mutation in between.
Root cause
MoleculeListis a fixed-capacity object pool:create()hands back a pre-allocatedMoleculefrom its internal array andbumps a counter — it does not allocate per molecule.
remove()only unbinds and swaps-and-decrements the live count.~MoleculeList()owns anddeletes every slot in[0, capacity).MoleculeType::removeAllMolecules()additionally calleddelete molon eachpooled molecule. That leaves a dangling pointer in the pool, so the next
genDefaultMolecule()/create()returns freed memory (use-after-free), and~MoleculeList()later frees it again (double free).In parallel,
destroyAllMolecules()calledclearAllComplexes(), deleting theComplexobjects that the recycled pool molecules still reference byID_complex— stranding them on the next restore.Fix
removeAllMolecules()tears each molecule down (unbind, drop fromobservables/reactions, mark dead, decrement) without deleting it, leaving
the object in the pool for reuse.
destroyAllMolecules()no longer deletes the complex list. Unbinding alreadyroutes through
Complex::updateComplexMembership(), which splits each formercomplex back into singletons, so the pooled molecules stay paired with valid,
in-range complex IDs and the available-complex queue stays consistent across
the destroy/recreate cycle.
Two files, +22/-6, comments only besides the two removed lines.
Verification
save_concentrations→ mutate →reset_concentrations→ simulate round-tripscorrectly on models with bonded complexes (verified both with and without
complex bookkeeping /
-cb), the restored observable counts match the savedstate exactly, and the
Systemtears down without a double free.