Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1239fc7
Add bytecode parsing/writing support, add test lua detour function fo…
yogwoggf Nov 12, 2025
2661d32
Merge remote-tracking branch 'origin/master' into 56-lua-detours
yogwoggf Nov 12, 2025
af471f0
Add upvalue replacement
yogwoggf Nov 12, 2025
e5b8339
Add trampoline module, which makes detouring work
yogwoggf Nov 12, 2025
3067645
Basic explanation
yogwoggf Nov 12, 2025
e6e1336
Fix register allocation
yogwoggf Nov 12, 2025
6a50ae1
Check for varags
yogwoggf Nov 12, 2025
b047543
Clean up code
yogwoggf Nov 12, 2025
e002389
Add WIP detour restoration
yogwoggf Nov 12, 2025
f084fd4
Remove extraneous state
yogwoggf Nov 12, 2025
f0aacf5
Replace proto's debug info with the target
yogwoggf Nov 12, 2025
e1507f6
Add preliminary varg handling
yogwoggf Nov 13, 2025
034c966
Cleanup code
yogwoggf Nov 13, 2025
fbdc361
Add comment
yogwoggf Nov 13, 2025
f32c871
Write initial implementation of function cloning
yogwoggf Nov 13, 2025
1b4407a
Fixup offsets and fix other issues
yogwoggf Nov 13, 2025
b3c02c8
Fix copying garbage data, function cloning somewhat working.
yogwoggf Nov 13, 2025
3c433e3
Change upvalue replacement to create a TValue pointer as opposed to o…
yogwoggf Nov 13, 2025
7e978b2
Add deep upvalue cloning
yogwoggf Nov 13, 2025
fe9a3c6
Fix a major bug causing tons of problems with upvalue clones
yogwoggf Nov 14, 2025
ca1de7f
Clone upvalue before replacing, instead of writing to the upvalue in-…
yogwoggf Nov 14, 2025
903afe9
Update dhash
yogwoggf Nov 14, 2025
03f450b
Add an experimental method of detouring upvalue-less functions by for…
yogwoggf Nov 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/autorun-env/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ impl EnvHandle {
lua.set(state, &t, "triggerRemote", wrap!(functions::trigger_remote));
lua.set(state, &t, "isFunctionAuthorized", wrap!(functions::is_function_authorized));
lua.set(state, &t, "isProtoAuthorized", wrap!(functions::is_proto_authorized));
lua.set(state, &t, "detourLua", wrap!(functions::detour_lua));
lua.set(state, &t, "restoreLua", wrap!(functions::restore_lua));
lua.set(state, &t, "cloneFunction", wrap!(functions::clone_lua_function));
lua.set(state, &t, "VERSION", env!("CARGO_PKG_VERSION"));

return t;
Expand Down
96 changes: 95 additions & 1 deletion packages/autorun-env/src/functions/detour.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
mod handlers;
mod lua;
mod raw;
mod userdata;

use crate::functions::detour::handlers::{detour_handler, retour_handler};
use crate::functions::detour::lua::state::OriginalDetourState;
use crate::functions::detour::raw::{make_detour_trampoline, make_retour_lua_trampoline};
use crate::functions::detour::userdata::Detour;
use anyhow::Context;
use autorun_lua::{LuaApi, LuaCFunction, LuaTypeId, RawHandle, RawLuaReturn};
use autorun_luajit::{GCfunc, LJState, get_gcobj, get_gcobj_mut};
use autorun_luajit::bytecode::{BCWriter, Op};
use autorun_luajit::{BCIns, GCfunc, GCfuncL, LJState, get_gcobj, get_gcobj_mut, get_gcobj_ptr, index2adr};
use autorun_types::LuaState;
use retour::GenericDetour;
use std::ffi::c_int;
Expand Down Expand Up @@ -118,3 +121,94 @@ pub fn copy_fast_function(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHa

Ok(RawLuaReturn(1))
}

pub fn detour_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result<RawLuaReturn> {
if lua.raw.typeid(state, 1) != LuaTypeId::Function {
anyhow::bail!("First argument must be a function.");
}

// get function
let lj_state = state as *mut LJState;
let lj_state = unsafe { lj_state.as_mut().context("Failed to dereference LJState.")? };
let gcfunc = get_gcobj::<GCfunc>(lj_state, 1).context("Failed to get GCfunc for target function.")?;
let gcfunc_ptr = get_gcobj_ptr::<GCfunc>(lj_state, 1).context("Failed to get GCfunc pointer for target function.")?;
autorun_log::debug!("Detouring Lua function at {:p}", gcfunc_ptr);
let gcfunc_l_ptr = unsafe { std::mem::transmute::<*mut GCfunc, *mut GCfuncL>(gcfunc_ptr) };

let gcfunc_l = gcfunc.as_l().context("Target function must be a Lua function.")?;
let proto = gcfunc_l.get_proto()?;
let proto = unsafe { proto.as_mut().context("Failed to get proto for target function.")? };

let replacement_tv = unsafe {
index2adr(lj_state, 2)
.context("Failed to get TValue for replacement upvalue.")?
.read()
};

let detour_gcfunc = get_gcobj::<GCfunc>(lj_state, 2).context("Failed to get GCfunc for detour function.")?;
let detour_gcfunc_l = detour_gcfunc.as_l().context("Detour function must be a Lua function.")?;
let detour_proto = detour_gcfunc_l.get_proto()?;
let detour_proto = unsafe { detour_proto.as_mut().context("Failed to get proto for detour function.")? };

// Copy over debug information from detour to target
detour_proto.chunkname = proto.chunkname;
detour_proto.firstline = proto.firstline;
detour_proto.lineinfo = proto.lineinfo;
detour_proto.uvinfo = proto.uvinfo;
detour_proto.varinfo = proto.varinfo;

// Another thing, now this is technically UB. But, LuaJIT relies on the uvptr being whats known as a flexible array member.
// It is not allocated if there are no upvalues, but with padding and the like, we can just use it as normal.

if proto.sizeuv == 0 {
proto.sizeuv = 1;
unsafe {
(*gcfunc_l_ptr).header.nupvalues = 1;
}
}

let gcfunc_l = gcfunc.as_l().context("Must be a Lua function.")?;
autorun_log::debug!("Patching upvalue...");
let mut original_detour_state = OriginalDetourState::new();
lua::upvalue::replace(lj_state, gcfunc_l_ptr, 0, replacement_tv, &mut original_detour_state)?;
lua::trampoline::overwrite_with_trampoline(gcfunc_l, &mut original_detour_state)?;

let original_function_ptr = index2adr(lj_state, 1).context("Failed to get TValue for target function.")?;
let original_function_ptr =
unsafe { (*original_function_ptr).as_ptr::<GCfunc>() }.context("Failed to get GCfunc pointer.")?;

lua::state::save_state(original_function_ptr, original_detour_state);

Ok(RawLuaReturn(0))
}

pub fn restore_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result<RawLuaReturn> {
if lua.raw.typeid(state, 1) != LuaTypeId::Function {
anyhow::bail!("First argument must be a function.");
}

// get function
let lj_state = state as *mut LJState;
let lj_state = unsafe { lj_state.as_mut().context("Failed to dereference LJState.")? };
let original_function_ptr = index2adr(lj_state, 1).context("Failed to get TValue for target function.")?;
let original_function_ptr =
unsafe { (*original_function_ptr).as_ptr::<GCfunc>() }.context("Failed to get GCfunc pointer.")?;

lua::state::restore_func(original_function_ptr)?;

Ok(RawLuaReturn(0))
}

pub fn clone_lua_function(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result<RawLuaReturn> {
if lua.raw.typeid(state, 1) != LuaTypeId::Function {
anyhow::bail!("First argument must be a function.");
}

let lj_state = state as *mut LJState;
let lj_state = unsafe { lj_state.as_mut().context("Failed to dereference LJState.")? };

let gcfunc = get_gcobj_ptr::<GCfunc>(lj_state, 1).context("Failed to get GCfunc for target function.")?;
let gcfunc_l = unsafe { std::mem::transmute::<*mut GCfunc, *mut GCfuncL>(gcfunc) };
lua::clone(lj_state, gcfunc_l)?;
Ok(RawLuaReturn(1))
}
6 changes: 6 additions & 0 deletions packages/autorun-env/src/functions/detour/lua.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod clone;
pub mod state;
pub mod trampoline;
pub mod upvalue;

pub use clone::clone;
180 changes: 180 additions & 0 deletions packages/autorun-env/src/functions/detour/lua/clone.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//! Provides advanced cloning functionality for Lua functions.
//! This is an ultimate deep clone that basically duplicates everything about a Lua function,
//! including its upvalues, bytecode, and other internal structures.

use anyhow::Context;
use autorun_luajit::{
BCIns, GCHeader, GCProto, GCRef, GCSize, GCUpval, GCfunc, GCfuncL, LJState, TValue, mem_newgco, push_tvalue,
};
use std::mem::offset_of;

/// Clones the given Lua function deeply, duplicating its internal structures.
/// Pushes the cloned function onto the Lua stack.
pub fn clone(lj_state: &mut LJState, target_func: *mut GCfuncL) -> anyhow::Result<()> {
dbg!(&target_func);
// Proto must be cloned first.
let proto = unsafe { (*target_func).get_proto()? };
let proto_size = unsafe { (*proto).sizept } as GCSize;
let proto_uv_size = unsafe { (*proto).sizeuv } as GCSize;

dbg!(unsafe { &(*proto) });
dbg!(&proto_size);
dbg!(&proto_uv_size);
// What we want to do is allocate a new proto and copy over everything, but
// keep the GCHeader intact or else the GC system will get super confused.
let new_proto_ptr = unsafe { mem_newgco::<GCProto>(lj_state, proto_size)? };
unsafe {
// Treat every pointer as raw bytes, since sizept is specified as bytes.
std::ptr::copy_nonoverlapping(
(proto as *const u8).byte_add(size_of::<GCHeader>()),
(new_proto_ptr as *mut u8).byte_add(size_of::<GCHeader>()),
proto_size as usize - size_of::<GCHeader>(),
);
};

dbg!("Fixing up proto offsets...");

// Fix up internal offsets within the proto
fixup_proto_offsets(proto, new_proto_ptr)?;

// Now create the new function, we'll keep everything about it intact, of course except for the GCHeader like
// the proto.

// Lua functions store their upvalue array in a contiguous block after the main struct
let func_size =
size_of::<GCfuncL>() as GCSize - size_of::<GCRef>() as GCSize + size_of::<GCRef>() as GCSize * proto_uv_size;
let new_func_ptr = unsafe { mem_newgco::<GCfunc>(lj_state, func_size)? };

unsafe {
let target_func = target_func as *const GCfuncL as *const u8;

std::ptr::copy_nonoverlapping(
target_func.byte_add(size_of::<GCHeader>()),
(new_func_ptr as *mut u8).byte_add(size_of::<GCHeader>()),
func_size as usize - size_of::<GCHeader>(),
);
};

// Fix pc pointer to point to the new proto's bytecode
unsafe {
// Bytecode is located immediately after the GCProto struct
let bc_ptr = new_proto_ptr.byte_add(size_of::<GCProto>()) as *mut BCIns;
(*new_func_ptr).header_mut().pc.set_ptr(bc_ptr);
}

clone_upvalue_list(lj_state, new_func_ptr)?;

dbg!(unsafe { &(*new_func_ptr).l });
// Create a TValue for the new function and push it onto the stack
let func_tvalue = TValue::from_ptr(new_func_ptr);
push_tvalue(lj_state, &func_tvalue);

Ok(())
}

pub fn fixup_proto_offsets(original_proto: *mut GCProto, new_proto: *mut GCProto) -> anyhow::Result<()> {
// Basically, the proto contains several offsets that point to various internal structures within its own allocation.
// Technically speaking, we can hardcode these, but it would be better to read them from the original proto and adjust them accordingly.

let original_base = original_proto as usize;
let new_base = new_proto as usize;

let k_offset = unsafe { (*original_proto).k.ptr64 as usize - original_base };
let uv_offset = unsafe { (*original_proto).uv.ptr64 as usize - original_base };
let lineinfo_offset = unsafe { (*original_proto).lineinfo.ptr64 as usize - original_base };
let uvinfo_offset = unsafe { (*original_proto).uvinfo.ptr64 as usize - original_base };
let varinfo_offset = unsafe { (*original_proto).varinfo.ptr64 as usize - original_base };

// apply offsets to new proto
unsafe {
(*new_proto).k.ptr64 = (new_base + k_offset) as u64;
(*new_proto).uv.ptr64 = (new_base + uv_offset) as u64;
(*new_proto).lineinfo.ptr64 = (new_base + lineinfo_offset) as u64;
(*new_proto).uvinfo.ptr64 = (new_base + uvinfo_offset) as u64;
(*new_proto).varinfo.ptr64 = (new_base + varinfo_offset) as u64;
}

Ok(())
}

pub fn clone_upvalue_list(lj_state: &mut LJState, func: *mut GCfunc) -> anyhow::Result<()> {
// The function stores a contiguous array of upvalue **references** after the main struct.
// We need to go through each upvalue reference and clone the actual upvalue objects they point to.

let gcfunc = unsafe { func.as_mut().context("Failed to dereference GCfunc.")? };
let gcfunc_l = gcfunc.as_l_mut().context("Function is not a Lua function.")?;
let nupvalues = gcfunc_l.header.nupvalues;
autorun_log::debug!("Cloning {} upvalues...", nupvalues);
unsafe {
autorun_log::debug!("uvptr: {:p}", (gcfunc_l as *mut GCfuncL).byte_add(offset_of!(GCfuncL, uvptr)));
autorun_log::debug!("uvptr offset: {}", offset_of!(GCfuncL, uvptr));
}

for i in 0..nupvalues {
autorun_log::debug!("Cloning upvalue {}...", i);
let upvalue_gcr = unsafe { gcfunc_l.uvptr.as_mut_ptr().add(i as usize) };
let upvalue_gcr = unsafe { upvalue_gcr.as_mut().context("Failed to deref upvalue GCRef.")? };

let upvalue = unsafe { upvalue_gcr.as_direct_ptr::<GCUpval>() };
if upvalue.is_null() {
anyhow::bail!("Upvalue pointer is null.");
}

let new_upvalue_ptr = unsafe { mem_newgco::<GCUpval>(lj_state, size_of::<GCUpval>() as GCSize)? };

let expected_address = (gcfunc_l as *const GCfuncL as usize) + 0x28usize + ((i as usize) * 8);

autorun_log::debug!(
"Upvalue {}: reading from {:p}, expected 0x{:x}",
i,
upvalue_gcr as *mut GCRef,
expected_address,
);

autorun_log::debug!("Original GCR pointer: {:p}", upvalue_gcr.gcptr64 as *const ());
autorun_log::debug!("Original upvalue pointer: {:p}", upvalue);
autorun_log::debug!("New upvalue pointer: {:p}", new_upvalue_ptr);

dbg!(&unsafe { &*upvalue });
// copy excluding GCHeader
unsafe {
std::ptr::copy_nonoverlapping(
(upvalue as *const u8).byte_add(size_of::<GCHeader>()),
(new_upvalue_ptr as *mut u8).byte_add(size_of::<GCHeader>()),
size_of::<GCUpval>() - size_of::<GCHeader>(),
);
}

// Close it out
close_upvalue(new_upvalue_ptr, None)?;

// update the reference to point to the new upvalue
upvalue_gcr.set_ptr(new_upvalue_ptr);
autorun_log::debug!("Upvalue {} cloned.", i);
}

Ok(())
}

pub fn close_upvalue(new_upvalue_ptr: *mut GCUpval, replacement_value: Option<TValue>) -> anyhow::Result<()> {
unsafe {
let new_upvalue = new_upvalue_ptr.as_mut().context("Failed to deref new upvalue.")?;
dbg!(&new_upvalue);
new_upvalue.closed = 1;

// copy TV from wherever it was pointing to
autorun_log::debug!("Reading original TV pointer: {:p}", new_upvalue.v.as_ptr::<TValue>());
new_upvalue.uv.tv = if let Some(replacement) = replacement_value {
replacement
} else {
new_upvalue.v.as_ptr::<TValue>().read()
};

// point v to the new TV
new_upvalue
.v
.set_ptr(new_upvalue_ptr.byte_add(offset_of!(GCUpval, uv)) as *mut TValue);
};

Ok(())
}
70 changes: 70 additions & 0 deletions packages/autorun-env/src/functions/detour/lua/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! Saves the original state of a detoured function before it was applied.

use crate::functions::detour::lua::upvalue::overwrite_upvalue;
use anyhow::Context;
use autorun_luajit::{BCIns, GCfunc, TValue};
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};

#[derive(Clone)]
pub struct OriginalDetourState {
pub original_bytecode: Vec<BCIns>,
pub original_frame_size: u8,
pub original_upvalue_0: TValue,
}

pub static ORIGINAL_DETOUR_STATES: LazyLock<Mutex<HashMap<usize, OriginalDetourState>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));

