Skip to content

Commit e8fb20b

Browse files
committed
Fix crash from stale texture pointers when changing model TXD ID
Changing a loaded model's TXD leaves material textures pointing at the old TXD. When it gets released - dangling pointers, DEP crash. - Add rebind functions to rewire textures by name lookup in new TXD - Handle ATOMIC/LOD_ATOMIC/TIME models (RpAtomic*, not RpClump*) - AddRef new before rebind, RemoveRef old after - no UAF's - Also rebind in SetCustomModel for shader replacement to work
1 parent f5409cb commit e8fb20b

File tree

5 files changed

+171
-106
lines changed

5 files changed

+171
-106
lines changed

Client/game_sa/CModelInfoSA.cpp

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -885,28 +885,66 @@ void CModelInfoSA::SetTextureDictionaryID(unsigned short usID)
885885
if (!m_pInterface)
886886
return;
887887

888-
// CBaseModelInfo::AddRef adds references to model and TXD
889-
// We need transfer added references from old TXD to new TXD
890-
size_t referencesCount = m_pInterface->usNumberOfRefs;
888+
unsigned short usOldTxdId = m_pInterface->usTextureDictionary;
889+
if (usOldTxdId == usID)
890+
return;
891891

892-
// +1 reference for active rwObject
893-
// The current textures will be removed in RpAtomicDestroy
894-
// RenderWare uses an additional reference counter per texture
892+
size_t referencesCount = m_pInterface->usNumberOfRefs;
895893
if (m_pInterface->pRwObject)
896894
referencesCount++;
897895

898-
for (size_t i = 0; i < referencesCount; i++)
899-
CTxdStore_RemoveRef(m_pInterface->usTextureDictionary);
900-
901-
// Store vanilla TXD ID
902896
if (!MapContains(ms_DefaultTxdIDMap, static_cast<unsigned short>(m_dwModelID)))
903-
ms_DefaultTxdIDMap[static_cast<unsigned short>(m_dwModelID)] = m_pInterface->usTextureDictionary;
897+
ms_DefaultTxdIDMap[static_cast<unsigned short>(m_dwModelID)] = usOldTxdId;
904898

905-
// Set new TXD and increase ref of it
906899
m_pInterface->usTextureDictionary = usID;
907900

901+
// Pin the new TXD before rebinding so textures remain valid during the switch
908902
for (size_t i = 0; i < referencesCount; i++)
909903
CTxdStore_AddRef(usID);
904+
905+
// Rebind loaded model's material textures to the new TXD.
906+
// Without this, material->texture pointers would become stale when the old TXD is released.
907+
if (m_pInterface->pRwObject)
908+
{
909+
eModelInfoType modelType = GetModelType();
910+
switch (modelType)
911+
{
912+
case eModelInfoType::PED:
913+
case eModelInfoType::WEAPON:
914+
case eModelInfoType::VEHICLE:
915+
case eModelInfoType::CLUMP:
916+
case eModelInfoType::UNKNOWN:
917+
{
918+
RpClump* pGameClump = reinterpret_cast<RpClump*>(m_pInterface->pRwObject);
919+
if (pGame)
920+
{
921+
CRenderWare* pRenderWare = pGame->GetRenderWare();
922+
if (pRenderWare)
923+
pRenderWare->RebindClumpTexturesToTxd(pGameClump, usID);
924+
}
925+
break;
926+
}
927+
case eModelInfoType::ATOMIC:
928+
case eModelInfoType::LOD_ATOMIC:
929+
case eModelInfoType::TIME:
930+
{
931+
RpAtomic* pAtomic = reinterpret_cast<RpAtomic*>(m_pInterface->pRwObject);
932+
if (pGame)
933+
{
934+
CRenderWare* pRenderWare = pGame->GetRenderWare();
935+
if (pRenderWare)
936+
pRenderWare->RebindAtomicTexturesToTxd(pAtomic, usID);
937+
}
938+
break;
939+
}
940+
default:
941+
break;
942+
}
943+
}
944+
945+
// Release old TXD refs after rebinding completes
946+
for (size_t i = 0; i < referencesCount; i++)
947+
CTxdStore_RemoveRef(usOldTxdId);
910948
}
911949

912950
void CModelInfoSA::ResetTextureDictionaryID()
@@ -1800,6 +1838,19 @@ bool CModelInfoSA::SetCustomModel(RpClump* pClump)
18001838
}
18011839
break;
18021840
}
1841+
case eModelInfoType::ATOMIC:
1842+
case eModelInfoType::LOD_ATOMIC:
1843+
case eModelInfoType::TIME:
1844+
{
1845+
RpAtomic* pAtomic = reinterpret_cast<RpAtomic*>(GetRwObject());
1846+
if (pAtomic && pGame)
1847+
{
1848+
CRenderWare* pRenderWare = pGame->GetRenderWare();
1849+
if (pRenderWare)
1850+
pRenderWare->RebindAtomicTexturesToTxd(pAtomic, GetTextureDictionaryID());
1851+
}
1852+
break;
1853+
}
18031854
default:
18041855
break;
18051856
}

Client/game_sa/CRenderWareSA.cpp

Lines changed: 95 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -558,51 +558,33 @@ bool CRenderWareSA::DoContainTheSameGeometry(RpClump* pClumpA, RpClump* pClumpB,
558558
return true;
559559
}
560560

561-
////////////////////////////////////////////////////////////////
562-
//
563-
// CRenderWareSA::RebindClumpTexturesToTxd
564-
//
565-
// Rebinds all material textures in a clump to current TXD textures.
566-
// This fixes stale texture pointers that occur when a TXD is reloaded
567-
// after a custom DFF has been loaded. Without this fix, shader texture
568-
// replacement fails because the materials point to old/destroyed textures.
569-
//
570-
////////////////////////////////////////////////////////////////
571-
void CRenderWareSA::RebindClumpTexturesToTxd(RpClump* pClump, unsigned short usTxdId)
561+
// Helpers for rebinding model textures to a different TXD.
562+
// When a model's TXD slot is changed, its loaded RwObject still holds pointers
563+
// to textures from the old TXD. These helpers update material texture pointers
564+
// to reference matching textures from the new TXD by name lookup.
565+
namespace
572566
{
573-
if (!pClump || !SharedUtil::IsReadablePointer(pClump, sizeof(RpClump)))
574-
return;
575-
576-
RwTexDictionary* pTxd = CTxdStore_GetTxd(usTxdId);
577-
if (!pTxd || !SharedUtil::IsReadablePointer(pTxd, sizeof(RwTexDictionary)))
578-
return;
579-
580-
// Build a safe name->texture map once.
581-
// Calling RW's FindNamedTexture can freeze if the TXD's internal linked list is bad
582-
std::vector<RwTexture*> txdTextures;
583-
GetTxdTextures(txdTextures, pTxd);
584-
585-
struct TextureNameHash
567+
// Hash functor for texture name lookup (bounded to RW_TEXTURE_NAME_LENGTH)
568+
struct TxdTextureNameHash
586569
{
570+
static constexpr std::size_t HASH_INIT = 2166136261u;
571+
static constexpr std::size_t HASH_MULTIPLIER = 16777619u;
572+
587573
std::size_t operator()(const char* s) const noexcept
588574
{
589575
if (!s)
590576
return 0;
591-
592-
std::size_t h = 2166136261u;
593-
for (std::size_t i = 0; i < RW_TEXTURE_NAME_LENGTH; ++i)
577+
std::size_t h = HASH_INIT;
578+
for (std::size_t i = 0; i < RW_TEXTURE_NAME_LENGTH && s[i]; ++i)
594579
{
595-
const unsigned char c = static_cast<unsigned char>(s[i]);
596-
if (c == 0)
597-
break;
598-
h ^= c;
599-
h *= 16777619u;
580+
h ^= static_cast<unsigned char>(s[i]);
581+
h *= HASH_MULTIPLIER;
600582
}
601583
return h;
602584
}
603585
};
604586

605-
struct TextureNameEq
587+
struct TxdTextureNameEq
606588
{
607589
bool operator()(const char* a, const char* b) const noexcept
608590
{
@@ -614,106 +596,126 @@ void CRenderWareSA::RebindClumpTexturesToTxd(RpClump* pClump, unsigned short usT
614596
}
615597
};
616598

617-
using TxdTextureMap = std::unordered_map<const char*, RwTexture*, TextureNameHash, TextureNameEq>;
618-
TxdTextureMap txdTextureMap;
619-
if (!txdTextures.empty())
599+
using TxdTextureMap = std::unordered_map<const char*, RwTexture*, TxdTextureNameHash, TxdTextureNameEq>;
600+
601+
// Build name->texture map for a TXD slot. Keys point directly into RwTexture::name buffers.
602+
TxdTextureMap BuildTxdTextureMap(unsigned short usTxdId)
620603
{
621-
txdTextureMap.reserve(txdTextures.size());
604+
TxdTextureMap result;
605+
606+
RwTexDictionary* pTxd = CTxdStore_GetTxd(usTxdId);
607+
if (!pTxd)
608+
return result;
609+
610+
std::vector<RwTexture*> txdTextures;
611+
CRenderWareSA::GetTxdTextures(txdTextures, pTxd);
612+
613+
if (txdTextures.empty())
614+
return result;
615+
616+
result.reserve(txdTextures.size());
622617
for (RwTexture* pTexture : txdTextures)
623618
{
624-
if (!pTexture || !SharedUtil::IsReadablePointer(pTexture, sizeof(RwTexture)))
619+
if (!pTexture)
625620
continue;
626621

627622
const char* name = pTexture->name;
628-
const std::size_t nameLen = strnlen(name, RW_TEXTURE_NAME_LENGTH);
629-
if (nameLen >= RW_TEXTURE_NAME_LENGTH)
630-
continue;
631-
632-
txdTextureMap[name] = pTexture;
623+
if (strnlen(name, RW_TEXTURE_NAME_LENGTH) < RW_TEXTURE_NAME_LENGTH)
624+
result[name] = pTexture;
633625
}
634-
}
635626

636-
// Iterate through all atomics in the clump
637-
std::vector<RpAtomic*> atomicList;
638-
GetClumpAtomicList(pClump, atomicList);
627+
return result;
628+
}
639629

640-
for (RpAtomic* pAtomic : atomicList)
630+
// Update each material's texture pointer to the matching texture from txdTextureMap.
631+
// Falls back to internal name mapping if direct lookup fails.
632+
void RebindAtomicMaterials(RpAtomic* pAtomic, const TxdTextureMap& txdTextureMap)
641633
{
642-
if (!pAtomic || !SharedUtil::IsReadablePointer(pAtomic, sizeof(RpAtomic)))
643-
continue;
634+
if (!pAtomic)
635+
return;
644636

645637
RpGeometry* pGeometry = pAtomic->geometry;
646-
if (!pGeometry || !SharedUtil::IsReadablePointer(pGeometry, sizeof(RpGeometry)))
647-
continue;
638+
if (!pGeometry)
639+
return;
648640

649641
RpMaterials& materials = pGeometry->materials;
650-
651-
// Validate materials array exists and entries is sane
652642
if (!materials.materials || materials.entries <= 0)
653-
continue;
643+
return;
654644

655-
// Sanity check - reject obviously corrupted values.
656-
// Normal geometry has at most a few hundred materials.
657645
constexpr int MAX_REASONABLE_MATERIALS = 10000;
658-
int materialCount = materials.entries;
659-
if (materialCount > MAX_REASONABLE_MATERIALS)
660-
continue;
661-
662-
// Validate materials array is readable
663-
if (!SharedUtil::IsReadablePointer(materials.materials, materialCount * sizeof(RpMaterial*)))
664-
continue;
646+
if (materials.entries > MAX_REASONABLE_MATERIALS)
647+
return;
665648

666-
// Iterate through all materials in the geometry
667-
for (int idx = 0; idx < materialCount; ++idx)
649+
for (int idx = 0; idx < materials.entries; ++idx)
668650
{
669651
RpMaterial* pMaterial = materials.materials[idx];
670-
if (!pMaterial || !SharedUtil::IsReadablePointer(pMaterial, sizeof(RpMaterial)))
652+
if (!pMaterial)
671653
continue;
672654

673655
RwTexture* pOldTexture = pMaterial->texture;
674-
if (!pOldTexture || !SharedUtil::IsReadablePointer(pOldTexture, sizeof(RwTexture)))
656+
if (!pOldTexture)
675657
continue;
676658

677-
// Get the current texture's name (RwTexture::name is char[32], always check first char)
678659
const char* szTextureName = pOldTexture->name;
679660
if (!szTextureName[0])
680661
continue;
681662

682-
RwTexture* pCurrentTexture = nullptr;
683-
const std::size_t nameLen = strnlen(szTextureName, RW_TEXTURE_NAME_LENGTH);
684-
if (nameLen < RW_TEXTURE_NAME_LENGTH)
685-
{
686-
auto itFound = txdTextureMap.find(szTextureName);
687-
if (itFound != txdTextureMap.end())
688-
pCurrentTexture = itFound->second;
663+
if (strnlen(szTextureName, RW_TEXTURE_NAME_LENGTH) >= RW_TEXTURE_NAME_LENGTH)
664+
continue;
689665

690-
if (!pCurrentTexture)
666+
RwTexture* pNewTexture = nullptr;
667+
auto itFound = txdTextureMap.find(szTextureName);
668+
if (itFound != txdTextureMap.end())
669+
pNewTexture = itFound->second;
670+
671+
if (!pNewTexture)
672+
{
673+
const char* szInternalName = CRenderWareSA::GetInternalTextureName(szTextureName);
674+
if (szInternalName && szInternalName != szTextureName)
691675
{
692-
const char* szInternalName = GetInternalTextureName(szTextureName);
693-
if (szInternalName && szInternalName != szTextureName)
676+
if (strnlen(szInternalName, RW_TEXTURE_NAME_LENGTH) < RW_TEXTURE_NAME_LENGTH)
694677
{
695-
const std::size_t internalLen = strnlen(szInternalName, RW_TEXTURE_NAME_LENGTH);
696-
if (internalLen < RW_TEXTURE_NAME_LENGTH)
697-
{
698-
auto itInternal = txdTextureMap.find(szInternalName);
699-
if (itInternal != txdTextureMap.end())
700-
pCurrentTexture = itInternal->second;
701-
}
678+
auto itInternal = txdTextureMap.find(szInternalName);
679+
if (itInternal != txdTextureMap.end())
680+
pNewTexture = itInternal->second;
702681
}
703682
}
704683
}
705684

706-
// If we found a texture and it's different from the material's current texture, update it
707-
// Validate the found texture to prevent crash from corrupted/freed texture in TXD
708-
if (pCurrentTexture && pCurrentTexture != pOldTexture &&
709-
SharedUtil::IsReadablePointer(pCurrentTexture, sizeof(RwTexture)))
710-
{
711-
RpMaterialSetTexture(pMaterial, pCurrentTexture);
712-
}
685+
if (pNewTexture && pNewTexture != pOldTexture)
686+
RpMaterialSetTexture(pMaterial, pNewTexture);
713687
}
714688
}
715689
}
716690

691+
void CRenderWareSA::RebindClumpTexturesToTxd(RpClump* pClump, unsigned short usTxdId)
692+
{
693+
if (!pClump)
694+
return;
695+
696+
TxdTextureMap txdTextureMap = BuildTxdTextureMap(usTxdId);
697+
if (txdTextureMap.empty())
698+
return;
699+
700+
std::vector<RpAtomic*> atomicList;
701+
GetClumpAtomicList(pClump, atomicList);
702+
703+
for (RpAtomic* pAtomic : atomicList)
704+
RebindAtomicMaterials(pAtomic, txdTextureMap);
705+
}
706+
707+
void CRenderWareSA::RebindAtomicTexturesToTxd(RpAtomic* pAtomic, unsigned short usTxdId)
708+
{
709+
if (!pAtomic || !pAtomic->geometry)
710+
return;
711+
712+
TxdTextureMap txdTextureMap = BuildTxdTextureMap(usTxdId);
713+
if (txdTextureMap.empty())
714+
return;
715+
716+
RebindAtomicMaterials(pAtomic, txdTextureMap);
717+
}
718+
717719
// Replaces a vehicle/weapon/ped model
718720
bool CRenderWareSA::ReplaceModel(RpClump* pNew, unsigned short usModelID, DWORD dwSetClumpFunction)
719721
{

Client/game_sa/CRenderWareSA.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,12 @@ class CRenderWareSA : public CRenderWare
155155
static void GetClumpAtomicList(RpClump* pClump, std::vector<RpAtomic*>& outAtomicList);
156156
static bool DoContainTheSameGeometry(RpClump* pClumpA, RpClump* pClumpB, RpAtomic* pAtomicB);
157157

158-
// Rebind clump material textures to current TXD textures (fixes stale texture pointers after TXD reload
158+
// Rebind clump material textures to current TXD textures (fixes stale texture pointers after TXD reload)
159159
void RebindClumpTexturesToTxd(RpClump* pClump, unsigned short usTxdId) override;
160160

161+
// Rebind single atomic's material textures to current TXD textures
162+
void RebindAtomicTexturesToTxd(RpAtomic* pAtomic, unsigned short usTxdId) override;
163+
161164
static const char* GetInternalTextureName(const char* szExternalName);
162165
static const char* GetExternalTextureName(const char* szInternalName);
163166

Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,11 @@ bool CLuaEngineDefs::EngineSetModelTXDID(uint uiModelID, unsigned short usTxdId)
14121412
if (uiModelID >= g_pGame->GetBaseIDforTXD() || !pModelInfo)
14131413
throw std::invalid_argument("Expected a valid model ID at argument 1");
14141414

1415+
// TXD slots occupy IDs from BaseIDforTXD to BaseIDforCOL-1
1416+
const unsigned short usMaxTxdSlots = static_cast<unsigned short>(g_pGame->GetBaseIDforCOL() - g_pGame->GetBaseIDforTXD());
1417+
if (usTxdId >= usMaxTxdSlots)
1418+
throw std::invalid_argument("Expected a valid TXD ID at argument 2");
1419+
14151420
// Clean up TXD isolation before changing TXD slot
14161421
if (pModelInfo->GetParentID() != 0)
14171422
{

Client/sdk/game/CRenderWare.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct RwMatrix;
3131
struct RwTexDictionary;
3232
struct RwTexture;
3333
struct RpClump;
34+
struct RpAtomic;
3435

3536
typedef CShaderItem CSHADERDUMMY;
3637

@@ -145,4 +146,7 @@ class CRenderWare
145146

146147
// Cleanup TXD slots created to isolate engineRequestModel clones.
147148
virtual void CleanupIsolatedTxdForModel(unsigned short usModelId) = 0;
149+
150+
// Rebind single atomic's material textures to current TXD textures
151+
virtual void RebindAtomicTexturesToTxd(RpAtomic* pAtomic, unsigned short usTxdId) = 0;
148152
};

0 commit comments

Comments
 (0)