Fix ROI UAF cluster and bump Emscripten INITIAL_MEMORY to 128 MB (#807)

ROI UAF fixes:
- LegoAnimActorStruct deep-copies m_roiMap and deletes copy-assignment, so
  LegoExtraActor's copy-construction at legoextraactor.cpp no longer shares
  ownership with the source presenter.
- LegoROI tracks slot back-references; its destructor nulls every registered
  LegoROI** so per-element UAF in arrays like LegoAnimPresenter::m_roiMap is
  impossible. LegoAnimActorStruct / LegoAnimPresenter / LegoLocomotionAnimPresenter
  register and unregister around their array lifetimes.
- LegoAnimationManager::Suspend now invokes ClearMaps on every persistent
  LegoExtraActor (not just those currently in m_extras) so dormant actors
  cannot retain stale m_AnimTreePtr after world teardown. Adds ClearMaps
  overrides on Act2Actor, Act3Cop, Act3Brickster, Act3Shark, LegoRaceCar,
  LegoExtraActor.

INITIAL_MEMORY=128mb is re-added to the Emscripten link options.
This commit is contained in:
foxtacles 2026-05-17 13:11:38 -07:00 committed by GitHub
parent 298e02b858
commit 9e82c461dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 146 additions and 6 deletions

View File

@ -14,7 +14,7 @@ endif()
if (EMSCRIPTEN)
add_compile_options(-pthread -gsource-map)
add_link_options(-sUSE_WEBGL2=1 -sMIN_WEBGL_VERSION=2 -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=2gb -sUSE_PTHREADS=1 -sPROXY_TO_PTHREAD=1 -sOFFSCREENCANVAS_SUPPORT=1 -sPTHREAD_POOL_SIZE_STRICT=0 -sFORCE_FILESYSTEM=1 -sWASMFS=1 -sEXIT_RUNTIME=1 -sABORT_ON_WASM_EXCEPTIONS=1 -gsource-map)
add_link_options(-sUSE_WEBGL2=1 -sMIN_WEBGL_VERSION=2 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=128mb -sMAXIMUM_MEMORY=2gb -sUSE_PTHREADS=1 -sPROXY_TO_PTHREAD=1 -sOFFSCREENCANVAS_SUPPORT=1 -sPTHREAD_POOL_SIZE_STRICT=0 -sFORCE_FILESYSTEM=1 -sWASMFS=1 -sEXIT_RUNTIME=1 -sABORT_ON_WASM_EXCEPTIONS=1 -gsource-map)
set(SDL_PTHREADS ON CACHE BOOL "Enable SDL pthreads" FORCE)
find_program(LLVM_OBJCOPY_BIN NAMES llvm-objcopy HINTS "${EMSCRIPTEN_ROOT_PATH}/../bin" REQUIRED)
set(ISLE_EMSCRIPTEN_VERSION_DIR "${CMAKE_BINARY_DIR}/generated")

View File

@ -39,6 +39,7 @@ class Act2Actor : public LegoAnimActor {
} // vtable+0x68
void Animate(float p_time) override; // vtable+0x70
void ClearMaps() override; // vtable+0x84
MxResult HitActor(LegoPathActor*, MxBool) override; // vtable+0x94
MxResult CalculateSpline() override; // vtable+0x9c
MxS32 NextTargetLocation() override; // vtable+0xa0

View File

@ -19,6 +19,7 @@ class Act3Shark : public LegoAnimActor {
void ParseAction(char*) override; // vtable+0x20
void Animate(float p_time) override; // vtable+0x70
void ClearMaps() override; // vtable+0x84
// LegoAnimActor vtable
virtual MxResult EatPizza(Act3Ammo* p_ammo); // vtable+0x10
@ -104,6 +105,7 @@ class Act3Cop : public Act3Actor {
void ParseAction(char* p_extra) override; // vtable+0x20
void Animate(float p_time) override; // vtable+0x70
void ClearMaps() override; // vtable+0x84
MxResult HitActor(LegoPathActor*, MxBool) override; // vtable+0x94
MxResult CalculateSpline() override; // vtable+0x9c
@ -137,6 +139,7 @@ class Act3Brickster : public Act3Actor {
void ParseAction(char* p_extra) override; // vtable+0x20
void Animate(float p_time) override; // vtable+0x70
void ClearMaps() override; // vtable+0x84
MxResult HitActor(LegoPathActor* p_actor, MxBool p_bool) override; // vtable+0x94
void SwitchBoundary(
LegoPathBoundary*& p_boundary,

View File

@ -9,6 +9,8 @@ class LegoAnim;
// SIZE 0x20
struct LegoAnimActorStruct {
LegoAnimActorStruct(float p_worldSpeed, LegoAnim* p_AnimTreePtr, LegoROI** p_roiMap, MxU32 p_numROIs);
LegoAnimActorStruct(const LegoAnimActorStruct& p_other);
LegoAnimActorStruct& operator=(const LegoAnimActorStruct&) = delete;
~LegoAnimActorStruct();
float GetDuration();

View File

@ -49,6 +49,7 @@ class LegoExtraActor : public virtual LegoAnimActor {
) override; // vtable+0x6c
void Animate(float p_time) override; // vtable+0x70
void ApplyTransform(Matrix4& p_transform) override; // vtable+0x74
void ClearMaps() override; // vtable+0x84
MxU32 StepState(float p_time, Matrix4& p_matrix) override; // vtable+0x90
MxResult HitActor(LegoPathActor* p_actor, MxBool p_bool) override; // vtable+0x94
MxResult CalculateSpline() override; // vtable+0x9c

View File

@ -160,6 +160,7 @@ class LegoRaceCar : public LegoCarRaceActor, public LegoRaceMap {
} // vtable+0x6c
void Animate(float p_time) override; // vtable+0x70
void ClearMaps() override; // vtable+0x84
MxResult HitActor(LegoPathActor* p_actor, MxBool p_bool) override; // vtable+0x94
// FUNCTION: LEGO1 0x10014560

View File

@ -877,3 +877,9 @@ LegoEntity* Act2Actor::GetNextEntity(MxBool* p_isBuilding)
return result;
}
void Act2Actor::ClearMaps()
{
m_shootAnim = NULL;
LegoAnimActor::ClearMaps();
}

View File

@ -1231,3 +1231,22 @@ void Act3Shark::ParseAction(char* p_extra)
m_unk0x38->SetVisibility(FALSE);
m_world->PlaceActor(this);
}
void Act3Cop::ClearMaps()
{
m_eatAnim = NULL;
Act3Actor::ClearMaps();
}
void Act3Brickster::ClearMaps()
{
m_shootAnim = NULL;
Act3Actor::ClearMaps();
}
void Act3Shark::ClearMaps()
{
m_unk0x34 = NULL;
m_unk0x38 = NULL;
LegoAnimActor::ClearMaps();
}

View File

@ -437,11 +437,14 @@ void LegoAnimationManager::Suspend()
LegoROI* roi = m_extras[i].m_roi;
if (roi != NULL) {
LegoPathActor* actor = CharacterManager()->GetExtraActor(roi->GetName());
LegoExtraActor* actor = CharacterManager()->GetExtraActor(roi->GetName());
if (actor != NULL && actor->GetController() != NULL) {
actor->GetController()->RemoveActor(actor);
actor->SetController(NULL);
if (actor != NULL) {
if (actor->GetController() != NULL) {
actor->GetController()->RemoveActor(actor);
actor->SetController(NULL);
}
actor->ClearMaps();
}
CharacterManager()->ReleaseActor(roi);
@ -466,6 +469,18 @@ void LegoAnimationManager::Suspend()
m_extras[i].m_speed = -1.0f;
}
// Catch dormant actors too: despawn paths null m_extras[i].m_roi
// without ClearMaps, leaving stale m_AnimTreePtr behind.
for (i = 0; i < (MxS32) CharacterManager()->GetNumActors(); i++) {
const char* name = CharacterManager()->GetActorName(i);
if (name != NULL) {
LegoExtraActor* extraActor = CharacterManager()->GetExtraActor(name);
if (extraActor != NULL) {
extraActor->ClearMaps();
}
}
}
m_unk0x18 = 0;
m_unk0x1a = FALSE;
m_enableCamAnims = FALSE;

View File

@ -22,13 +22,39 @@ LegoAnimActorStruct::LegoAnimActorStruct(
{
m_worldSpeed = p_worldSpeed;
m_AnimTreePtr = p_AnimTreePtr;
m_roiMap = p_roiMap;
m_numROIs = p_numROIs;
// Deep copy: source array's owner may outlive or rebuild it independently.
m_roiMap = new LegoROI*[p_numROIs + 1];
memcpy(m_roiMap, p_roiMap, (p_numROIs + 1) * sizeof(LegoROI*));
for (MxU32 i = 0; i <= p_numROIs; i++) {
if (m_roiMap[i]) {
m_roiMap[i]->RegisterSlotRef(&m_roiMap[i]);
}
}
}
LegoAnimActorStruct::LegoAnimActorStruct(const LegoAnimActorStruct& p_other)
: m_worldSpeed(p_other.m_worldSpeed), m_AnimTreePtr(p_other.m_AnimTreePtr), m_numROIs(p_other.m_numROIs),
m_unk0x10(p_other.m_unk0x10)
{
m_roiMap = new LegoROI*[m_numROIs + 1];
memcpy(m_roiMap, p_other.m_roiMap, (m_numROIs + 1) * sizeof(LegoROI*));
for (MxU32 i = 0; i <= m_numROIs; i++) {
if (m_roiMap[i]) {
m_roiMap[i]->RegisterSlotRef(&m_roiMap[i]);
}
}
}
// FUNCTION: LEGO1 0x1001c0a0
LegoAnimActorStruct::~LegoAnimActorStruct()
{
for (MxU32 i = 0; i <= m_numROIs; i++) {
if (m_roiMap[i]) {
m_roiMap[i]->UnregisterSlotRef(&m_roiMap[i]);
}
}
delete[] m_roiMap;
for (MxU16 i = 0; i < m_unk0x10.size(); i++) {
delete m_unk0x10[i];
}

View File

@ -536,3 +536,12 @@ inline MxU32 LegoExtraActor::CheckPresenterAndActorIntersections(
return 0;
}
void LegoExtraActor::ClearMaps()
{
delete m_assAnim;
m_assAnim = NULL;
delete m_disAnim;
m_disAnim = NULL;
LegoAnimActor::ClearMaps();
}

View File

@ -736,3 +736,10 @@ MxResult LegoJetski::HitActor(LegoPathActor* p_actor, MxBool p_bool)
return SUCCESS;
}
void LegoRaceCar::ClearMaps()
{
m_skelKick1Anim = NULL;
m_skelKick2Anim = NULL;
LegoAnimActor::ClearMaps();
}

View File

@ -86,6 +86,11 @@ void LegoAnimPresenter::Destroy(MxBool p_fromDestructor)
}
if (m_roiMap != NULL) {
for (MxU32 i = 0; i <= m_roiMapSize; i++) {
if (m_roiMap[i]) {
m_roiMap[i]->UnregisterSlotRef(&m_roiMap[i]);
}
}
delete[] m_roiMap;
}
@ -126,6 +131,11 @@ void LegoAnimPresenter::Destroy(MxBool p_fromDestructor)
}
if (m_ptAtCamROI != NULL) {
for (MxS32 i = 0; i < m_ptAtCamCount; i++) {
if (m_ptAtCamROI[i]) {
m_ptAtCamROI[i]->UnregisterSlotRef(&m_ptAtCamROI[i]);
}
}
delete[] m_ptAtCamROI;
}
@ -415,12 +425,22 @@ void LegoAnimPresenter::BuildROIMap()
LegoAnimStructMap anims;
if (m_ptAtCamROI != NULL) {
for (MxS32 i = 0; i < m_ptAtCamCount; i++) {
if (m_ptAtCamROI[i]) {
m_ptAtCamROI[i]->UnregisterSlotRef(&m_ptAtCamROI[i]);
}
}
memset(m_ptAtCamROI, 0, m_ptAtCamCount * sizeof(*m_ptAtCamROI));
}
UpdateStructMapAndROIIndex(anims, m_anim->GetRoot(), NULL);
if (m_roiMap != NULL) {
for (MxU32 i = 0; i <= m_roiMapSize; i++) {
if (m_roiMap[i]) {
m_roiMap[i]->UnregisterSlotRef(&m_roiMap[i]);
}
}
delete[] m_roiMap;
m_roiMapSize = 0;
}
@ -432,12 +452,14 @@ void LegoAnimPresenter::BuildROIMap()
for (LegoAnimStructMap::iterator it = anims.begin(); it != anims.end();) {
MxU32 index = (*it).second.m_index;
m_roiMap[index] = (*it).second.m_roi;
m_roiMap[index]->RegisterSlotRef(&m_roiMap[index]);
if (m_roiMap[index]->GetName() != NULL) {
for (MxS32 i = 0; i < m_ptAtCamCount; i++) {
if (m_ptAtCamROI[i] == NULL && m_ptAtCamNames[i] != NULL) {
if (!SDL_strcasecmp(m_ptAtCamNames[i], m_roiMap[index]->GetName())) {
m_ptAtCamROI[i] = m_roiMap[index];
m_ptAtCamROI[i]->RegisterSlotRef(&m_ptAtCamROI[i]);
break;
}
}
@ -1025,6 +1047,11 @@ void LegoAnimPresenter::ParseExtra()
}
if (m_ptAtCamROI != NULL) {
for (MxS32 i = 0; i < m_ptAtCamCount; i++) {
if (m_ptAtCamROI[i]) {
m_ptAtCamROI[i]->UnregisterSlotRef(&m_ptAtCamROI[i]);
}
}
delete[] m_ptAtCamROI;
m_ptAtCamROI = NULL;
}
@ -1447,6 +1474,13 @@ void LegoLocomotionAnimPresenter::CreateROIAndBuildMap(LegoAnimActor* p_actor, M
if (m_roiMap != NULL) {
m_roiMapList->Append(m_roiMap);
p_actor->CreateAnimActorStruct(m_anim, p_worldSpeed, m_roiMap, m_roiMapSize);
for (MxU32 i = 0; i <= m_roiMapSize; i++) {
if (m_roiMap[i]) {
m_roiMap[i]->UnregisterSlotRef(&m_roiMap[i]);
}
}
m_roiMap = NULL;
}

View File

@ -96,6 +96,9 @@ LegoROI::LegoROI(Tgl::Renderer* p_renderer, ViewLODList* p_lodList) : ViewROI(p_
// FUNCTION: BETA10 0x10189a42
LegoROI::~LegoROI()
{
for (LegoROI** slot : m_slotRefs) {
*slot = NULL;
}
if (comp) {
CompoundObject::iterator iterator;

View File

@ -5,6 +5,9 @@
#include "misc/legotypes.h"
#include "viewmanager/viewroi.h"
#include <algorithm>
#include <vector>
typedef unsigned char (*ColorOverride)(const char*, char*, unsigned int);
typedef unsigned char (*TextureHandler)(const char*, unsigned char*, unsigned int);
@ -99,6 +102,15 @@ class LegoROI : public ViewROI {
void SetBoundingSphere(const BoundingSphere& p_sphere) { m_sphere = m_world_bounding_sphere = p_sphere; }
void SetBoundingBox(const BoundingBox& p_box) { m_bounding_box = p_box; }
void RegisterSlotRef(LegoROI** p_slot) { m_slotRefs.push_back(p_slot); }
void UnregisterSlotRef(LegoROI** p_slot)
{
std::vector<LegoROI**>::iterator it = std::find(m_slotRefs.begin(), m_slotRefs.end(), p_slot);
if (it != m_slotRefs.end()) {
m_slotRefs.erase(it);
}
}
// SYNTHETIC: LEGO1 0x100a82b0
// SYNTHETIC: BETA10 0x1018c490
// LegoROI::`scalar deleting destructor'
@ -108,6 +120,7 @@ class LegoROI : public ViewROI {
BoundingSphere m_sphere; // 0xe8
LegoBool m_sharedLodList; // 0x100
LegoEntity* m_entity; // 0x104
std::vector<LegoROI**> m_slotRefs;
};
// VTABLE: LEGO1 0x100dbea8