Skip to content

Take spring attachments under the lock before iterating them - #428

Open
drbergman wants to merge 1 commit into
MathCancer:developmentfrom
drbergman:fix-spring-attachment-race-upstream
Open

Take spring attachments under the lock before iterating them#428
drbergman wants to merge 1 commit into
MathCancer:developmentfrom
drbergman:fix-spring-attachment-race-upstream

Conversation

@drbergman

@drbergman drbergman commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

dynamic_spring_attachments() runs inside the mechanics parallel-for and walks pCell->state.spring_attachments with no lock. That vector is not private to the thread: another thread running the same function for a neighbouring cell calls attach_cells_as_spring(), which reaches into this cell's spring_attachments and push_back()s.

Those writers serialize on the unnamed critical inside Cell::attach_cell_as_spring() / Cell::detach_cell_as_spring(). The read did not take it. A concurrent push_back that reallocates leaves the loop indexing a freed buffer, and the next iteration dereferences whatever it finds.

Change

Copy the vector under the same critical the writers use, then walk the copy. One hunk, one file, no API change. The existing loop form and traversal order are preserved.

The lock is deliberately not held across the loop body: detach_cells_as_spring() takes the same unnamed critical, and OpenMP criticals are not reentrant — holding it would deadlock.

Evidence

Reproduced on a user model that faults reliably. 60 runs per arm, 4 threads, -O3:

arm n crashes
baseline 60 10 (16.7%)
this change 60 0
baseline, 1 thread 30 0

Cost: +2.4% wall time — paired median ratio 1.0237, slower in 9 of 9 paired blocks, and independently 1.0237 in a separate campaign.

Scope

This fixes the crash, and only the crash. Two things are deliberately left alone, both predating this patch:

  • The attachment half still reads .size() outside the lock (three places), so the function is not data-race-free. It is crash-free. TSan will still flag it.
  • attach_cells_as_spring() / detach_cells_as_spring() each take the critical twice, once per cell, so a pair operation is not atomic. An interleaving can leave a one-way spring — A pulls toward B while B feels nothing, since standard_elastic_contact_function applies force to pC1 only.

dynamic_spring_attachments() runs inside the mechanics parallel-for and walks
pCell->state.spring_attachments with no lock. That vector is not private to this
thread: another thread running the same function for a neighbouring cell calls
attach_cells_as_spring(), which reaches into THIS cell's spring_attachments and
push_back()s. Those writers serialize on the unnamed critical inside
Cell::attach_cell_as_spring() and Cell::detach_cell_as_spring(); the read did
not take it. A concurrent push_back that reallocates leaves the loop indexing a
freed buffer, and the next iteration dereferences whatever it finds.

The result is an intermittent SIGSEGV in dynamic_spring_attachments, inside the
OpenMP outlined region, faulting on a garbage address. It is thread-count
dependent and therefore invisible on single-threaded runs, which is why it
survives casual testing.

Copy the vector under the same critical the writers use, then walk the copy. The
lock is deliberately not held across the loop body: detach_cells_as_spring()
takes the same unnamed critical, and OpenMP criticals are not reentrant.

Measured on a model that reproduces the fault reliably, 60 runs per arm at 4
threads: 10 crashes without the change, 0 with it. Cost is +2.4% wall time
(paired median ratio 1.0237, slower in 9 of 9 paired blocks).

This addresses the crash only. The function still reads .size() outside the lock
in the attachment half, so it is not yet data-race-free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@drbergman
drbergman marked this pull request as ready for review August 10, 2026 02:02
Copilot AI lite review requested due to automatic review settings August 10, 2026 02:02

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.

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