Skip to content

Fix uninitialized and leaked libRoadrunner instance handle - #425

Open
drbergman wants to merge 3 commits into
MathCancer:developmentfrom
drbergman:fix-librr-uninitialized-handle
Open

Fix uninitialized and leaked libRoadrunner instance handle#425
drbergman wants to merge 3 commits into
MathCancer:developmentfrom
drbergman:fix-librr-uninitialized-handle

Conversation

@drbergman

@drbergman drbergman commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fix uninitialized and leaked libRoadrunner instance handle

RoadRunnerIntracellular::rrHandle is declared with no initializer, clone() returns a model with no RoadRunner instance, and nothing ever frees one.

A cell that changes type gets a handle that was never created. Only start() creates the instance, and it is called from exactly two places — Cell::divide() and create_cell( Cell_Definition& ). Cell::convert_to_cell_definition() assigns a phenotype and does not, so every cell transformation leaves the cell passing indeterminate memory to the RoadRunner C API.

Nothing is ever freed. freeRRInstance appears nowhere in the tree and the class declares no destructor, so every cell death and every conversion permanently orphans one instance.

The fix

  • clone() calls start(), so it returns a usable model. This matches MaBoSS and dFBA, which both initialize in their own copy constructors — dFBAIntracellular::start() is empty for exactly that reason.
  • A destructor frees rrHandle and result.
  • rrHandle gets an in-class initializer, which covers the pointer-taking constructor too.
  • Copy construction and assignment are deleted; with an owning destructor the implicit ones would double-free.

Because clone() is now self-sufficient, both existing start() calls in core/PhysiCell_cell.cpp are redundant and removed.

Verification

The stock ode_energy sample exercises neither bug — it has no cell death and no type conversion. Extended with a second cell definition carrying the same <intracellular> block, transformations in both directions, and matched birth/death holding ~144 agents:

scenario before after
no conversion, no death 127.9 MB, exit 0 121.4 MB, exit 0
conversions enabled crash 8/8 clean 6/6
death only, 2880 min 530.7 / 578.6 MB 109.1 / 108.8 MB

Also included: unknown SBML species

get_parameter_value() / set_parameter_value() indexed species_result_column_index with operator[], so an unknown species silently resolved to column 0 — reading, or writing, a different species than the caller named. Both now use find() and exit, as validate_SBML_species() already does at setup. See this comment.

Two bugs, both in the lifetime of the RoadRunner instance behind
RoadRunnerIntracellular::rrHandle.

A cell that changes type never gets its model started. Phenotype::operator= deep-copies
the intracellular model through clone(), but clone() returned a model with no RoadRunner
instance -- only start() creates one, and start() was called from exactly two places in
the engine, Cell::divide() and create_cell( Cell_Definition& ).
Cell::convert_to_cell_definition() assigns a phenotype and did not, so every cell
transformation left the cell holding whatever the recycled heap block contained.
rrHandle is declared with no initializer, so that is undefined memory; on glibc the block
comes back with its previous bytes, on Darwin it comes back zeroed.

Nothing was ever freed. freeRRInstance appears nowhere in the tree and the class declared
no destructor, so the implicit one destroyed the STL members and leaked the instance
behind the raw handle. Every cell death and every conversion orphaned one.

Measured with this branch's ode_energy sample extended with a second model-carrying cell
type, transformations in both directions, and matched birth/death holding the population
near 144 agents:

  conversions enabled   before: crash 8/8   after: clean 6/6
                        (5 churn seeds gave 4x SIGSEGV and 1x SIGABRT out of libmalloc)
  death only, 2880 min  before: 530.7/578.6/573.8 MB   after: 105.6/108.6/101.0 MB
  no conversion/death   before: 127.9 MB   after: 121.4 MB   (both exit 0)

The mixed SIGSEGV/SIGABRT pattern matches what an HPC user of this addon reported. Under
lldb the faulting frame is get_parameter_value() with rrHandle == 0x0 and a valid
sbml_filename, inside an OpenMP region.

The fix keeps the instance's whole lifetime inside the addon:

  clone() calls start() on the new model, after the field assignments it already does,
  since start() loads sbml_filename. It therefore hands back a usable model. This matches
  MaBoSS and dFBA, which both re-initialize their engines in their own copy constructors
  -- dFBAIntracellular::start() is empty precisely because of it. Requiring callers to
  call start() afterwards was libRoadrunner's requirement leaking into core.

  A destructor frees rrHandle and result. freeRRInstance and freeRRCData both accept null,
  verified against the shipped library, so no guards are needed.

  rrHandle gets an in-class initializer, which covers the pointer constructor too since it
  has no mem-initializer list.

Because clone() is now self-sufficient, core/PhysiCell_cell.cpp only loses code: both
existing start() calls are redundant and are removed. That also closes the remaining hole
-- the no-argument create_cell() reaches Cell::Cell(), which clones cell_defaults.phenotype
and previously left it unstarted.

The copy operations are deleted, since with an owning destructor the implicit ones would
shallow-copy the handle and double-free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@drbergman
drbergman requested a lite review from Copilot August 7, 2026 12:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@drbergman
drbergman marked this pull request as ready for review August 7, 2026 12:26
drbergman and others added 2 commits August 7, 2026 09:33
Keeps only the part that is not obvious from the code -- that start() has to run
after the field assignments, because it loads sbml_filename.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_parameter_value() and set_parameter_value() index species_result_column_index
with operator[], the inserting form. An unknown species name silently resolves to
column 0, so a typo -- or a model whose column map was never populated -- reads, and
writes, a different species than the caller named. A read also mutates the map.

Both accessors now look the species up with find() and, on a miss, report and exit.
Exiting rather than warning is deliberate: there is no value to return and nothing
sensible to write, so continuing would feed a fabricated number into the model and
every step after it. validate_SBML_species() already exits for exactly this condition
at setup; it just cannot cover these calls, because it only validates the mappings
declared in the XML and custom code calls the accessors directly with its own strings.

The lookup also moved ahead of getFloatingSpeciesConcentrations(), so a miss no longer
allocates a concentration vector it then discards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@drbergman drbergman Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rheiland I added these correctness checks in this commit onto PR #425. The rest of this PR should make this obsolete, but before a wrong species name would be given the default value (0) which meant it would do stuff to/with index 0 rather than throw.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants