Skip to content

Conversation

@yogwoggf
Copy link
Collaborator

@yogwoggf yogwoggf commented Oct 31, 2025

Resolves #40
Resolves #43

Changes

  • Adds a new, highly advanced stack spoofing algorithm which includes frame stitching to seamlessly "splice" Autorun out of any untrusted code. This means, even if some code is somehow arbitrarily executed in Autorun's context (say, a bad detour that calls a metamethod by accident), that code cannot do anything to detect the presence of Autorun explicitly.
  • Adds a new file to the stdlib, protections.lua that installs a few necessary and critical safe-call wrappers to protect Autorun. This is NOT something related to Safety Equivalent #10 .
  • Hooks lj_debug_funcname to frame stitch the original call stack and restores proper errors for fast functions without anything unusual happening.
  • Adds pcall_forward and error to LuaApi which enables Autorun to finally error properly in the proper format expected without anything seeming off. The detour handler now forwards these errors properly, so erroring inside of a detour functions as expected.
  • Adds an arbitrary sleep to the Windows autorun-lib entrypoint which fixes the "cant switch to single thread" popup.
  • Adds a few necessary functions to LuaApi, luaL_where, lua_concat and lua_remove

Example for the new error handling:

] lua_run_cl render.Capture()

[ERROR] FUCK OFF
  1. Capture - [C]:-1
   2. unknown - LuaCmd:1

Source:

detour = Autorun.detour(_G.render.Capture, function()
    _G.error("FUCK OFF")
end)

It is a bit cumbersome to need to use the global variant of error, but we can't use the typical error or else it will bypass the safe calling mechanism and violate new assumptions held by the hooks and such.

@yogwoggf yogwoggf self-assigned this Oct 31, 2025
@yogwoggf yogwoggf changed the base branch from NONE-function-authorization to master October 31, 2025 19:47
@yogwoggf yogwoggf mentioned this pull request Oct 31, 2025
@yogwoggf yogwoggf marked this pull request as draft November 1, 2025 17:37
@yogwoggf
Copy link
Collaborator Author

yogwoggf commented Nov 1, 2025

I've identified an unfortunate core problem with this:
image

By default, LuaJIT will attempt to resolve the name of the current function by traversing one frame below, and reading the Lua frame (if any)'s last call instruction to determine the slot it used, then looking up the slot's name.

We basically totally destroy this with our detour frames and whatnot, and marking as dummy frames doesn't work for this.

/* Deduce function name from caller of a frame. */
const char *lj_debug_funcname(lua_State *L, cTValue *frame, const char **name)
{
  cTValue *pframe;
  GCfunc *fn;
  BCPos pc;
  if (frame <= tvref(L->stack)+LJ_FR2)
    return NULL;
  if (frame_isvarg(frame))
    frame = frame_prevd(frame);
  pframe = frame_prev(frame);
  fn = frame_func(pframe);
  pc = debug_framepc(L, fn, frame);
  if (pc != NO_BCPOS) {
    GCproto *pt = funcproto(fn);
    const BCIns *ip = &proto_bc(pt)[check_exp(pc < pt->sizebc, pc)];
    MMS mm = bcmode_mm(bc_op(*ip));
    if (mm == MM_call) {
      BCReg slot = bc_a(*ip);
      if (bc_op(*ip) == BC_ITERC) slot -= 3;
      return lj_debug_slotname(pt, ip, slot, name);
    } else if (mm != MM__MAX) {
      *name = strdata(mmname_str(G(L), mm));
      return "metamethod";
    }
  }
  return NULL;
}

We can't do any tricks like resizing a frame either because it violates the typical delta frame assumptions LuaJIT makes and would likely result in a crash. The only promising thing, in my opinion, is to hook this function natively, walk the frame down to the next Lua frame, go one frame before that, and call this function again with the proper frame.

@yogwoggf yogwoggf mentioned this pull request Nov 3, 2025
This one is funny because it can also leak Autorun's presence in the stack.
It sucks that its a constant, but since I now force all the call frames to generate (including the closure wrapper), there's an extra two now.
@yogwoggf
Copy link
Collaborator Author

yogwoggf commented Nov 3, 2025

There were some problems with adjusting to the new call frames introduced by std/protections.lua. I don't like how its not too robust in the sense of its call setup, but I think it is fine for now. Anyways, it appears to be working now:

Here are some test results (method of test is some function that somehow lets you run arbitrary code in Autorun's context, the worst-case scenario):

Protected

] lua_openscript_cl test-autorun.lua
Running script test-autorun.lua...
-- Testing Autorun stack spoof --
[Autorun] Running code in Autorun context...
Hi! Running in autorun.
[+] debug.getlocal check
-- 1 --
-- 2 --
    [+] Local (for index): nil
    [+] Local (for limit): nil
    [+] Local (for step): nil
    [+] Local lvl: nil
    [+] Local i: nil
