From ec924a7c1f10925a0af2d79dd387a9e0b9cae79d Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Thu, 16 Jul 2026 09:57:01 +0900 Subject: [PATCH 1/6] fix out-of-bounds when interpolating a node beyond the previous tip In applyInterventionalRadiologyController() STEP 3, the search for the previous node could leave prev_xId == m_nodeCurvAbs.size() (the "Case 1" path), leading to out-of-bounds reads of m_nodeCurvAbs[prev_xId] and m_idInstrumentCurvAbsTable[prev_xId]. Clamp prev_xId to the last node. --- .../controller/InterventionalRadiologyController.inl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl index affde0f77..2e563cba2 100644 --- a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl +++ b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl @@ -850,6 +850,11 @@ void InterventionalRadiologyController::applyInterventionalRadiologyC break; } + // If no previous node was found beyond xCurvAbs (see the "Case 1" warning above), the loop + // ends with prev_xId == m_nodeCurvAbs.size(): clamp to the last node to avoid out-of-bounds access. + if (prev_xId >= m_nodeCurvAbs.size()) + prev_xId = m_nodeCurvAbs.size() - 1; + sofa::Index prev_globalNodeId = prev_numberOfUnactiveNodes + prev_xId; const Real prev_xCurvAbs = m_nodeCurvAbs[prev_xId]; From c88bd8ba72322d7a8ca9e90cf94fb534fccac5a1 Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Thu, 16 Jul 2026 09:59:53 +0900 Subject: [PATCH 2/6] guard against a negative controlledInstrument --- ...InterventionalRadiologyController_test.cpp | 30 +++++++++++++++++++ .../InterventionalRadiologyController.inl | 4 +-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/BeamAdapter_test/component/controller/InterventionalRadiologyController_test.cpp b/BeamAdapter_test/component/controller/InterventionalRadiologyController_test.cpp index 580342fb2..1728e42a5 100644 --- a/BeamAdapter_test/component/controller/InterventionalRadiologyController_test.cpp +++ b/BeamAdapter_test/component/controller/InterventionalRadiologyController_test.cpp @@ -465,6 +465,20 @@ TEST_F(InterventionalRadiologyController_test, action_movesControlledInstrument) EXPECT_NEAR(ctrl->d_xTip.getValue()[0], x0Before, 1e-9); } +/// A negative controlledInstrument must be rejected (bounds check), not used as a negative index. +TEST_F(InterventionalRadiologyController_test, action_negativeControlledInstrument_isRejected) +{ + loadScene(singleInstrumentScene("controlledInstrument='-1'")); + auto* ctrl = getController(); + ASSERT_NE(ctrl, nullptr); + + const Real x0Before = ctrl->d_xTip.getValue()[0]; + + // applyAction returns early for an out-of-range (negative) instrument: no crash, no change. + ctrl->applyAction(BeamAdapterAction::MOVE_FORWARD); + EXPECT_NEAR(ctrl->d_xTip.getValue()[0], x0Before, 1e-9); +} + ///////////////////////////////////// Getters / helpers /////////////////////////////////////////// @@ -595,4 +609,20 @@ TEST_F(InterventionalRadiologyController_test, deploy_runsStable) EXPECT_EQ(ctrl->getComponentState(), ComponentState::Valid); } +/// A negative controlledInstrument during stepping must fall back to instrument 0 (no negative index). +TEST_F(InterventionalRadiologyController_test, deploy_negativeControlledInstrument_fallsBackToZero) +{ + loadAndInitRoot(deploymentScene("xtip='0' step='0.5' speed='10' controlledInstrument='-1'")); + + auto* ctrl = getController(); + ASSERT_NE(ctrl, nullptr); + ASSERT_EQ(ctrl->getComponentState(), ComponentState::Valid); + + for (int i = 0; i < 10; ++i) + sofa::simulation::node::animate(m_root.get(), 0.01); + + // the step is applied to the fallback instrument 0 instead of dereferencing index -1 + EXPECT_GT(ctrl->d_xTip.getValue()[0], 0.0); +} + } // namespace beamadapter_test diff --git a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl index 2e563cba2..e8c1ee6a7 100644 --- a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl +++ b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl @@ -372,7 +372,7 @@ void InterventionalRadiologyController::onBeginAnimationStep(const do if(m_FF || m_RW) { int id = d_controlledInstrument.getValue(); - if (id >= (int)xInstrTip.size()) + if (id < 0 || id >= (int)xInstrTip.size()) { msg_warning()<<"Controlled Instument num "< void InterventionalRadiologyController::applyAction(BeamAdapterAction action) { int id = d_controlledInstrument.getValue(); - if (id >= int(m_instrumentsList.size())) + if (id < 0 || id >= int(m_instrumentsList.size())) { msg_warning() << "Controlled Instrument num " << id << " does not exist (size =" << m_instrumentsList.size() << ")."; return; From c8c17b85a84b58aa778e5a5eb376d5070fa0a93a Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Thu, 16 Jul 2026 10:12:15 +0900 Subject: [PATCH 3/6] guard against an instrument with empty sampling parameters --- .../controller/InterventionalRadiologyController.inl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl index e8c1ee6a7..95b9878a1 100644 --- a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl +++ b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl @@ -500,6 +500,14 @@ void InterventionalRadiologyController::computeInstrumentsCurvAbs(typ type::vector density_I; m_instrumentsList[i]->getSamplingParameters(xP_noticeable_I, density_I); // sampling of the different section of this instrument + // an instrument must provide at least one noticeable point; otherwise the loop below and the + // final key point access (xP_noticeable_I.size()-1) would read out of bounds. + if (xP_noticeable_I.empty()) + { + msg_error() << "Instrument " << i << " provides no sampling parameters (no noticeable point). Skipping it."; + continue; + } + // check each interval of noticeable point to see if they go out (>0) and use corresponding density to sample the interval. for (int j=0; j<(int)(xP_noticeable_I.size()-1); j++) { From 0c4c1c3cb6eb938cca8455569924fd86be8b2e3b Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Thu, 16 Jul 2026 10:19:11 +0900 Subject: [PATCH 4/6] fix segRemove sentinel dropping edge index 0 --- .../controller/InterventionalRadiologyController.inl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl index 95b9878a1..0e1dc9fdf 100644 --- a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl +++ b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl @@ -611,10 +611,8 @@ void InterventionalRadiologyController::interventionalRadiologyCollis Real xAbsCurv = m_nodeCurvAbs[node]; int firstInstruOnx = m_idInstrumentCurvAbsTable[node][0]; - type::vector segRemove; - - for (unsigned int it=0; it segRemove(m_instrumentsList.size(), -1); for (int i = static_cast(xPointList.size()) - 1; i>=0; i--) { @@ -667,7 +665,7 @@ void InterventionalRadiologyController::interventionalRadiologyCollis for (unsigned int it=0; it Date: Thu, 16 Jul 2026 10:21:18 +0900 Subject: [PATCH 5/6] clamp node 1 in totalLengthIsChanging The monotonicity clamp on modifiedNodeCurvAbs started at i > 1, skipping node 1, which could then dip below node 0 (the origin) and feed unsorted curv abscissa into the STEP 3 interpolation. Start the clamp at i > 0. --- .../controller/InterventionalRadiologyController.inl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl index 0e1dc9fdf..050e11c85 100644 --- a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl +++ b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl @@ -1022,7 +1022,9 @@ void InterventionalRadiologyController::totalLengthIsChanging(const t if (newTable[i].size() == 1) { modifiedNodeCurvAbs[i] -= dLength; - if (i > 1 && modifiedNodeCurvAbs[i] < modifiedNodeCurvAbs[i - 1]) + // keep the curv abscissa monotonic: clamp against the previous node (including node 0, the origin), + // otherwise applyInterventionalRadiologyController's interpolation would receive unsorted abscissa. + if (i > 0 && modifiedNodeCurvAbs[i] < modifiedNodeCurvAbs[i - 1]) { modifiedNodeCurvAbs[i] = modifiedNodeCurvAbs[i - 1]; } From 3aec1ed71ce4b443cd819e12c5e74604289ee847 Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Thu, 16 Jul 2026 10:25:12 +0900 Subject: [PATCH 6/6] bound the STEP 5 rigid-segment loop The loop fixing rigid-segment border nodes could compute firstSimulatedNode + i == numberOfNodes (one past the last dof) and pass it to FixedProjectiveConstraint::addConstraint. Stop the loop before the index leaves the valid dof range. --- .../component/controller/InterventionalRadiologyController.inl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl index 050e11c85..efba3afbe 100644 --- a/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl +++ b/src/BeamAdapter/component/controller/InterventionalRadiologyController.inl @@ -961,7 +961,8 @@ void InterventionalRadiologyController::applyInterventionalRadiologyC { RealConstIterator it = rigidCurvAbs->begin(); - for (unsigned int i=0; i::epsilon()) && newCurvAbs[i] > ((*it)- std::numeric_limits::epsilon())) // node= border of the rigid segment {