Abstract
Game developers use gcinfo to detect memory (GC) spikes. You can bypass by using hookfunction, but make sure to return a sensible and dynamic value to avoid more advanced detection. However, the root cause can be functions like getgc which create a copy of the entire GC, almost doubling it. To avoid that, use filtergc which only pick out the specific values you want. Excessive use of instances like Frames/TextBoxes can also cause spikes.
Introduction
Thanks to Roblox’s sandbox, the only tool game developers have to check memory is gcinfo, returning the “total memory heap size in kilobytes”. Under usual loads, gcinfo is relatively stable within a range. However, when a script is executed, it could suddenly spike, casuing you to get “detected”. Game developers can benchmark the usual range and then flag anything outside of that.
Example Code
local function Detected()
while true do end
end
local function Range(Values)
local Min = math.huge
local Max = 0
for Value in ipairs(Values) do
if Value > Max then
Max = Value
elseif Value < Min then
Mix = Value
end
end
return Max, Min
end
local PreviousValues = {}
RunService.RenderStepped:Connect(function()
local Memory = gcinfo()
if Memory > 600 or Memory < 300 then
Detected()
end
table.insert(PreviousValues, Memory)
if #PreviousValues > 5 then
table.remove(PreviousValues, 1)
end
local Min, Max = Range(PreviousValues)
local Range = Max - Min
if Range == 0 or Range < 5 or Range > 100 then
Detected()
end
end)
Understanding the example code
To start, it’s important to realise that the code is just an example (and untested). It can easily be disabled through various methods, however, that isn’t the focus. The magic numbers chosen are random, and aren’t real values. Anyway, the code is able to detect spikes or troughs in memory usage. It can also detect lazy hooks that don’t properly simulate realistic memory usage.
Initial approach
local old
old = hookfunction(getrenv().gcinfo, function()
return 0
end)
This approach will get detected, due to the trough check. Note the usage of getrenv. This ensures we hook the game’s version of gcinfo, and not our own (if the executor properly implements getrenv aka get roblox environment). However, we can improve the hook by making it return a random value between the range that the game wants:
local old
old = hookfunction(getrenv().gcinfo, function()
return math.random(300, 600)
end)
Why do spikes occur?
However, I think it would be better if we address the actual problem at hand: the memory spikes. Majority of scripts should not spike so drastically. It could happen due to making many instances at once, like with Dex explorer. However, more commonly, iterating through getgc. Lets look at an example of how getgc is implemented within executors. Do not worry if you do not understand.
int RbxApi::getgc(DWORD rL)
{
const syn::RbxLua RL(rL);
auto IncludeTables = false;
if (RL.IsBoolean(1))
IncludeTables = RL.ToBoolean(1);
const auto GlobalState = (DWORD) syn::PointerObfuscation::DeObfuscateGlobalState(RL + L_GS);
const auto DeadMask = *(BYTE*)(GlobalState + G_WMASK) ^ 3;
auto Object = *(GCObject**)(GlobalState + G_ROOTGC);
RL.NewTable();
auto n = 1;
while (Object != nullptr)
{
const auto TT = *(BYTE*)((DWORD) Object + GCO_TT);
/* Make sure the object is not dead and is a 'safe' type (without including tables) */
if ((TT == R_LUA_TFUNCTION || (IncludeTables ? TT == R_LUA_TTABLE : TT == R_LUA_TFUNCTION) || (IncludeTables ? TT == R_LUA_TUSERDATA : TT == R_LUA_TFUNCTION)) && (*(BYTE*)((DWORD) Object + GCO_MARKED) ^ 3) & DeadMask)
{
RL.PushInteger(n++);
RL.PushRawObject((DWORD) Object, TT);
RL.SetTable(-3);
}
Object = Object->gch.next;
}
return 1;
}
The main takeaways are that it creates a new lua table, then adds each element inside of the gc (garbage collector) into that new table, essentially making a copy of the garbage collector. The GC contains all tables, functions and userdata currently in use by the game. Therefore, understandably, adding everything into a new table would cause a spike in memory. Note that it being a lua table is important. Since it’s a lua table, it’s now also in the GC, resulting a spike in what gcinfo returns.
The better approach
However, how do you avoid using getgc? Introduced by Synapse V3, and being introduced into sUNC, a new function called filtergc is being implemented into many executors. While seeming lame at first, it can be very powerful and more efficient. Instead of adding every element inside of the GC, it only adds the specific ones you want, therefore no memory spike! Note that some executors implement a lua version which uses getgc, and while it does the same thing in the end, it does not have these performance benefits and is detected.