-- 3 --
-- 4 --
[+] debug.getinfo check
-- 1 --
["currentline"]    =    22
["func"]    =    function: 0x8088097684ef0cb2
["isvararg"]    =    false
["lastlinedefined"]    =    44
["linedefined"]    =    3
["namewhat"]    =    
["nparams"]    =    0
["nups"]    =    0
["short_src"]    =    lua/test-autorun.lua
["source"]    =    @lua/test-autorun.lua
["what"]    =    Lua
-- 2 --
["currentline"]    =    3
["func"]    =    function: 0x8088097684cd8e2a
["isvararg"]    =    true
["lastlinedefined"]    =    44
["linedefined"]    =    0
["namewhat"]    =    
["nparams"]    =    0
["nups"]    =    0
["short_src"]    =    lua/test-autorun.lua
["source"]    =    @lua/test-autorun.lua
["what"]    =    main
[+] debug.traceback check
stack traceback:
    lua/test-autorun.lua:31: in function <lua/test-autorun.lua:3>
    lua/test-autorun.lua:3: in main chunk
[+] debug.getfenv check
    [+] env 1 = table: 0x80880976eadf1d62 (is _G)
    [+] env 2 = table: 0x80880976eadf1d62 (is _G)
    [+] env 3 = table: 0x80880976eadf1d62 (is _G)
[Autorun] Finished running code in Autorun context.

Unprotected

] lua_openscript_cl test-autorun.lua
Running script test-autorun.lua...
-- Testing Autorun stack spoof --
[Autorun] Running code in Autorun context...
Hi! Running in autorun.
[+] debug.getlocal check
-- 1 --
-- 2 --
    [+] Local (for index): nil
    [+] Local (for limit): nil
    [+] Local (for step): nil
    [+] Local lvl: nil
    [+] Local i: nil
-- 3 --
    [+] Local cb: nil
-- 4 --
[+] debug.getinfo check
-- 1 --
["currentline"]    =    22
["func"]    =    function: 0x8088093eacef1efa
["isvararg"]    =    false
["lastlinedefined"]    =    44
["linedefined"]    =    3
["name"]    =    cb
["namewhat"]    =    local
["nparams"]    =    0
["nups"]    =    0
["short_src"]    =    lua/test-autorun.lua
["source"]    =    @lua/test-autorun.lua
["what"]    =    Lua
-- 2 --
["currentline"]    =    20
["func"]    =    function: 0x8088093e8bbdfdfa
["isvararg"]    =    false
["lastlinedefined"]    =    22
["linedefined"]    =    18
["name"]    =    RunInAutorun
["namewhat"]    =    global
["nparams"]    =    1
["nups"]    =    0
["short_src"]    =    [string "client/init.lua"]
["source"]    =    client/init.lua
["what"]    =    Lua
-- 3 --
["currentline"]    =    3
["func"]    =    function: 0x8088093eaceccaba
["isvararg"]    =    true
["lastlinedefined"]    =    44
["linedefined"]    =    0
["namewhat"]    =    
["nparams"]    =    0
["nups"]    =    0
["short_src"]    =    lua/test-autorun.lua
["source"]    =    @lua/test-autorun.lua
["what"]    =    main
[+] debug.traceback check
stack traceback:
    lua/test-autorun.lua:31: in function 'cb'
    [string "client/init.lua"]:20: in function 'RunInAutorun'
    lua/test-autorun.lua:3: in main chunk
[+] debug.getfenv check
    [+] env 1 = table: 0x8088093e8bab1d62 (is _G)
    [+] env 2 = table: 0x8088093e8bab1d62 (is _G)
    [+] env 3 = table: 0x8088093e8bbd0962
    [+] env 4 = table: 0x8088093e8bab1d62 (is _G)
[Autorun] Finished running code in Autorun context.

@yogwoggf yogwoggf changed the title NONE - Implement safeCall to spoof the stack #40 - Implement safeCall to spoof the stack Nov 3, 2025
@yogwoggf yogwoggf marked this pull request as ready for review November 3, 2025 20:53
@yogwoggf
Copy link
Collaborator Author

yogwoggf commented Nov 3, 2025

Ready for review. Also would be nice if you could test this branch @thevurv . You should be able to load a map like usual, and it shouldn't crash.. hopefully.

@yogwoggf yogwoggf requested a review from vurvdev November 3, 2025 20:53
@yogwoggf
Copy link
Collaborator Author

yogwoggf commented Nov 3, 2025

also do not merge yet cause it's still quite experimental but I requested a review to get rid of any major blockers

Now, we default to zero for any native errors. This is in line with GMod, which does the same and this is why native errors never have a source attached.
@yogwoggf yogwoggf marked this pull request as draft November 7, 2025 23:12
@yogwoggf
Copy link
Collaborator Author

yogwoggf commented Nov 7, 2025

Starting to notice crashes and general instability... keeping as draft for now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Safely throwing errors Stack level spoofing

2 participants