--- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMaintenanceProbe.c +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMaintenanceProbe.c @@ -30,6 +30,7 @@ protected SCR_FuelNode m_Tank; protected ref array m_Nodes = {}; protected ref array m_InitialNodes = {}; + protected bool m_NodeSnapshotDiagnosed; protected SCR_RefuelAtSupportStationAction m_Action; protected BaseActionsManagerComponent m_Actions; protected UserActionContext m_Context; @@ -205,13 +206,39 @@ } return allowed; } + protected bool CaptureNodes() + { + if (!m_Fuel || !m_Nodes.IsEmpty() || !m_InitialNodes.IsEmpty()) return false; + // Native out-array ownership is unspecified. Never expose our retained + // baseline arrays to it; copy original identities and values together once. + array found = {}; m_Fuel.GetFuelNodesList(found); + if (found.IsEmpty() || found.Count() > 8) return false; + foreach (BaseFuelNode node : found) + { + if (!SCR_FuelNode.Cast(node) || !ERR_ForceRules.Finite(node.GetFuel())) return false; + m_Nodes.Insert(node); m_InitialNodes.Insert(node.GetFuel()); + } + return m_Nodes.Count() == m_InitialNodes.Count(); + } + protected bool RefuseNodeSnapshot(int current) + { + if (!m_NodeSnapshotDiagnosed) + { + m_NodeSnapshotDiagnosed = true; + PrintFormat("ERR MAINTENANCE NODES: current=%1 original=%2 values=%3; immutable snapshot mismatch", current, m_Nodes.Count(), m_InitialNodes.Count()); + } + return false; + } protected bool Conservation() { + if (!m_Fuel || !m_CanFuel || !m_Tank) return RefuseNodeSnapshot(-1); array nodes = {}; m_Fuel.GetFuelNodesList(nodes); - if (nodes.Count() != m_Nodes.Count()) return false; - for (int i = 0; i < nodes.Count(); i++) - { - if (!nodes[i] || nodes[i] != m_Nodes[i]) return false; + int count = nodes.Count(); + if (count < 1 || count > 8 || count != m_Nodes.Count() || count != m_InitialNodes.Count()) return RefuseNodeSnapshot(count); + for (int i = 0; i < count; i++) + { + if (!nodes[i] || nodes[i] != m_Nodes[i]) return RefuseNodeSnapshot(count); + if (!ERR_ForceRules.Finite(m_InitialNodes[i])) return RefuseNodeSnapshot(count); if (nodes[i] != m_Tank && !(Math.AbsFloat(nodes[i].GetFuel() - m_InitialNodes[i]) <= 0.001)) return false; } float can = m_CanFuel.GetTotalFuel(), tank = m_Tank.GetFuel(); @@ -262,9 +289,7 @@ if (!m_CanFuel || !m_SpareFuel || !m_Station) return false; m_InitialCan = m_CanFuel.GetTotalFuel(); m_InitialSpare = m_SpareFuel.GetTotalFuel(); if (!(Math.AbsFloat(m_InitialCan - 5) <= 0.001) || !(Math.AbsFloat(m_InitialSpare - 5) <= 0.001)) return false; - m_Fuel.GetFuelNodesList(m_Nodes); - if (m_Nodes.IsEmpty() || m_Nodes.Count() > 8) return false; - foreach (BaseFuelNode node : m_Nodes) { if (!SCR_FuelNode.Cast(node)) return false; m_InitialNodes.Insert(node.GetFuel()); } + if (!CaptureNodes()) return false; m_Actions = BaseActionsManagerComponent.Cast(m_Vehicle.FindComponent(BaseActionsManagerComponent)); array actions = {}; if (m_Actions) m_Actions.GetActionsList(actions); if (actions.Count() > 64) return false; --- a/tests/test_native_maintenance_refuel.py +++ b/tests/test_native_maintenance_refuel.py @@ -140,3 +140,50 @@ for name in ('m_Fuel','m_CanFuel','m_SpareFuel'):assert name+'.GetOwner()' not in context assert 'm_Tank.GetFuel() - 6' in tick and 'm_Tank.GetMaxFuel() - beforeTank >= 0.99' in tick print('PASS: actual exact-context membership, bounded identity/reach gates, native start/cache/eligibility ordering and no duplicate/foreign action start; fuel manager identity and empty-can headroom guards checked') + +# The native out-array may alias a manager-owned list. Exercise the actual +# snapshot and conservation methods with an explicitly aliasing boundary double. +nodes='\n'.join(body(P,sig) for sig in ('protected bool CaptureNodes(', 'protected bool RefuseNodeSnapshot(', 'protected bool Conservation(')) +nodes=nodes.replace('protected ','').replace('array','Array').replace('foreach (BaseFuelNode node : found)','for (BaseFuelNode* node : found)') +for ns in ('SCR_FuelNode','ERR_ForceRules','Math'):nodes=nodes.replace(ns+'.',ns+'::') +for obj in ('m_Fuel','m_CanFuel','m_Tank','node'):nodes=re.sub(r'\b'+obj+r'\.',obj+'->',nodes) +nodes=nodes.replace('nodes[i].','nodes[i]->') +run(r''' +#include +#include +#include +#include +#include +#include +templatestruct Array{ + std::shared_ptr> data=std::make_shared>(); + int Count(){return data->size();}bool IsEmpty(){return data->empty();}void Insert(T value){data->push_back(value);}T&operator[](int index){return data->at(index);}auto begin(){return data->begin();}auto end(){return data->end();} +}; +struct BaseFuelNode{float fuel=10,capacity=40;bool supported=true;float GetFuel(){return fuel;}float GetMaxFuel(){return capacity;}}; +namespace SCR_FuelNode{BaseFuelNode*Cast(BaseFuelNode*n){return n&&n->supported?n:nullptr;}} +namespace ERR_ForceRules{bool Finite(float v){return std::isfinite(v);}} +namespace Math{float AbsFloat(float v){return std::fabs(v);}} +struct Manager{Array native;float total=5;int reads=0;void GetFuelNodesList(Array&out){reads++;out.data=native.data;}float GetTotalFuel(){return total;}}; +int diagnostics=0,seenCurrent=-99,seenOriginal=-99,seenValues=-99; +void PrintFormat(const char*,int current,int original,int values){diagnostics++;seenCurrent=current;seenOriginal=original;seenValues=values;} +'''+fuel+r''' +struct Probe{Manager*m_Fuel=nullptr,*m_CanFuel=nullptr;BaseFuelNode*m_Tank=nullptr;Arraym_Nodes;Arraym_InitialNodes;bool m_NodeSnapshotDiagnosed=false;float m_InitialCan=5,m_BaseTank=10; +'''+nodes+r''' +}; +struct Fixture{Manager vehicle,can;BaseFuelNode tank,other;Probe probe;Fixture(){vehicle.native.Insert(&tank);vehicle.native.Insert(&other);probe.m_Fuel=&vehicle;probe.m_CanFuel=&can;probe.m_Tank=&tank;}}; +int main(){ + Fixture truncated;assert(truncated.probe.CaptureNodes());truncated.probe.m_InitialNodes.data->pop_back();assert(!truncated.probe.Conservation()); + Fixture stable;assert(stable.probe.CaptureNodes());assert(stable.probe.m_Nodes.Count()==2&&stable.probe.m_InitialNodes.Count()==2);assert(stable.probe.Conservation());assert(!stable.probe.CaptureNodes()); + stable.can.total=4;stable.tank.fuel=11;assert(stable.probe.Conservation());stable.can.total=0;stable.tank.fuel=15;assert(stable.probe.Conservation()); + // Exact known native count drift cannot silently mutate the retained baseline. + Fixture alias;assert(alias.probe.CaptureNodes());BaseFuelNode added;alias.vehicle.native.Insert(&added);assert(alias.probe.m_Nodes.Count()==2&&alias.probe.m_InitialNodes.Count()==2);diagnostics=0;assert(!alias.probe.Conservation());assert(diagnostics==1&&seenCurrent==3&&seenOriginal==2&&seenValues==2);assert(!alias.probe.Conservation()&&diagnostics==1); + // Every length mismatch refuses before any checked array indexing. + for(int fault=0;fault<6;fault++){Fixture f;assert(f.probe.CaptureNodes());switch(fault){case 0:f.probe.m_InitialNodes.data->clear();break;case 1:f.probe.m_InitialNodes.data->pop_back();break;case 2:f.probe.m_InitialNodes.Insert(1);break;case 3:f.probe.m_Nodes.data->pop_back();break;case 4:f.vehicle.native.data->clear();break;case 5:f.vehicle.native.data->resize(9,&f.tank);break;}diagnostics=0;assert(!f.probe.Conservation()&&diagnostics==1);assert(!f.probe.Conservation()&&diagnostics==1);} + for(int fault=0;fault<6;fault++){Fixture f;assert(f.probe.CaptureNodes());BaseFuelNode foreign;switch(fault){case 0:f.vehicle.native[1]=&foreign;break;case 1:f.vehicle.native[1]=nullptr;break;case 2:std::swap(f.vehicle.native[0],f.vehicle.native[1]);break;case 3:f.probe.m_InitialNodes[1]=NAN;break;case 4:f.probe.m_Fuel=nullptr;break;case 5:f.probe.m_CanFuel=nullptr;break;}assert(!f.probe.Conservation());} + Fixture changed;assert(changed.probe.CaptureNodes());changed.other.fuel+=1;assert(!changed.probe.Conservation());changed.other.fuel-=1;changed.can.total=4;changed.tank.fuel=12;assert(!changed.probe.Conservation()); + for(int fault=0;fault<4;fault++){Fixture f;if(fault==0)f.vehicle.native.data->clear();if(fault==1)f.vehicle.native.data->resize(9,&f.tank);if(fault==2)f.other.supported=false;if(fault==3)f.other.fuel=NAN;assert(!f.probe.CaptureNodes());} +} +''') +assert 'GetFuelNodesList(m_Nodes)' not in s +assert body(P,'protected bool Select(').count('CaptureNodes()')==1 +print('PASS: actual detached node/value snapshot and conservation refuse mismatched lengths before indexed reads, retain original identities under native out-array aliasing and log bounded counts once; native cause remains unknown')