Take spring attachments under the lock before iterating them - #428
Open
drbergman wants to merge 1 commit into
Open
Take spring attachments under the lock before iterating them#428drbergman wants to merge 1 commit into
drbergman wants to merge 1 commit into
Conversation
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
marked this pull request as ready for review
August 10, 2026 02:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
dynamic_spring_attachments()runs inside the mechanics parallel-for and walkspCell->state.spring_attachmentswith no lock. That vector is not private to the thread: another thread running the same function for a neighbouring cell callsattach_cells_as_spring(), which reaches into this cell'sspring_attachmentsandpush_back()s.Those writers serialize on the unnamed
criticalinsideCell::attach_cell_as_spring()/Cell::detach_cell_as_spring(). The read did not take it. A concurrentpush_backthat 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: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:
.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, sincestandard_elastic_contact_functionapplies force topC1only.