closures
Contains functions which interact and modify closures and their behaviour.
Functions
getfunctionhash
closures.
getfunctionhash
(
fn:
(
T...
)
→
U...
,
--
The function to hash.
hashType:
"SHA384"
|
"BLAKE2B"
|
"BLAKE3"
--
The hash type, defaults to SHA384.
) →
string
--
The hex representation of the selected hashType hash of the function.
Provides a hash with a digest size of 48 bytes (384 bits) in hex format for the given luau function
Implementation
You must use the instructions and constants of the given function to obtain the hash.
Compiler
If your function is outputting different hashes every time its called, then you may have forgotten to account for Compiler settings. We recommend you read this post to know to resolve this issue and make your Luau compiler rSUNC compliant.
newcclosures/ newlclosure hashing
For newcclosures and newlclosures, the result of the hash must be come from the 'deepest' closure it wraps.
If such a closure is a C closure, then the function must error.
Errors
Type | Description |
---|---|
the closure must resolve to a Luau closure | A wrapper function was provided, but when resolving its root closure, it didn't point to a luau closure. |
function must be a Luau closure. | The first parameter was a C function instead of a Luau function. |
Unsupported hash type. | The given hash type is not supported by the executor. |
hookfunction
closures.
hookfunction
(
fn:
(
T...
)
→
U...
,
--
Function to hook.
hook:
(
T...
)
→
U...
--
The hook.
) →
(
T...
)
→
U...
--
The old/original function.
Hooks the given function with the desired hook. This function supports hooking all kinds of closures, from Lua to C, C to Lua, etc.
Implementations
Some implementations of this function may bypass upvalue limits for you. If this is not the case, wrap hook
in newcclosure
/newlclosure
before calling hookfunction
.
Restoring Hooked Functions
You can restore functions hooked with hookfunction
using restorefunction
, in case restorefunction
is not available you may unhook by calling hookfunction
with the first function being the hooked function, and the second being the original one.
Proto and savedpc
When a proto is hooked and it is on a callstack of any thread you must emit a clone of it, and set the func parameter of the callstack to the clone.
This will prevent a vulnerability where, upon returning from a yield or C code, the savedpc which was set with the original proto, attempts to resume execution in the new proto.
This causes: - Crashes in the form of Segmentation Faults - Line number error to be displayed improperly.
Among other things, you must either emit a clone of the proto or replace the callstack of that thread with the hooked one and perform all proper validation for it.
Example of line number being dislayed improperly if the function implementation is vulnerable: https://i.imgur.com/5sSfnut.png RbxStu V4 has fixed this issue already!
Vulnerability test code:
local bFinishedExecution = false
local bExecuted = false
local a
local function b()
print"b babyyy"
print"b baby"
print"b babyyy"
restorefunction(a) -- may be affected as well, and you must apply similar fixes!
warn("im b")
bExecuted = true
end
a = function()
local x = hookfunction(a, b)
task.wait()
print("I'm a")
bFinishedExecution = true
return x
end
local s, e = pcall(a)
assert(s, "savedpc [1]")
assert((bFinishedExecution and not bExecuted) or (not bFinishedExecution and bExecuted), "savedpc [2]")
print("no issues most likely")
--[[
When hooking and suspending, the current instruction is saved into the threads' CallInfo->savedpc to resume. But as we have changed the Proto object, the address and offset of this savedpc, is NO LONGER VALID!
Attempting to do anything with it would likely yield us either: a Luau C error with line number of the error in a completely invalid line, undefined behaviour and inexplicable crashes.
This is a potential ACE that MUST be fixed and handled properly.
]]
Examples:
Testing hookfunction and restorefunction:
local function a()
return "baa"
end
local function b()
return "caa"
end
local aReturn = a()
local bReturn = b()
local hooked = closures.hookfunction(a, b)
local hookedReturn = hooked()
assert(hookedReturn == aReturn)
assert(a() == b() and a() == bReturn)
assert(hooked ~= a and b ~= a)
closures.restorefunction(hooked, a) -- Restore a again.
assert(a() == aReturn and a() ~= hookedReturn)
-- If this script errors, your hookfunction may not be working properly.
Example testing upvalue limits:
local a = "A"
local b = "B"
local c = "C"
local function a() -- Upvalue count: 3
a = "B"
b = "C"
c = "D"
end
local function b() -- Upvalue Count: 2
a = "E"
b = "F"
end
a() -- Set variables
local original = closures.hookfunction(a, b) -- If this call fails, consider implicitly wrapping `b` in a newlclosure!
a()
assert(a == "E" and b == "F" and c == "D") -- If this errors, the upvalues are improperly set, and this could lead to a crash!
assert()
Example testing hooking pairs:
local upref = false
local dummyFunc = function(f)
upref = f == "Hello"
end
local old; old = closures.hookfunction(coroutine.resume, dummyFunc) -- Hook C -> L. This test also checks that hookfunction can bypass upvalue limits.
coroutine.resume("Hello")
if not upref then
error("hookfunction", "Failed to hook C -> L Closures")
end
upref = false
closures.hookfunction(coroutine.resume, old)
pcall(coroutine.resume, "Hello")
Errors
Type | Description |
---|---|
Too many upvalues! | The function to hook has more upvalues than the function you want to hook it with. (This error may not show on some implementations.) |
This function cannot be hooked from Luau | The function has been marked as unhookable by `protectfunction` or by native executor code. |
restorefunction
closures.
restorefunction
(
restoreWhat:
(
T...
)
→
U...
--
The function that should be restored.
) →
(
)
Restores any hooks done to restoreWhat
, regardless of the hook type (i.e., C closure hook, Luau closure hook, ...) and of how many hooks were done to restoreWhat
.
Vulnerability
This function is vulnerable to the savedpc
vulnerability, refer to hookfunction
's documentation on how to fix this vulnerability!
Errors
Type | Description |
---|---|
Function is not hooked | The function is not hooked, and thus cannot be restored. Use `ishooked` before calling this function! |
loadstring
closures.
loadstring
(
luauCode:
string
,
--
The code to compile and load.
chunkName:
string?
--
The name of the chunk to load.
) →
(
(
(
T...
)
→
U...
)
|
nil
,
--
The loaded function, if loading succeeded.
string?
--
The error message, if loading failed.
)
Compiles and loads the given luauCode
. If chunkName
is not provided, it must default to either: a pseudo-randomly generated string, or =loadstring
.
Loadstring must compile the code with the following compile configuration: - Optimization Level 1 - Debug Level 1
The function must also mark the environment as 'unsafe' to disable environment optimizations as per Luau specifications.
Errors
Type | Description |
---|---|
N/A | This function does not explicitly error, however it returns an error message if the code fails to load in the second return. |
newcclosure
closures.
newcclosure
(
fn:
(
T...
)
→
U...
,
--
Function to wrap
debugName:
string?
--
The name of the function, optional.
) →
(
T...
)
→
U...
Wraps the given closure into a upvalue-less C closure.
Detections
The proxy emited by newlclosure and the called function MUST be automatically hidden from the callstack using sethiddenstack
to prevent callstack attacks.
While executor functions are automatically hidden, if the closure given is not a hidden function (i.e., a roblox or game function) you must hide it accordingly.
Yielding
The emitted closure must yield. To implement this, check for lua_pcall on the Luau source!
newlclosure
closures.
newlclosure
(
fn:
(
T...
)
→
U...
--
Function to wrap
) →
(
T...
)
→
U...
Wraps the given closure into a upvalue-less L closure.
Obfuscators
Obfuscators like Luraph™ use the function environment to wrap closures and bypass upvalue limits. Make sure that the environment of the pushed wrapper proxies the environment of the original function using __index
. This should fix issues relating to Luraph™ not working with this function.
WARNING
This function, depending on the implementation, may rely on function environments to work or in a closure map-backed proxy function (like in newcclosure). Beware of this when using the function.
Detections
The proxy emited by newlclosure and the called function MUST be automatically hidden from the callstack using sethiddenstack
to prevent callstack attacks.
While executor functions are automatically hidden, if the closure given is not a hidden function (i.e., a roblox or game function) you must hide it accordingly.
Example Implementation (In Luau)
local function newlclosure<T..., U...>(f: (T...) -> U...): (T...) -> U...
local env = getfenv(f) -- Get environment (env of f gets deoptimized)
local x = setmetatable({
__F = f, -- Store function on environment (prevents upreference)
}, {
__index = env, -- proxy original fenv for obfuscator support
__newindex = env, -- proxy original fenv for obfuscator support
})
local nf = function(...)
return __F(...) -- proxy call
end
setfenv(nf, x) -- set func env (env of nf gets deoptimized)
return nf
end
wrapclosure
closures.
wrapclosure
(
fn:
(
T...
)
→
U...
--
Function to wrap
) →
(
T...
)
→
U...
--
A copy of the function, without upvalues.
Wraps the given closure into an upvalue-less version of itself.
INFO
This function is a proxy to the correct implementation of newcclosure
/ newlclosure
.
Examples:
local b = "b"
local function a() -- Upvalue Count: 1 ('b')
b = "a"
end
assert(#debug.getupvalues(a) == 1, "debug.getupvalues failure") -- Validate the behaviour of debug.getupvalues.
assert(#debug.getupvalues(closures.wrapclosure(a)) == 0, "wrapped closure has upreferences/upvalues")
assert(islclosure(a) == iscclosure(closures.wrapclosure(a)), "closure type mismatch")
-- If this script errors, your wrapclosure may not be working properly.
isexecutorclosure
closures.
isexecutorclosure
(
fn:
(
T...
)
→
U...
--
The function to check.
) →
boolean
--
Whether the function is an executor closure.
Checks if the given function is an executor closure.
Implementation
The function may use any method, as long as it returns true for executor Luau and C functions, as well as hooked functions, and false for any non-executor functions. This means that functions retrieved from loadstring or getscriptclosure will also return true.
clonefunction
closures.
clonefunction
(
fn:
(
T...
)
→
U...
--
The function to clone.
) →
(
T...
)
→
U...
--
A clone of the function.
Clones the given function, providing a unique copy of it that is completely unrelated to fn
.
Obfuscator Support
You must note that a cloned Luau function must have the SAME environment as the original function. If you do not provide an environment and an obfuscator depends on it for upvalues, you may face script errors.
This has been documented behaviour on scripts obfuscated by obfuscators such as Luraph, where newlclosure
and/or clonefunction
do not properly set the environment of the cloned function, which causes script errors.
isfunctionprotected
closures.
isfunctionprotected
(
fn:
(
T...
)
→
U...
--
The function to check.
) →
boolean
--
If true, the function is protected against hooking.
Returns if the given function is protected against hooks.
Whitelists
You can use this function if your executor abides by rSUNC to determine if the environment is proper for your whitelist.
ishooked
closures.
ishooked
(
fn:
(
T...
)
→
U...
--
The function to check.
) →
boolean
--
Whether the function has been hooked.
Returns if the given function has been hooked.
Whitelists
You can use this function if your executor abides by rSUNC to determine if the environment is proper for your whitelist.
protectfunction
closures.
protectfunction
(
fn:
(
T...
)
→
U...
--
The function to make unhookable and remove all hooks of.
) →
(
)
Makes a function unable to be hooked. This function automatically reverts ANY hooks placed on the function if there are any set.
comparefunction
closures.
comparefunction
(
fn1:
(
T...
)
→
U...
,
--
The first function to compare.
fn2:
(
T...
)
→
U...
--
The second function to compare.
) →
boolean
--
Whether the two functions point to the same code.
Returns true if the function points to the same code. The function should return false if the function type results in a mis-match (i.e., fn1
is a Luau closure and fn2
is a C closure).
newcclosures/ newlclosure comparison
For newcclosures and newlclosures, the result must be derived from the 'deepest' closure it wraps.
For example, if fn1
is a newcclosure that wraps a newlclosure that wraps a luau function, and fn2
is a Luau function, you must search to the deepest call fn1 can do and check if it is fn2
.
The same should happen if fn2
is the nested one and fn1
is a luau function.
This function does not check if the function code is the same, only if the two functions point to the same 'Prototype' or 'C function'.
Examples
print"comparefunction test"
local x = function() end
print(comparefunction(function() end, function() end)) -- false
print(comparefunction(x, x)) -- true
print(comparefunction(x, clonefunction(x))) -- true
print(comparefunction(x, newcclosure(x))) -- true
print(comparefunction(x, newlclosure(x))) -- true
print(comparefunction(newcclosure(x), x)) -- true
print(comparefunction(newlclosure(x), x)) -- true
print(comparefunction(print, newcclosure(print))) -- true
print(comparefunction(print, newlclosure(print))) -- true
print(comparefunction(print, newcclosure(warn))) -- false
print(comparefunction(print, newlclosure(warn))) -- false
print(comparefunction(newcclosure(print), newcclosure(print))) -- true
print(comparefunction(
newcclosure(
newlclosure(
newcclosure(
print
)
)
),
newlclosure(
newcclosure(
newlclosure(
print
)
)
))
) -- true
print"comparefunction test end"
iswrappedclosure
closures.
iswrappedclosure
(
fn:
(
T...
)
→
U...
--
The function to check.
) →
boolean
--
Whether the function has been wrapped using a wrapper function, such as newlclosure, newcclosure or wrapclosure.
Returns if the given function is a wrapper function.
sethiddenstack
closures.
sethiddenstack
(
fnOrLevel:
number
|
(
T...
)
→
U...
,
--
The function or stack level to hide.
hidden:
boolean
--
If true, the function will be hidden from the callstack, else it will be unhidden.
) →
(
)
Hides a function from functions which expose the callstack to users (i.e., getfenv
, setfenv
, debug.info
and debug.traceback
).
Implementation
You must hide the Proto and the backing C function, not the actual Closure object. This is because the closure can be cloned, and then it will appear on the callstack!
INFO
All executor functions must be hidden from the callstack by default, and this cannot change regardless of the users' desire.
ishiddenstack
closures.
ishiddenstack
(
fnOrLevel:
number
|
(
T...
)
→
U...
--
The function or stack level to check.
) →
boolean
--
If true, the function is hidden from the callstack.
Returns if the given function or stack level is hidden from functions which expose the callstack to users.
INFO
All executor functions must be hidden from the callstack by default, and this cannot change regardless of the users' desire.
trampoline_call
closures.
trampoline_call
(
fn:
(
T...
)
→
U...
,
--
The function to call with the fake stack and new thread.
stackLevels:
{
CallStack
}
,
--
An array of callstacks, the first item will be the beginning of the callstack.
threadInitializationInformation:
ThreadInitializationInformation
,
--
The thread initialization information to use.
...:
K...
--
Additional arguments to pass to the function.
) →
(
boolean
,
--
Whether the function was successfully called.
J...
--
The return of the function
)
Creates a new thread and sets up a fake call stack for it, then calls the given function.
INFO
If on the stackLevels
, a CallStack
's currentline
is not given (i.e., nil
), the beginning of the function must be used by default.