Skip to content

Panic “Matrix slicing out of bounds” in Multibody::fill_jacobians on first multibody contact when the parallel feature is enabled #954

Description

@twehler

Affected versions: 0.32.0, 0.33.0, current master (7e72d13). 3D, f32 (2D presumably identical — same code path).

Summary

With the parallel feature enabled, the first contact involving a multibody
link panics:

thread '<unnamed>' panicked at nalgebra-0.35.0/src/base/matrix_view.rs:310:9:
Matrix slicing out of bounds.
   ...
   7: rapier3d::dynamics::joint::multibody_joint::multibody::Multibody::fill_jacobians
   8: rapier3d::dynamics::solver::contact_constraint::generic_contact_constraint::GenericContactConstraintBuilder::generate
   9: rapier3d::dynamics::solver::contact_constraint::contact_constraints_set::ContactConstraintsSet::init
  10: rapier3d::dynamics::solver::velocity_solver::VelocitySolver::init_constraints
  11: rapier3d::dynamics::solver::island_solver::IslandSolver::init_and_solve
  12-19: rayon ...

Serial builds are unaffected.

Root cause

generic_contact_constraint.rs (and the same pattern in
generic_joint_constraint_builder.rs) gates the on-demand jacobian-buffer
resize on the feature flag:

if jacobians.nrows() < required_jacobian_len && !cfg!(feature = "parallel") {
jacobians.resize_vertically_mut(required_jacobian_len, 0.0);
}

This guard appears to be a leftover from the old intra-island parallelism
(where the buffer was shared across tasks and could not be resized inside
generate). The current solver parallelizes across islands only — the
comment in physics_pipeline.rs says intra-island parallelism "hasn't been
ported to the new solver yet" — and each rayon task owns its IslandSolver
(and therefore its ContactConstraintsSet::generic_jacobians) exclusively via
par_iter_mut. Nothing else ever sizes generic_jacobians (it is created as
DVector::zeros(0) and only fill(0.0) is called on it), so under
feature = "parallel" it stays 0-length forever and the first
fill_jacobians slices out of bounds.

Since the buffer is task-exclusive under the current design, the resize is
race-free and the guard can simply be removed (see attached patch/PR, which
also adds a regression test).

Reproduction

Any scene where a multibody link touches anything, built with
--features parallel:

let mut world = PhysicsWorld::new();

// Free-standing fixed ground (NOT part of any multibody).
world.insert(
RigidBodyBuilder::fixed(),
ColliderBuilder::cuboid(50.0, 0.5, 50.0),
);

// A 4-link dynamic chain on spherical joints, starting above the ground.
let mut prev = None;
for i in 0..4 {
let (link, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(i as f32 * 1.2, 4.0, 0.0)),
ColliderBuilder::cuboid(0.5, 0.5, 0.5),
);
if let Some(p) = prev {
world.insert_multibody_joint(
p,
link,
SphericalJointBuilder::new().local_anchor1(Vector::new(1.2, 0.0, 0.0)),
);
}
prev = Some(link);
}

for _ in 0..400 {
world.step(); // panics when the chain reaches the ground
}

Passes without parallel; panics with it. With the guard removed it passes in
both modes, and the full test suite stays green in both modes.

Found in a Bevy/bevy_rapier3d simulation (articulated entities with limbs controlled by neural networks under constant
spawn/despawn churn) where it crashed long-running sessions the first time a
swimmer link collided with terrain.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions