Skip to content

Fix uninitialized members, deferred-invalidation reset, and erase-remove idiom#45

Open
harryzz wants to merge 1 commit into
jcmosc:mainfrom
harryzz:fix-latent-bugs
Open

Fix uninitialized members, deferred-invalidation reset, and erase-remove idiom#45
harryzz wants to merge 1 commit into
jcmosc:mainfrom
harryzz:fix-latent-bugs

Conversation

@harryzz

@harryzz harryzz commented Jun 25, 2026

Copy link
Copy Markdown

These are latent on a fresh/zeroed heap but corrupt under heap reuse. I hit them
running Compute under sustained allocation churn (on a 32‑bit target, where freed
memory is reused aggressively). All are platform‑agnostic C++ correctness bugs; each
fix is one line.

Uninitialized members

Declared with no initializer and not set in the constructor (a = default constructor
or member‑init list leaves them indeterminate), then read as pointers, presence checks,
or lazy‑init guards. Found via clang-tidy cppcoreguidelines-pro-type-member-init.

  • indirect_pointer_vector::_data (backs Subgraph::_parents) — 0 == empty;
    garbage here yields a bogus parent pointer → out‑of‑bounds deref.
  • Graph::_main_handler (+ _main_handler_context) — has_main_handler() returns
    _main_handler != nullptr; garbage → call_main_handler() invokes a junk function pointer.
  • Graph::_keys — lazy‑init if (_keys == nullptr) _keys = new KeyTable and
    ~Graph: if (_keys) delete _keys; garbage skips init / deletes a junk pointer.
  • Graph::_trace_recorder — read as if (_trace_recorder).
  • Graph::{_deferring_subgraph_invalidation, _needs_update},
    Context::{_graph_version, _needs_update, _invalidated},
    Subgraph::{_traversal_seed, _index, _flags, _descendent_flags, _dirty_flags, _descendent_dirty_flags}.

UpdateStack deferred-invalidation reset

The "end deferring on exit" option is combined with & (which clears the bit) instead
of | (which sets it):

// before
_options = IAGGraphUpdateOptions(_options & IAGGraphUpdateOptionsEndDeferringSubgraphInvalidationOnExit);
// after
_options = IAGGraphUpdateOptions(_options | IAGGraphUpdateOptionsEndDeferringSubgraphInvalidationOnExit);

The destructor checks that bit to clear _deferring_subgraph_invalidation. Because the
bit is cleared instead of set, the destructor never resets the flag, so after the first
attribute update
the graph stays in the deferring state permanently. Every later
Subgraph::invalidate() then takes the deferred path — which is only flushed in the
non‑deferred branch — so the invalidated subgraphs and their values are never reclaimed
(a leak).

erase-remove idiom

In Subgraph::remove_child / invalidate_now / remove_observer and the Graph
subgraph/trace removal, the erase‑remove uses the single‑element erase(it) instead of
the range erase(it, end()), leaving stale tail elements whenever more than one element
is removed:

auto it = std::remove_if(v.begin(), v.end(), pred);
v.erase(it);                 // before — removes one element
v.erase(it, v.end());        // after  — removes the whole stale tail

Verification

The engine, dataflow, and heavy subgraph‑churn workloads run clean after these fixes on
both 64‑bit (macOS/Linux) and a 32‑bit build. 7 files changed, all one‑line edits.

…ove idiom

These are latent on a fresh/zeroed heap but corrupt under heap reuse (found while
running Compute under churn on a 32-bit target; all are platform-agnostic).

Uninitialized members (a `= default` ctor / member-init list leaves them indeterminate;
they're read as pointers, presence checks, or lazy-init guards):
- indirect_pointer_vector::_data (backs Subgraph::_parents): 0 == empty; garbage here
  yields a bogus parent pointer.
- Graph::_main_handler (+_main_handler_context): has_main_handler() == (_main_handler !=
  nullptr); garbage -> call_main_handler() invokes a junk function pointer.
- Graph::_keys: lazy-init `if (_keys == nullptr) _keys = new KeyTable` + ~Graph
  `if (_keys) delete _keys` -> garbage skips init / deletes a junk pointer.
- Graph::_trace_recorder: read as `if (_trace_recorder)`.
- Graph::_deferring_subgraph_invalidation, _needs_update; Context::{_graph_version,
  _needs_update, _invalidated}; Subgraph::{_traversal_seed, _index, _flags,
  _descendent_flags, _dirty_flags, _descendent_dirty_flags}.

UpdateStack deferred-invalidation reset: the "end deferring on exit" flag was combined
with `&` (which clears it) instead of `|` (which sets it). The destructor checks this
flag to clear _deferring_subgraph_invalidation, so after the first attribute update the
graph stays in the deferring state permanently, and every later Subgraph::invalidate()
takes the deferred path that is only flushed in the non-deferred branch -> invalidated
subgraphs and their values are never reclaimed.

erase-remove idiom (Subgraph::remove_child, invalidate_now, remove_observer; Graph
subgraph/trace removal): the erase-remove uses the single-element erase(it) instead of
the range erase(it, end()), leaving stale tail elements whenever more than one element
is removed.
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.

1 participant