impl OriginalDetourState {
pub fn new() -> Self {
Self {
original_bytecode: Vec::new(),
original_frame_size: 0,
original_upvalue_0: TValue::nil(),
}
}
}

/// Associates the given function with its original detour state.
pub fn save_state(func: *mut GCfunc, state: OriginalDetourState) {
ORIGINAL_DETOUR_STATES.lock().unwrap().insert(func as usize, state);
}

/// Retrieves the original detour state for the given function, if it exists.
pub fn get_state(func: *mut GCfunc) -> Option<OriginalDetourState> {
ORIGINAL_DETOUR_STATES.lock().unwrap().get(&(func as usize)).cloned()
}

pub fn restore_func(func: *mut GCfunc) -> anyhow::Result<()> {
let state = get_state(func).context("Failed to find original detour state for function.")?;
let gcfunc_l = unsafe { (*func).as_l().context("Function is not a Lua function.")? };

// Fix upvalues
autorun_log::debug!("Fixing upvalues...");
//overwrite_upvalue(gcfunc_l, 0, state.original_upvalue_0)?;
autorun_log::debug!("Upvalues fixed.");

// Restore bytecode and frame size
autorun_log::debug!("Restoring bytecode...");

// We don't want to instantiate a writer do it one by one, we can just copy directly
let original_bytecode = state.original_bytecode;
let bc_ptr = gcfunc_l.get_bc_ins().context("Failed to get bytecode pointer.")?;
let proto = gcfunc_l.get_proto().context("Failed to get proto.")?;
let proto = unsafe { proto.as_mut().context("Failed to dereference proto.")? };
unsafe {
std::ptr::copy_nonoverlapping(original_bytecode.as_ptr(), bc_ptr, original_bytecode.len());
}

autorun_log::debug!("Bytecode restored.");

autorun_log::debug!("Restoring frame size...");
proto.framesize = state.original_frame_size;
autorun_log::debug!("Frame size restored.");

// We no longer need to keep the state
ORIGINAL_DETOUR_STATES.lock().unwrap().remove(&(func as usize));

Ok(())
}
Loading