FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
0038 — Auv output ports dereference a null environment during InitialConditionSetup
ID 0038
Class BUG
Severity 2
Status ready
Models Auv/Vehicle
Found 2026-09-22
Decision needed

Evidence

src/auv/Auv.cpp:224-230 resolves the shared environment in FinalSetup:

m_environment = static_cast<environment::EnvironmentProvider*>(creator->GetSharedResource("Environment"));

and Auv::CalcOutput used it unconditionally, reaching it from every output port through the registered common calculation (src/auv/Auv.cpp:1034-1049: OutPos, OutQuater, OutVel, OutEuler and the rest all begin m_commonCalc->ComputeFunction(dT, adX)).

FinalSetup runs after the initial condition phase, not before it. In fhsim, ModelAssemblyService::Assemble calls InitialConditionSetup(...) (ModelAssemblyService.cpp:47) and only then loops over the objects calling FinalSetup (ModelAssemblyService.cpp:54). So during the whole initial condition phase Auv::m_environment is null.

Reading an Auv output port in that phase is therefore a null dereference. Measured while building increment I: a Auv/CTD whose InitialConditionSetup seeds its lag states from Ctd.Quater = "Auv.Quater" segfaults at model load,

#0 Auv::CalcOutput(double, double const*)
#2 Auv::OutQuater(double, double const*)
#5 SensorFrame::SensorPosition(double, double const*, double*) const
#6 SensorCtd::InitialConditionSetup(...)
#7 ModelAssemblyService::InitialConditionSetup(...)

Seeding a state from an input port during InitialConditionSetup is the documented purpose of that method and is what Trawl/Crowfoot already does (src/trawl/Crowfoot.cpp, reading m_inFasteningPositions); it happens not to crash only because a cable's output ports use no environment.

Effect

Any SimObject that computes an initial condition from an Auv pose crashes the whole simulator at model load, with no error message. It is not a wrong number but a segmentation fault, and the stack points at Auv rather than at the object that asked, so it reads as a fault in the sensor.

The same shape applies to every SimObject in this library that resolves the environment in FinalSetup and then uses it in an output function. Trawl/TrawlDoorBase and Trawl/CenterWeight have the same structure; nothing currently reads their ports during the initial condition phase, so the defect is latent there rather than absent.

Possible fix

  1. Taken in increment I, and the whole of the change: guard the environment-derived block of Auv::CalcOutput with if (m_environment != nullptr). The pose, velocity and Euler outputs are pure functions of the state and are computed unconditionally above it, so Pos, Quater, Vel, Euler, Heading and Pitch become valid during the initial condition phase. Depth, Altitude, SeaDepth, Speed and Thrust keep their constructor values for that one phase, which is honest: they are not knowable before the environment exists.
  2. The general fix is upstream and larger: either let a SimObject resolve shared resources before the initial condition phase, or give SimObject a documented "ports are not valid yet" contract that the host enforces, so that reading a not-yet-ready port is a diagnosable error rather than undefined behaviour. That is an fhsim change and is not attempted here.
  3. ISimObjectCreator::ReportUnfinishedPort exists for the related case of a port that cannot yet return a legal value. It does not help here, because Auv's pose ports genuinely can return a legal value; it is only the environment-derived ones that cannot.

Option 1 leaves the latent instance of the same defect in TrawlDoorBase and CenterWeight untouched, deliberately: neither is read during the initial condition phase today, and changing them would need their own verification.

Test that would prove it

SimObject.Auv_CtdRampGivesTheAnalyticFirstOrderResponse in tests/AuvCtd_Test.cpp. Its fixture wires Ctd.Pos and Ctd.Quater to the Auv, and SensorCtd::InitialConditionSetup reads both. Reverting the guard in Auv::CalcOutput makes that test segfault the whole test binary rather than fail.

Risk

Low for option 1. The guard cannot change anything during a simulation, because m_environment is non-null from FinalSetup onwards and ReportError stops the run if the resource is missing. The full suite was run with the guard in place: 105 passing gtest cases, with only the pre-existing 0030 — CenterWeight regression baseline fails after a dependency update, and is not run-to-run reproducible SimObject.CenterWeight red.

The residual risk is that a consumer reads Auv.Depth or Auv.Altitude during the initial condition phase and silently gets the constructor's zero instead of a crash. That is a quieter failure than a segfault, and it is the reason option 2 is the real fix.