From 54348ff04d3fa6d1583224d3568cd2cf5825a2ab Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:11:02 -0700 Subject: [PATCH] fix: guard against already-dead reclaim targets (#3020) Engine crash thread: https://discordapp.com/channels/549281623154229250/1516994684591931535 Bugged Scenario: A reclaimer A is guarding another reclaimer B, while both reclaiming the same wreck. A can be notified of wreck death BEFORE the guarded unit B is notified, leading to trying to reclaim the same (actively-deleting) wreck because B's reference hasn't been cleaned up yet. This leads to an eventual segfault. So: the short term/easy fix is to skip reclaiming of a dead/dying target. --- rts/Sim/Units/CommandAI/BuilderCAI.cpp | 4 ++-- rts/Sim/Units/UnitTypes/Builder.cpp | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/rts/Sim/Units/CommandAI/BuilderCAI.cpp b/rts/Sim/Units/CommandAI/BuilderCAI.cpp index b747e342615..e1faddb7dd6 100644 --- a/rts/Sim/Units/CommandAI/BuilderCAI.cpp +++ b/rts/Sim/Units/CommandAI/BuilderCAI.cpp @@ -891,13 +891,13 @@ void CBuilderCAI::ExecuteGuard(Command& c) StopSlowGuard(); } return; - } else if (b->curReclaim && owner->unitDef->canReclaim) { + } else if (b->curReclaim && !b->curReclaim->detached && owner->unitDef->canReclaim) { StopSlowGuard(); if (!ReclaimObject(b->curReclaim)) { StopMove(); } return; - } else if (b->curResurrect && owner->unitDef->canResurrect) { + } else if (b->curResurrect && !b->curResurrect->detached && owner->unitDef->canResurrect) { StopSlowGuard(); if (!ResurrectObject(b->curResurrect)) { StopMove(); diff --git a/rts/Sim/Units/UnitTypes/Builder.cpp b/rts/Sim/Units/UnitTypes/Builder.cpp index 4cf3240b1cd..deb8c846b73 100644 --- a/rts/Sim/Units/UnitTypes/Builder.cpp +++ b/rts/Sim/Units/UnitTypes/Builder.cpp @@ -605,6 +605,15 @@ void CBuilder::SetRepairTarget(CUnit* target) void CBuilder::SetReclaimTarget(CSolidObject* target) { RECOIL_DETAILED_TRACY_ZONE; + // A target already being destroyed (detached) cannot have a death dependence + // registered (AddDeathDependence no-ops on detached objects), which would leave + // curReclaim dangling. This happens when ~CObject's DependentDied cascade re-enters + // reclaim logic mid-deletion (e.g. CBuilderCAI::ExecuteGuard reading a guardee's + // stale curReclaim) on the very feature being freed. Refuse the dead target. + assert(target != nullptr); + if (target->detached) + return; + if (dynamic_cast(target) != nullptr && !static_cast(target)->def->reclaimable) return; @@ -630,6 +639,11 @@ void CBuilder::SetReclaimTarget(CSolidObject* target) void CBuilder::SetResurrectTarget(CFeature* target) { RECOIL_DETAILED_TRACY_ZONE; + // see SetReclaimTarget: never depend on an object that is already being destroyed + assert(target != nullptr); + if (target->detached) + return; + if (curResurrect == target || target->udef == nullptr) return; @@ -646,6 +660,11 @@ void CBuilder::SetResurrectTarget(CFeature* target) void CBuilder::SetCaptureTarget(CUnit* target) { RECOIL_DETAILED_TRACY_ZONE; + // see SetReclaimTarget: never depend on an object that is already being destroyed + assert(target != nullptr); + if (target->detached) + return; + if (target == curCapture) return;