Skip to content

Commit 5dc09d9

Browse files
committed
- Fix GTA memory allocator issues like the notorious 0x0032F4DE crash that just wouldn't die
- Massively improve streaming performance and constraints (CMemoryMgr::MallogAlign is a big part of it) Some details (not all up-to-date): https://pastebin.com/hX5Gffnw
1 parent 0a36c63 commit 5dc09d9

File tree

3 files changed

+356
-1
lines changed

3 files changed

+356
-1
lines changed

Client/multiplayer_sa/CMultiplayerSA.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class CMultiplayerSA : public CMultiplayer
7777
void InitHooks_Direct3D();
7878
void InitHooks_FixLineOfSightArgs();
7979
void InitHooks_Streaming();
80+
void InitHooks_FixMallocAlign();
8081
void InitHooks_FrameRateFixes();
8182
void InitHooks_ProjectileCollisionFix();
8283
void InitHooks_ObjectStreamerOptimization();

Client/multiplayer_sa/CMultiplayerSA_1.3.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
*
33
* PROJECT: Multi Theft Auto v1.0
44
* LICENSE: See LICENSE in the top level directory
5-
* FILE: multiplayer_sa/CMultiplayerSA.cpp
5+
* FILE: multiplayer_sa/CMultiplayerSA_1.3.cpp
66
* PURPOSE: Multiplayer module class 1.3
77
*
88
* Multi Theft Auto is available from https://www.multitheftauto.com/
@@ -141,6 +141,8 @@ void HOOK_CBoat_ApplyDamage();
141141
void HOOK_CProjectile_FixTearGasCrash();
142142
void HOOK_CProjectile_FixExplosionLocation();
143143
void HOOK_CPed_RemoveWeaponWhenEnteringVehicle();
144+
void* __cdecl HOOK_CMemoryMgr_MallocAlign(int size, int alignment, int nHint);
145+
void __cdecl HOOK_CMemoryMgr_FreeAlign(void* ptr);
144146

145147
void CMultiplayerSA::Init_13()
146148
{
@@ -213,6 +215,7 @@ void CMultiplayerSA::InitHooks_13()
213215
InitHooks_VehicleDummies();
214216
InitHooks_Vehicles();
215217
InitHooks_Rendering();
218+
InitHooks_FixMallocAlign();
216219
}
217220

