Skip to content

fix(places): use-after-free and NaN centroid in RegionGrowingTraversabilityClustering#152

Merged
nathanhhughes merged 2 commits into
MIT-SPARK:developfrom
harelb:fix/rg-traversability-heap-and-nan
Jul 6, 2026
Merged

fix(places): use-after-free and NaN centroid in RegionGrowingTraversabilityClustering#152
nathanhhughes merged 2 commits into
MIT-SPARK:developfrom
harelb:fix/rg-traversability-heap-and-nan

Conversation

@harelb

@harelb harelb commented Jul 4, 2026

Copy link
Copy Markdown

These were both discovered while I was working on some of the image keyframing code which does a lot more work in the heap.

Summary:
Two heap/correctness bugs in the region-growing traversability clustering, found with AddressSanitizer.

1. Use-after-free / iterator invalidation in mergeRegions

mergeRegions iterates region.neighbors while region.merge() calls neighbors.erase(other.id) on that same map. That erase invalidates the loop iterator and frees the node the loop’s neighbor_id refers to, which graph.removeNode(neighbor_id) then reads. (ASan: heap-use-after-free via _Rb_tree erase; report available on request.)

Fix (root cause, not the symptom): Region::merge no longer erases from neighbors — it only inserts, and std::map insertion never invalidates existing iterators. mergeRegions now walks region.neighbors with an explicit iterator and erases the stale and merged-away neighbors in place (n_it = neighbors.erase(n_it)).

2. NaN centroid in Region::computeBoundary

centroid /= exterior_boundary.size() divides a zero vector by 0 when a region has no exterior boundary, producing a NaN place position. Fix: guard the division (leave the centroid at zero when the boundary is empty).

Both fixes verified on infinite corridor bags — the map (agents/places/objects) which were previously crashing.

harelb added a commit to harelb/Hydra that referenced this pull request Jul 4, 2026
mergeRegions held a reference (neighbor_id) into region.neighbors, but
region.merge() erases it, leaving a dangling reference read by the
subsequent graph.removeNode() call (heap-use-after-free). Copy it by
value first. Also guard computeBoundary against an empty exterior
boundary, which produced a NaN centroid/place position.

Upstream: Hydra PR MIT-SPARK#152.
…bilityClustering

Two heap/correctness bugs in the region-growing traversability clustering,
found with AddressSanitizer.

1. Use-after-free / iterator invalidation in mergeRegions(). The loop iterates
   `region.neighbors` while `region.merge()` calls `neighbors.erase(other.id)`
   on that same map, invalidating the loop iterator and freeing the node the
   loop's `neighbor_id` refers to (read immediately after by graph.removeNode).
   Fix the root cause rather than the symptom: Region::merge no longer erases
   from `neighbors` (it only inserts, which never invalidates iterators), and
   mergeRegions() now iterates with an explicit iterator and erases the stale
   and merged-away neighbors in place.

2. NaN centroid in Region::computeBoundary(). `centroid /= exterior_boundary.size()`
   divides a zero vector by 0 when a region has no exterior boundary, producing
   a NaN place position. Guard the division.
@harelb
harelb force-pushed the fix/rg-traversability-heap-and-nan branch from e4933d7 to c6563aa Compare July 6, 2026 16:51

@nathanhhughes nathanhhughes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, thanks for catching this! Some style things that would be helpful

Comment on lines +233 to +236
// Use an explicit iterator so we can erase entries in place. Region::merge
// only inserts into region.neighbors (std::map insertion never invalidates
// iterators), so n_it stays valid across the merge; we remove the stale and
// merged-away neighbors here rather than inside merge().

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

drop comment, this isn't useful where it is

// iterators), so n_it stays valid across the merge; we remove the stale and
// merged-away neighbors here rather than inside merge().
for (auto n_it = region.neighbors.begin(); n_it != region.neighbors.end();) {
const NodeId neighbor_id = n_it->first;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

minor preference for structured binding like:

const auto [neighbor_id, num_connecting_voxels] = *n_it; 

Comment on lines +257 to +259
// Merge neighbor into this region, then remove it from regions_, the
// graph, and this region's neighbor map (in place, so no iterator is
// left dangling).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Something shorter like "merge neighbor into this region and then cleanup" is probably better

}

neighbors.erase(other.id);
// NOTE: other.id is intentionally NOT erased from `neighbors` here. Erasing it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Simpler comment would be better

Comment on lines +513 to +517
// Guard against an empty exterior boundary (e.g. a region with no boundary
// voxels): dividing a zero vector by 0 yields a NaN centroid/place position.
if (!exterior_boundary.empty()) {
centroid /= exterior_boundary.size();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think I would prefer this check to come earlier, i.e., after exterior_boundary = growRegion(boundary, farthest) I would put something like if (exterior_boundary.empty()) { LOG(WARNING) << "Empty region ecountered!"; } and have the region constructor default the position to something sane (like NaNs tbh). My understanding is that we shouldn't ever need to compute the centroid of a place that doesn't have exterior boundary voxels (and picking zero is a bad choice because you can't distinguish between a valid place at the origin and invalid places)

- drop/shorten comments per review; structured binding for neighbor iteration
- warn on empty region right after growRegion, and default Region::centroid to
  NaN so a boundary-less place stays distinguishable from a valid origin place
  (rather than guarding the division to leave it at zero)
@harelb
harelb marked this pull request as ready for review July 6, 2026 20:05
@nathanhhughes
nathanhhughes merged commit e5d33ee into MIT-SPARK:develop Jul 6, 2026
2 checks passed
harelb added a commit to harelb/Hydra that referenced this pull request Jul 7, 2026
Mirrors the upstream Hydra PR fixes onto the build branch:
- deformation_interpolator: stamp cache entries with the computed
  timestamp_ns (not last_update_time_ns, which is 0 for objects) and
  reset to init_pos before transform() so repeated deformation of
  archived nodes stays idempotent (upstream PR MIT-SPARK#153).
- region_growing_traversability_clustering: erase merged/stale
  neighbors in place with the loop iterator instead of erasing inside
  Region::merge, removing the dangling-reference hazard (upstream PR MIT-SPARK#152).
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