218221
void CMultiplayerSA::InitMemoryCopies_13()
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
/*****************************************************************************
2+
*
3+
* PROJECT: Multi Theft Auto v1.0
4+
* LICENSE: See LICENSE in the top level directory
5+
* FILE: multiplayer_sa/CMultiplayerSA_FixMallocAlign.cpp
6+
* PURPOSE: Fixed version of GTA:SA CMemoryMgr
7+
*
8+
* Multi Theft Auto is available from https://www.multitheftauto.com/
9+
*
10+
*****************************************************************************/
11+
12+
#include "StdInc.h"
13+
14+
namespace mta::memory
15+
{
16+
constexpr bool is_power_of_two(std::size_t value) noexcept
17+
{
18+
return value != 0 && (value & (value - 1)) == 0;
19+
}
20+
21+
void* SafeMallocAlignVirtual(std::size_t size, std::size_t alignment) noexcept;
22+
23+
// Safe aligned memory allocation replacement for CMemoryMgr::MallocAlign
24+
// Must match original GTA:SA behavior exactly - stores original pointer at result-4
25+
[[nodiscard]] void* SafeMallocAlign(std::size_t size, std::size_t alignment) noexcept
26+
{
27+
// Validate alignment FIRST to prevent undefined behavior in calculations
28+
if (alignment == 0 || !is_power_of_two(alignment) || alignment > 8192)
29+
{
30+
return nullptr;
31+
}
32+
33+
// Safety checks - these are fast and prevent crashes from direct calls
34+
if (size == 0)
35+
{
36+
return nullptr;
37+
}
38+
39+
// Check for x86 size limits (2GB max for user-mode allocations)
40+
// Reserve space for actual alignment overhead
41+
if (size > 0x7FFFFFFF - alignment - sizeof(void*))
42+
{
43+
return nullptr;
44+
}
45+
46+
// For very large allocations, use VirtualAlloc
47+
if (size > 1024 * 1024) // 1MB threshold
48+
{
49+
return SafeMallocAlignVirtual(size, alignment);
50+
}
51+
52+
// Calculate total size needed with x86-specific overflow protection
53+
const std::uint32_t size_u32 = static_cast<std::uint32_t>(size);
54+
const std::uint32_t align_u32 = static_cast<std::uint32_t>(alignment);
55+
56+
// Validate that casting to uint32 didn't truncate the values (x86 safety)
57+
if (static_cast<std::size_t>(size_u32) != size || static_cast<std::size_t>(align_u32) != alignment)
58+
{
59+
return nullptr;
60+
}
61+
62+
const std::uint32_t total_size = size_u32 + align_u32 + sizeof(void*);
63+
64+
// Check for overflow and validate total size
65+
if (total_size < size_u32 || total_size > 0x7FFFFFFF)
66+
{
67+
return nullptr;
68+
}
69+
70+
void* raw_memory = nullptr;
71+
try
72+
{
73+
raw_memory = malloc(total_size);
74+
if (!raw_memory)
75+
{
76+
return nullptr;
77+
}
78+
79+
// Calculate aligned address like original GTA:SA with safer arithmetic
80+
std::uint32_t raw_addr = reinterpret_cast<std::uint32_t>(raw_memory);
81+
82+
// Check for arithmetic overflow in alignment calculation
83+
// We need to check if (raw_addr + align_u32 + sizeof(void*)) would overflow uint32_t
84+
if (raw_addr > 0xFFFFFFFFU - align_u32 - sizeof(void*))
85+
{
86+
free(raw_memory);
87+
return nullptr;
88+
}
89+
90+
std::uint32_t aligned_addr = (raw_addr + align_u32 + sizeof(void*)) & ~(align_u32 - 1);
91+
92+
// Enhanced bounds checking for pointer storage
93+
if (aligned_addr < raw_addr + sizeof(void*) || // Minimum offset check (includes wraparound protection)
94+
aligned_addr > raw_addr + total_size) // Upper bound check
95+
{
96+
free(raw_memory);
97+
return nullptr;
98+
}
99+
100+
void* result = reinterpret_cast<void*>(aligned_addr);
101+
102+
// Store original pointer exactly like GTA:SA
103+
*reinterpret_cast<void**>(aligned_addr - sizeof(void*)) = raw_memory;
104+
105+
return result;
106+
}
107+
catch (...)
108+
{
109+
// Clean up allocated memory if an exception occurred
110+
if (raw_memory)
111+
{
112+
free(raw_memory);
113+
}
114+
return nullptr;
115+
}
116+
}
117+
118+
// VirtualAlloc implementation for large allocations
119+
[[nodiscard]] void* SafeMallocAlignVirtual(std::size_t size, std::size_t alignment) noexcept
120+
{
121+
// Validate parameters first to prevent undefined behavior
122+
if (alignment == 0 || !is_power_of_two(alignment) || alignment > 8192)
123+
{
124+
return nullptr;
125+
}
126+
127+
if (size == 0)
128+
{
129+
return nullptr;
130+
}
131+
132+
const std::uint32_t size_u32 = static_cast<std::uint32_t>(size);
133+
const std::uint32_t align_u32 = static_cast<std::uint32_t>(alignment);
134+
135+
// Validate that casting to uint32 didn't truncate the values
136+
if (static_cast<std::size_t>(size_u32) != size || static_cast<std::size_t>(align_u32) != alignment)
137+
{
138+
return nullptr;
139+
}
140+
141+
// Check for overflow before allocation
142+
if (size_u32 > 0x7FFFFFFF - align_u32 - sizeof(void*) - 4096)
143+
{
144+
return nullptr;
145+
}
146+
147+
// Calculate total size with overflow protection
148+
std::uint32_t total_size_check = size_u32 + align_u32 + sizeof(void*) + 4096;
149+
if (total_size_check < size_u32) // Overflow occurred
150+
{
151+
return nullptr;
152+
}
153+
154+
const DWORD total_size = total_size_check;
155+
156+
void* raw_ptr = VirtualAlloc(nullptr, static_cast<SIZE_T>(total_size), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
157+
if (!raw_ptr)
158+
{
159+
return nullptr;
160+
}
161+
162+
// Calculate aligned address with overflow protection
163+
std::uint32_t raw_addr = reinterpret_cast<std::uint32_t>(raw_ptr);
164+
165+
// Check for arithmetic overflow in alignment calculation
166+
// We need to check if (raw_addr + align_u32 + sizeof(void*)) would overflow uint32_t
167+
if (raw_addr > 0xFFFFFFFFU - align_u32 - sizeof(void*))
168+
{
169+
VirtualFree(raw_ptr, 0, MEM_RELEASE);
170+
return nullptr;
171+
}
172+
173+
std::uint32_t aligned_addr = (raw_addr + align_u32 + sizeof(void*)) & ~(align_u32 - 1);
174+
175+
// Enhanced bounds checking with wraparound detection
176+
if (aligned_addr < raw_addr + sizeof(void*) || // Minimum offset check (includes wraparound protection)
177+
aligned_addr > raw_addr + total_size) // Upper bound check
178+
{
179+
VirtualFree(raw_ptr, 0, MEM_RELEASE);
180+
return nullptr;
181+
}
182+
183+
void* result = reinterpret_cast<void*>(aligned_addr);
184+
185+
// Store original pointer for SafeFreeAlign compatibility
186+
*reinterpret_cast<void**>(aligned_addr - sizeof(void*)) = raw_ptr;
187+
188+
return result;
189+
}
190+
191+
// Safe aligned memory deallocation
192+
void SafeFreeAlign(void* ptr) noexcept
193+
{
194+
if (!ptr)
195+
return;
196+
197+
try
198+
{
199+
std::uint32_t ptr_addr = reinterpret_cast<std::uint32_t>(ptr);
200+
201+
// Enhanced pointer validation for x86 user-mode address space
202+
// x86 user-mode: 0x00010000 - 0x7FFFFFFF (avoid null page and kernel space)
203+
if (ptr_addr < 0x10000 || ptr_addr >= 0x80000000)
204+
{
205+
return;
206+
}
207+
208+
// Check pointer alignment - must be at least 4-byte aligned for x86
209+
if ((ptr_addr & 3) != 0)
210+
{
211+
return;
212+
}
213+
214+
// Validate that we can safely read the stored pointer
215+
void* stored_ptr_location = reinterpret_cast<void*>(ptr_addr - sizeof(void*));
216+
217+
// Check if the memory location is readable using VirtualQuery
218+
MEMORY_BASIC_INFORMATION mbi_check;
219+
SIZE_T query_result = VirtualQuery(stored_ptr_location, &mbi_check, sizeof(mbi_check));
220+
if (query_result == 0 || mbi_check.State != MEM_COMMIT)
221+
{
222+
return;
223+
}
224+
225+
// Check if memory is readable (any readable protection flag)
226+
const DWORD readable_flags = PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_WRITECOPY;
227+
if (!(mbi_check.Protect & readable_flags))
228+
{
229+
return;
230+
}
231+
232+
void* original_ptr = *reinterpret_cast<void**>(ptr_addr - sizeof(void*));
233+
std::uint32_t original_addr = reinterpret_cast<std::uint32_t>(original_ptr);
234+
235+
// Validation for original pointer
236+
// Must be in valid user-mode range (alignment checked separately)
237+
if (original_addr < 0x10000 || original_addr >= 0x7FFFFFFF)
238+
{
239+
return;
240+
}
241+
242+
// Validate alignment is reasonable (4-byte minimum for x86)
243+
if ((original_addr & 3) != 0)
244+
{
245+
return;
246+
}
247+
248+
// Determine allocation method using VirtualQuery
249+
MEMORY_BASIC_INFORMATION mbi;
250+
SIZE_T mbi_result = VirtualQuery(original_ptr, &mbi, sizeof(mbi));
251+
if (mbi_result != 0)
252+
{
253+
// Check if this is VirtualAlloc memory
254+
const std::uint32_t base_addr = reinterpret_cast<std::uint32_t>(mbi.AllocationBase);
255+
256+
// Safe overflow-protected calculation for region end
257+
std::uint32_t region_end;
258+
if (base_addr > 0xFFFFFFFFU - static_cast<std::uint32_t>(mbi.RegionSize))
259+
{
260+
// Overflow would occur - this indicates corrupted memory info
261+
return;
262+
}
263+
region_end = base_addr + static_cast<std::uint32_t>(mbi.RegionSize);
264+
265+
if (mbi.Type == MEM_PRIVATE && mbi.State == MEM_COMMIT && original_addr >= base_addr && original_addr < region_end)
266+
{
267+
VirtualFree(original_ptr, 0, MEM_RELEASE);
268+
}
269+
else
270+
{
271+
free(original_ptr);
272+
}
273+
}
274+
else
275+
{
276+
// VirtualQuery failed - assume malloc
277+
free(original_ptr);
278+
}
279+
}
280+
catch (...)
281+
{
282+
// Ignore exceptions in free - standard behavior
283+
}
284+
}
285+
} // namespace mta::memory
286+
287+
// Hook constants
288+
#define HOOKPOS_CMemoryMgr_MallocAlign 0x72F4C0
289+
#define HOOKSIZE_CMemoryMgr_MallocAlign 5
290+
291+
#define HOOKPOS_CMemoryMgr_FreeAlign 0x72F4F0
292+
#define HOOKSIZE_CMemoryMgr_FreeAlign 5
293+
294+
// Hook replacement functions
295+
// These functions directly replace the GTA:SA memory allocation functions.
296+
// They already have the correct __cdecl calling convention and signature,
297+
// so no assembly wrappers or trampolines are needed.
298+
// No extern "C" to match the rest of MTA's hook style
299+
300+
// Direct replacement for CMemoryMgr::MallocAlign (SA address 0x72F4C0)
301+
// Original signature: void* __cdecl(int size, int align, int nHint)
302+
// This function IS the hook - no wrapper needed
303+
void* __cdecl HOOK_CMemoryMgr_MallocAlign(int size, int alignment, int nHint)
304+
{
305+
if (size <= 0 || size > 0x7FFFFFFF)
306+
{
307+
return nullptr;
308+
}
309+
310+
if (alignment <= 0 || alignment > 8192 || !mta::memory::is_power_of_two(static_cast<std::size_t>(alignment)))
311+
{
312+
return nullptr;
313+
}
314+
315+
if (static_cast<std::uint32_t>(size) > 0x7FFFFFFF - static_cast<std::uint32_t>(alignment) - sizeof(void*))
316+
{
317+
return nullptr;
318+
}
319+
320+
// Ignore nHint parameter (not used in our implementation)
321+
return mta::memory::SafeMallocAlign(static_cast<std::uint32_t>(size), static_cast<std::uint32_t>(alignment));
322+
}
323+
324+
// Direct replacement for CMemoryMgr::FreeAlign (SA address 0x72F4F0)
325+
// Original signature: void __cdecl(void* ptr)
326+
// This function IS the hook - no wrapper needed
327+
void __cdecl HOOK_CMemoryMgr_FreeAlign(void* ptr)
328+
{
329+
mta::memory::SafeFreeAlign(ptr);
330+
}
331+
332+
//////////////////////////////////////////////////////////////////////////////////////////
333+
//
334+
// Setup hooks for memory allocation functions
335+
// Using direct function pointers - no assembly wrappers needed
336+
//
337+
//////////////////////////////////////////////////////////////////////////////////////////
338+
void CMultiplayerSA::InitHooks_FixMallocAlign()
339+
{
340+
try
341+
{
342+
// Install hooks using the direct C functions
343+
// The functions already have the correct __cdecl calling convention and matching signatures, so they work as direct replacements
344+
HookInstall(HOOKPOS_CMemoryMgr_MallocAlign, reinterpret_cast<DWORD>(HOOK_CMemoryMgr_MallocAlign), HOOKSIZE_CMemoryMgr_MallocAlign);
345+
HookInstall(HOOKPOS_CMemoryMgr_FreeAlign, reinterpret_cast<DWORD>(HOOK_CMemoryMgr_FreeAlign), HOOKSIZE_CMemoryMgr_FreeAlign);
346+
}
347+
catch (...)
348+
{
349+
// Silent failure - use original functions
350+
}
351+
}

0 commit comments

Comments
 (0)