From 1239fc71880df7442dc635bd1ee7bfa77e6041b4 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:14:58 -0800 Subject: [PATCH 01/22] Add bytecode parsing/writing support, add test lua detour function for now --- packages/autorun-env/src/env.rs | 1 + packages/autorun-env/src/functions/detour.rs | 32 +- packages/autorun-luajit/src/bytecode.rs | 363 ++++++++++++++++++ .../autorun-luajit/src/bytecode/writer.rs | 68 ++++ packages/autorun-luajit/src/lib.rs | 1 + packages/autorun-luajit/src/types/common.rs | 87 ++++- 6 files changed, 540 insertions(+), 12 deletions(-) create mode 100644 packages/autorun-luajit/src/bytecode.rs create mode 100644 packages/autorun-luajit/src/bytecode/writer.rs diff --git a/packages/autorun-env/src/env.rs b/packages/autorun-env/src/env.rs index 1f630dc..3e3d83a 100644 --- a/packages/autorun-env/src/env.rs +++ b/packages/autorun-env/src/env.rs @@ -108,6 +108,7 @@ 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, "testLua", wrap!(functions::test_lua)); lua.set(state, &t, "VERSION", env!("CARGO_PKG_VERSION")); return t; diff --git a/packages/autorun-env/src/functions/detour.rs b/packages/autorun-env/src/functions/detour.rs index 36417c9..e59d3e5 100644 --- a/packages/autorun-env/src/functions/detour.rs +++ b/packages/autorun-env/src/functions/detour.rs @@ -7,7 +7,8 @@ use crate::functions::detour::raw::{make_detour_trampoline, make_retour_lua_tram 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, LJState, get_gcobj, get_gcobj_mut}; use autorun_types::LuaState; use retour::GenericDetour; use std::ffi::c_int; @@ -118,3 +119,32 @@ pub fn copy_fast_function(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHa Ok(RawLuaReturn(1)) } + +pub fn test_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result { + 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::(lj_state, 1).context("Failed to get GCfunc for target function.")?; + + let gcfunc_l = gcfunc.as_l().context("Must be a Lua function.")?; + let proto = unsafe { gcfunc_l.get_proto()?.as_mut() }.context("Failed to get prototype.")?; + + if proto.sizebc < 3 { + autorun_log::debug!("Bytecode instruction count: {}, cannot patch test function.", proto.sizebc); + anyhow::bail!("Not enough bytecode instructions to patch."); + } + + let mut bc_writer = BCWriter::from_gcfunc_l(gcfunc_l)?; + bc_writer.set_offset(1)?; // skip after the FUNCF opcode + let old_ins = bc_writer.replace(BCIns::from_ad(Op::KSHORT, 0, 1337))?; // KSHORT + let old_ins2 = bc_writer.replace(BCIns::from_ad(Op::RET1, 0, 2))?; // RET1 + + autorun_log::debug!("Patched bytecode instructions."); + autorun_log::debug!("Old instructions: \n{:#?}\n{:#?}", old_ins, old_ins2); + + Ok(RawLuaReturn(0)) +} diff --git a/packages/autorun-luajit/src/bytecode.rs b/packages/autorun-luajit/src/bytecode.rs new file mode 100644 index 0000000..ed5f7ee --- /dev/null +++ b/packages/autorun-luajit/src/bytecode.rs @@ -0,0 +1,363 @@ +mod writer; + +/** +/* Bytecode instruction definition. Order matters, see below. +** +** (name, filler, Amode, Bmode, Cmode or Dmode, metamethod) +** +** The opcode name suffixes specify the type for RB/RC or RD: +** V = variable slot +** S = string const +** N = number const +** P = primitive type (~itype) +** B = unsigned byte literal +** M = multiple args/results +*/ +#define BCDEF(_) \ + /* Comparison ops. ORDER OPR. */ \ + _(ISLT, var, ___, var, lt) \ + _(ISGE, var, ___, var, lt) \ + _(ISLE, var, ___, var, le) \ + _(ISGT, var, ___, var, le) \ + \ + _(ISEQV, var, ___, var, eq) \ + _(ISNEV, var, ___, var, eq) \ + _(ISEQS, var, ___, str, eq) \ + _(ISNES, var, ___, str, eq) \ + _(ISEQN, var, ___, num, eq) \ + _(ISNEN, var, ___, num, eq) \ + _(ISEQP, var, ___, pri, eq) \ + _(ISNEP, var, ___, pri, eq) \ + \ + /* Unary test and copy ops. */ \ + _(ISTC, dst, ___, var, ___) \ + _(ISFC, dst, ___, var, ___) \ + _(IST, ___, ___, var, ___) \ + _(ISF, ___, ___, var, ___) \ + _(ISTYPE, var, ___, lit, ___) \ + _(ISNUM, var, ___, lit, ___) \ + \ + /* Unary ops. */ \ + _(MOV, dst, ___, var, ___) \ + _(NOT, dst, ___, var, ___) \ + _(UNM, dst, ___, var, unm) \ + _(LEN, dst, ___, var, len) \ + \ + /* Binary ops. ORDER OPR. VV last, POW must be next. */ \ + _(ADDVN, dst, var, num, add) \ + _(SUBVN, dst, var, num, sub) \ + _(MULVN, dst, var, num, mul) \ + _(DIVVN, dst, var, num, div) \ + _(MODVN, dst, var, num, mod) \ + \ + _(ADDNV, dst, var, num, add) \ + _(SUBNV, dst, var, num, sub) \ + _(MULNV, dst, var, num, mul) \ + _(DIVNV, dst, var, num, div) \ + _(MODNV, dst, var, num, mod) \ + \ + _(ADDVV, dst, var, var, add) \ + _(SUBVV, dst, var, var, sub) \ + _(MULVV, dst, var, var, mul) \ + _(DIVVV, dst, var, var, div) \ + _(MODVV, dst, var, var, mod) \ + \ + _(POW, dst, var, var, pow) \ + _(CAT, dst, rbase, rbase, concat) \ + \ + /* Constant ops. */ \ + _(KSTR, dst, ___, str, ___) \ + _(KCDATA, dst, ___, cdata, ___) \ + _(KSHORT, dst, ___, lits, ___) \ + _(KNUM, dst, ___, num, ___) \ + _(KPRI, dst, ___, pri, ___) \ + _(KNIL, base, ___, base, ___) \ + \ + /* Upvalue and function ops. */ \ + _(UGET, dst, ___, uv, ___) \ + _(USETV, uv, ___, var, ___) \ + _(USETS, uv, ___, str, ___) \ + _(USETN, uv, ___, num, ___) \ + _(USETP, uv, ___, pri, ___) \ + _(UCLO, rbase, ___, jump, ___) \ + _(FNEW, dst, ___, func, gc) \ + \ + /* Table ops. */ \ + _(TNEW, dst, ___, lit, gc) \ + _(TDUP, dst, ___, tab, gc) \ + _(GGET, dst, ___, str, index) \ + _(GSET, var, ___, str, newindex) \ + _(TGETV, dst, var, var, index) \ + _(TGETS, dst, var, str, index) \ + _(TGETB, dst, var, lit, index) \ + _(TGETR, dst, var, var, index) \ + _(TSETV, var, var, var, newindex) \ + _(TSETS, var, var, str, newindex) \ + _(TSETB, var, var, lit, newindex) \ + _(TSETM, base, ___, num, newindex) \ + _(TSETR, var, var, var, newindex) \ + \ + /* Calls and vararg handling. T = tail call. */ \ + _(CALLM, base, lit, lit, call) \ + _(CALL, base, lit, lit, call) \ + _(CALLMT, base, ___, lit, call) \ + _(CALLT, base, ___, lit, call) \ + _(ITERC, base, lit, lit, call) \ + _(ITERN, base, lit, lit, call) \ + _(VARG, base, lit, lit, ___) \ + _(ISNEXT, base, ___, jump, ___) \ + \ + /* Returns. */ \ + _(RETM, base, ___, lit, ___) \ + _(RET, rbase, ___, lit, ___) \ + _(RET0, rbase, ___, lit, ___) \ + _(RET1, rbase, ___, lit, ___) \ + \ + /* Loops and branches. I/J = interp/JIT, I/C/L = init/call/loop. */ \ + _(FORI, base, ___, jump, ___) \ + _(JFORI, base, ___, jump, ___) \ + \ + _(FORL, base, ___, jump, ___) \ + _(IFORL, base, ___, jump, ___) \ + _(JFORL, base, ___, lit, ___) \ + \ + _(ITERL, base, ___, jump, ___) \ + _(IITERL, base, ___, jump, ___) \ + _(JITERL, base, ___, lit, ___) \ + \ + _(LOOP, rbase, ___, jump, ___) \ + _(ILOOP, rbase, ___, jump, ___) \ + _(JLOOP, rbase, ___, lit, ___) \ + \ + _(JMP, rbase, ___, jump, ___) \ + \ + /* Function headers. I/J = interp/JIT, F/V/C = fixarg/vararg/C func. */ \ + _(FUNCF, rbase, ___, ___, ___) \ + _(IFUNCF, rbase, ___, ___, ___) \ + _(JFUNCF, rbase, ___, lit, ___) \ + _(FUNCV, rbase, ___, ___, ___) \ + _(IFUNCV, rbase, ___, ___, ___) \ + _(JFUNCV, rbase, ___, lit, ___) \ + _(FUNCC, rbase, ___, ___, ___) \ + _(FUNCCW, rbase, ___, ___, ___) + +/* Bytecode opcode numbers. */ +typedef enum { +#define BCENUM(name, ma, mb, mc, mt) BC_##name, +BCDEF(BCENUM) +#undef BCENUM + BC__MAX +} BCOp; +*/ + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Op { + ISLT, + ISGE, + ISLE, + ISGT, + ISEQV, + ISNEV, + ISEQS, + ISNES, + ISEQN, + ISNEN, + ISEQP, + ISNEP, + ISTC, + ISFC, + IST, + ISF, + ISTYPE, + ISNUM, + MOV, + NOT, + UNM, + LEN, + ADDVN, + SUBVN, + MULVN, + DIVVN, + MODVN, + ADDNV, + SUBNV, + MULNV, + DIVNV, + MODNV, + ADDVV, + SUBVV, + MULVV, + DIVVV, + MODVV, + POW, + CAT, + KSTR, + KCDATA, + KSHORT, + KNUM, + KPRI, + KNIL, + UGET, + USETV, + USETS, + USETN, + USETP, + UCLO, + FNEW, + TNEW, + TDUP, + GGET, + GSET, + TGETV, + TGETS, + TGETB, + TGETR, + TSETV, + TSETS, + TSETB, + TSETM, + TSETR, + CALLM, + CALL, + CALLMT, + CALLT, + ITERC, + ITERN, + VARG, + ISNEXT, + RETM, + RET, + RET0, + RET1, + FORI, + JFORI, + FORL, + IFORL, + JFORL, + ITERL, + IITERL, + JITERL, + LOOP, + ILOOP, + JLOOP, + JMP, + FUNCF, + IFUNCF, + JFUNCF, + FUNCV, + IFUNCV, + JFUNCV, + FUNCC, + FUNCCW, + MAX, +} + +impl TryFrom for Op { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + x if x == Op::ISLT as u8 => Ok(Op::ISLT), + x if x == Op::ISGE as u8 => Ok(Op::ISGE), + x if x == Op::ISLE as u8 => Ok(Op::ISLE), + x if x == Op::ISGT as u8 => Ok(Op::ISGT), + x if x == Op::ISEQV as u8 => Ok(Op::ISEQV), + x if x == Op::ISNEV as u8 => Ok(Op::ISNEV), + x if x == Op::ISEQS as u8 => Ok(Op::ISEQS), + x if x == Op::ISNES as u8 => Ok(Op::ISNES), + x if x == Op::ISEQN as u8 => Ok(Op::ISEQN), + x if x == Op::ISNEN as u8 => Ok(Op::ISNEN), + x if x == Op::ISEQP as u8 => Ok(Op::ISEQP), + x if x == Op::ISNEP as u8 => Ok(Op::ISNEP), + x if x == Op::ISTC as u8 => Ok(Op::ISTC), + x if x == Op::ISFC as u8 => Ok(Op::ISFC), + x if x == Op::IST as u8 => Ok(Op::IST), + x if x == Op::ISF as u8 => Ok(Op::ISF), + x if x == Op::ISTYPE as u8 => Ok(Op::ISTYPE), + x if x == Op::ISNUM as u8 => Ok(Op::ISNUM), + x if x == Op::MOV as u8 => Ok(Op::MOV), + x if x == Op::NOT as u8 => Ok(Op::NOT), + x if x == Op::UNM as u8 => Ok(Op::UNM), + x if x == Op::LEN as u8 => Ok(Op::LEN), + x if x == Op::ADDVN as u8 => Ok(Op::ADDVN), + x if x == Op::SUBVN as u8 => Ok(Op::SUBVN), + x if x == Op::MULVN as u8 => Ok(Op::MULVN), + x if x == Op::DIVVN as u8 => Ok(Op::DIVVN), + x if x == Op::MODVN as u8 => Ok(Op::MODVN), + x if x == Op::ADDNV as u8 => Ok(Op::ADDNV), + x if x == Op::SUBNV as u8 => Ok(Op::SUBNV), + x if x == Op::MULNV as u8 => Ok(Op::MULNV), + x if x == Op::DIVNV as u8 => Ok(Op::DIVNV), + x if x == Op::MODNV as u8 => Ok(Op::MODNV), + x if x == Op::ADDVV as u8 => Ok(Op::ADDVV), + x if x == Op::SUBVV as u8 => Ok(Op::SUBVV), + x if x == Op::MULVV as u8 => Ok(Op::MULVV), + x if x == Op::DIVVV as u8 => Ok(Op::DIVVV), + x if x == Op::MODVV as u8 => Ok(Op::MODVV), + x if x == Op::POW as u8 => Ok(Op::POW), + x if x == Op::CAT as u8 => Ok(Op::CAT), + x if x == Op::KSTR as u8 => Ok(Op::KSTR), + x if x == Op::KCDATA as u8 => Ok(Op::KCDATA), + x if x == Op::KSHORT as u8 => Ok(Op::KSHORT), + x if x == Op::KNUM as u8 => Ok(Op::KNUM), + x if x == Op::KPRI as u8 => Ok(Op::KPRI), + x if x == Op::KNIL as u8 => Ok(Op::KNIL), + x if x == Op::UGET as u8 => Ok(Op::UGET), + x if x == Op::USETV as u8 => Ok(Op::USETV), + x if x == Op::USETS as u8 => Ok(Op::USETS), + x if x == Op::USETN as u8 => Ok(Op::USETN), + x if x == Op::USETP as u8 => Ok(Op::USETP), + x if x == Op::UCLO as u8 => Ok(Op::UCLO), + x if x == Op::FNEW as u8 => Ok(Op::FNEW), + x if x == Op::TNEW as u8 => Ok(Op::TNEW), + x if x == Op::TDUP as u8 => Ok(Op::TDUP), + x if x == Op::GGET as u8 => Ok(Op::GGET), + x if x == Op::GSET as u8 => Ok(Op::GSET), + x if x == Op::TGETV as u8 => Ok(Op::TGETV), + x if x == Op::TGETS as u8 => Ok(Op::TGETS), + x if x == Op::TGETB as u8 => Ok(Op::TGETB), + x if x == Op::TGETR as u8 => Ok(Op::TGETR), + x if x == Op::TSETV as u8 => Ok(Op::TSETV), + x if x == Op::TSETS as u8 => Ok(Op::TSETS), + x if x == Op::TSETB as u8 => Ok(Op::TSETB), + x if x == Op::TSETM as u8 => Ok(Op::TSETM), + x if x == Op::TSETR as u8 => Ok(Op::TSETR), + x if x == Op::CALLM as u8 => Ok(Op::CALLM), + x if x == Op::CALL as u8 => Ok(Op::CALL), + x if x == Op::CALLMT as u8 => Ok(Op::CALLMT), + x if x == Op::CALLT as u8 => Ok(Op::CALLT), + x if x == Op::ITERC as u8 => Ok(Op::ITERC), + x if x == Op::ITERN as u8 => Ok(Op::ITERN), + x if x == Op::VARG as u8 => Ok(Op::VARG), + x if x == Op::ISNEXT as u8 => Ok(Op::ISNEXT), + x if x == Op::RETM as u8 => Ok(Op::RETM), + x if x == Op::RET as u8 => Ok(Op::RET), + x if x == Op::RET0 as u8 => Ok(Op::RET0), + x if x == Op::RET1 as u8 => Ok(Op::RET1), + x if x == Op::FORI as u8 => Ok(Op::FORI), + x if x == Op::JFORI as u8 => Ok(Op::JFORI), + x if x == Op::FORL as u8 => Ok(Op::FORL), + x if x == Op::IFORL as u8 => Ok(Op::IFORL), + x if x == Op::JFORL as u8 => Ok(Op::JFORL), + x if x == Op::ITERL as u8 => Ok(Op::ITERL), + x if x == Op::IITERL as u8 => Ok(Op::IITERL), + x if x == Op::JITERL as u8 => Ok(Op::JITERL), + x if x == Op::LOOP as u8 => Ok(Op::LOOP), + x if x == Op::ILOOP as u8 => Ok(Op::ILOOP), + x if x == Op::JLOOP as u8 => Ok(Op::JLOOP), + x if x == Op::JMP as u8 => Ok(Op::JMP), + x if x == Op::FUNCF as u8 => Ok(Op::FUNCF), + x if x == Op::IFUNCF as u8 => Ok(Op::IFUNCF), + x if x == Op::JFUNCF as u8 => Ok(Op::JFUNCF), + x if x == Op::FUNCV as u8 => Ok(Op::FUNCV), + x if x == Op::IFUNCV as u8 => Ok(Op::IFUNCV), + x if x == Op::JFUNCV as u8 => Ok(Op::JFUNCV), + x if x == Op::FUNCC as u8 => Ok(Op::FUNCC), + x if x == Op::FUNCCW as u8 => Ok(Op::FUNCCW), + _ => Err(()), + } + } +} + +pub use writer::*; diff --git a/packages/autorun-luajit/src/bytecode/writer.rs b/packages/autorun-luajit/src/bytecode/writer.rs new file mode 100644 index 0000000..402116f --- /dev/null +++ b/packages/autorun-luajit/src/bytecode/writer.rs @@ -0,0 +1,68 @@ +use crate::{BCIns, GCfuncL}; +use anyhow::Context; + +pub struct BCWriter { + ptr: *mut BCIns, + size: usize, + offset: usize, +} + +impl BCWriter { + pub fn from_bc_ptr(ptr: *mut BCIns, size: usize) -> Self { + Self { ptr, size, offset: 0 } + } + + pub fn from_gcfunc_l(gcfunc: &GCfuncL) -> anyhow::Result { + let proto = gcfunc.get_proto().context("Failed to get proto from GCfuncL")?; + let proto = unsafe { proto.as_mut().context("Failed to dereference proto")? }; + let bc_ptr = gcfunc.get_bc_ins()?; + + Ok(Self::from_bc_ptr(bc_ptr, proto.sizebc as usize)) + } + + pub fn reset(&mut self) { + self.offset = 0; + } + + pub fn set_offset(&mut self, offset: usize) -> anyhow::Result<()> { + if offset >= self.size { + anyhow::bail!( + "Bytecode writer set_offset out of bounds: offset {} exceeds size {}.", + offset, + self.size + ); + } + + self.offset = offset; + Ok(()) + } + + pub fn write(&mut self, instruction: BCIns) -> anyhow::Result<()> { + if self.offset >= self.size { + anyhow::bail!("Bytecode writer overflow: attempted to write beyond allocated size."); + } + + unsafe { + std::ptr::write(self.ptr.add(self.offset), instruction); + } + + self.offset += 1; + Ok(()) + } + + pub fn replace(&mut self, instruction: BCIns) -> anyhow::Result { + if self.offset >= self.size { + anyhow::bail!("Bytecode writer replace out of bounds: no instruction to replace at current offset."); + } + + let target_ptr = unsafe { self.ptr.add(self.offset) }; + let old_instruction = unsafe { std::ptr::read(target_ptr) }; + + unsafe { + std::ptr::write(target_ptr, instruction); + } + + self.offset += 1; + Ok(old_instruction) + } +} diff --git a/packages/autorun-luajit/src/lib.rs b/packages/autorun-luajit/src/lib.rs index 5e8b95f..33145c9 100644 --- a/packages/autorun-luajit/src/lib.rs +++ b/packages/autorun-luajit/src/lib.rs @@ -1,3 +1,4 @@ +pub mod bytecode; mod helpers; mod types; diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index 34aa4f7..4e0d363 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -1,5 +1,6 @@ // Subset of lj_obj.h +use crate::bytecode::Op; use anyhow::Context; use std::ffi::{c_int, c_void}; use std::fmt::Debug; @@ -58,7 +59,7 @@ pub type LuaAllocFn = extern "C-unwind" fn( nsize: usize, ) -> *mut core::ffi::c_void; -#[repr(C)] +#[repr(C, packed)] #[derive(Clone, Copy, Debug)] pub struct MRef { pub ptr64: u64, @@ -67,7 +68,7 @@ pub struct MRef { impl MRef { // equivalent to the mref macro in LuaJIT pub fn as_ptr(&self) -> *mut T { - self.ptr64 as *mut T + (self.ptr64 & LJ_GCVMASK) as *mut T } pub fn tvref(&self) -> *mut TValue { @@ -75,7 +76,7 @@ impl MRef { } } -#[repr(C)] +#[repr(C, packed)] #[derive(Clone, Copy, Debug)] pub struct GCRef { pub gcptr64: u64, @@ -100,7 +101,7 @@ impl PartialEq for GCRef { } } -#[repr(C, packed)] +#[repr(C)] #[derive(Clone, Copy, Debug)] pub struct GCHeader { pub nextgc: GCRef, @@ -108,6 +109,13 @@ pub struct GCHeader { pub gct: u8, } +impl GCHeader { + pub fn check_type(&self, lj_type: u32) -> bool { + // GCT is stored as the bitwise NOT of the type + self.gct as u32 == (!lj_type & 0xFF) + } +} + // #define LJ_GCVMASK (((uint64_t)1 << 47) - 1) pub const LJ_GCVMASK: u64 = (1u64 << 47) - 1; @@ -182,10 +190,12 @@ pub struct GCFuncHeader { pub nupvalues: u8, pub env: GCRef, pub gclist: GCRef, + // Compiler randomly adds 4 bytes of padding here for alignment, not too sure why since it is packed + pub _pad: [u8; 4], pub pc: MRef, } -#[repr(C)] +#[repr(C, packed)] #[derive(Clone, Copy)] pub struct GCfuncC { pub header: GCFuncHeader, @@ -193,7 +203,7 @@ pub struct GCfuncC { pub upvalue: [TValue; 1], } -#[repr(C)] +#[repr(C, packed)] #[derive(Clone, Copy)] pub struct GCfuncL { pub header: GCFuncHeader, @@ -201,17 +211,20 @@ pub struct GCfuncL { } impl GCfuncL { - pub fn get_proto(&self) -> anyhow::Result<&GCProto> { + pub fn get_proto(&self) -> anyhow::Result<*mut GCProto> { let pc_ref = self.header.pc; // proto starts immediately before the pc pointer let proto = unsafe { pc_ref.as_ptr::().offset(-1) }; - let proto_ref = unsafe { proto.as_ref().context("Failed to dereference GCProto from GCfuncL")? }; + Ok(proto) + } - Ok(proto_ref) + pub fn get_bc_ins(&self) -> anyhow::Result<*mut BCIns> { + let pc_ref = self.header.pc; + Ok(pc_ref.as_ptr::()) } } -#[repr(C)] +#[repr(C, packed)] #[derive(Clone, Copy)] pub union GCfunc { pub c: GCfuncC, @@ -400,14 +413,64 @@ impl IntoLJType for GCUpval { const LJ_TYPE: u32 = LJ_TUPVAL; } -#[derive(Debug, Clone, Copy)] +#[derive(Clone, Copy)] pub struct BCIns(u32); impl BCIns { + // instruction consisting of an opcode, 8-bit A and 16-bit D fields + pub fn from_ad(opcode: Op, a: u8, d: i16) -> Self { + Self((opcode as u32) | ((a as u32) << 8) | (((d as i32 as u32) & 0xFFFF) << 16)) + } + + pub fn from_abc(opcode: Op, a: u8, b: u8, c: u8) -> Self { + Self((opcode as u32) | ((a as u32) << 8) | ((c as u32) << 16) | ((b as u32) << 24)) + } + + pub fn opcode(&self) -> Op { + //#define bc_op(i) ((BCOp)((i)&0xff)) + Op::try_from((self.0 & 0xff) as u8).unwrap() + } + + /* + #define bc_a(i) ((BCReg)(((i)>>8)&0xff)) + #define bc_b(i) ((BCReg)((i)>>24)) + #define bc_c(i) ((BCReg)(((i)>>16)&0xff)) + #define bc_d(i) ((BCReg)((i)>>16)) + */ pub fn a(&self) -> u8 { //#define bc_a(i) ((BCReg)(((i)>>8)&0xff)) ((self.0 >> 8) & 0xff) as u8 } + + pub fn b(&self) -> u8 { + //#define bc_b(i) ((BCReg)((i)>>24)) + ((self.0 >> 24) & 0xff) as u8 + } + + pub fn c(&self) -> u8 { + //#define bc_c(i) ((BCReg)(((i)>>16)&0xff)) + ((self.0 >> 16) & 0xff) as u8 + } + + pub fn d(&self) -> i16 { + //#define bc_d(i) ((BCReg)((i)>>16)) + ((self.0 >> 16) & 0xffff) as i16 + } +} + +impl Debug for BCIns { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "BCIns {{ opcode: {:?}, a: {}, b: {}, c: {}, d: {}, raw: 0x{:08x} }}", + self.opcode(), + self.a(), + self.b(), + self.c(), + self.d(), + self.0 + ) + } } pub type BCLine = u32; @@ -435,6 +498,8 @@ pub struct GCProto { pub lineinfo: MRef, pub uvinfo: MRef, pub varinfo: MRef, + // padding for alignment + pub _pad: [u8; 4], } impl GCProto { From af471f038a9274b7c915d9136fff082fee15ddc2 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:02:51 -0800 Subject: [PATCH 02/22] Add upvalue replacement --- packages/autorun-env/src/functions/detour.rs | 1 + .../autorun-env/src/functions/detour/lua.rs | 1 + .../src/functions/detour/lua/upvalue.rs | 34 +++++++++++++++++++ packages/autorun-luajit/src/types/common.rs | 6 +++- 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 packages/autorun-env/src/functions/detour/lua.rs create mode 100644 packages/autorun-env/src/functions/detour/lua/upvalue.rs diff --git a/packages/autorun-env/src/functions/detour.rs b/packages/autorun-env/src/functions/detour.rs index e59d3e5..5bb1df7 100644 --- a/packages/autorun-env/src/functions/detour.rs +++ b/packages/autorun-env/src/functions/detour.rs @@ -1,4 +1,5 @@ mod handlers; +mod lua; mod raw; mod userdata; diff --git a/packages/autorun-env/src/functions/detour/lua.rs b/packages/autorun-env/src/functions/detour/lua.rs new file mode 100644 index 0000000..574a2c7 --- /dev/null +++ b/packages/autorun-env/src/functions/detour/lua.rs @@ -0,0 +1 @@ +mod upvalue; diff --git a/packages/autorun-env/src/functions/detour/lua/upvalue.rs b/packages/autorun-env/src/functions/detour/lua/upvalue.rs new file mode 100644 index 0000000..4496838 --- /dev/null +++ b/packages/autorun-env/src/functions/detour/lua/upvalue.rs @@ -0,0 +1,34 @@ +//! Handles upvalue overwriting to support detouring Lua functions. +//! This module provides the ability to overwrite upvalues of Lua functions, +//! which is essential for our detouring mechanism to pull the target function into a register + +use anyhow::Context; +use autorun_luajit::{GCUpval, GCfuncL, TValue}; + +pub fn replace(func: GCfuncL, target_index: u32, replacement_tv: TValue) -> anyhow::Result<()> { + if target_index >= func.header.nupvalues as u32 { + anyhow::bail!( + "Upvalue replacement index out of bounds: target_index {} exceeds number of upvalues {}.", + target_index, + func.header.nupvalues + ); + } + + let upvalue_array_ptr = func.uvptr.as_ptr(); + let target_uv_gcr = unsafe { *upvalue_array_ptr.add(target_index as usize) }; + let target_uv = unsafe { + target_uv_gcr + .as_ptr::() + .as_ref() + .context("Failed to deref GCUpval.")? + }; + + // #define uvval(uv_) (mref((uv_)->v, TValue)) + let tvalue_ptr = unsafe { target_uv.v.as_mut_ptr::() }; + // We want to *actually* overwrite the TValue at this location, not just reassign the pointer + unsafe { + std::ptr::write(tvalue_ptr, replacement_tv); + } + + Ok(()) +} diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index 4e0d363..5b5f272 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -71,6 +71,10 @@ impl MRef { (self.ptr64 & LJ_GCVMASK) as *mut T } + pub fn as_mut_ptr(&self) -> *mut T { + (self.ptr64 & LJ_GCVMASK) as *mut T + } + pub fn tvref(&self) -> *mut TValue { self.as_ptr::() } @@ -91,7 +95,7 @@ impl GCRef { // equivalent to the gcref macro in LuaJIT pub fn as_ptr(&self) -> *mut T { - self.gcptr64 as *mut T + (self.gcptr64 & LJ_GCVMASK) as *mut T } } From e5b83396a0df95541911f676e33abb4fe12a0445 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:28:37 -0800 Subject: [PATCH 03/22] Add trampoline module, which makes detouring work --- packages/autorun-env/src/functions/detour.rs | 24 ++++------- .../autorun-env/src/functions/detour/lua.rs | 3 +- .../src/functions/detour/lua/trampoline.rs | 43 +++++++++++++++++++ .../src/functions/detour/lua/upvalue.rs | 2 +- 4 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 packages/autorun-env/src/functions/detour/lua/trampoline.rs diff --git a/packages/autorun-env/src/functions/detour.rs b/packages/autorun-env/src/functions/detour.rs index 5bb1df7..2614b8b 100644 --- a/packages/autorun-env/src/functions/detour.rs +++ b/packages/autorun-env/src/functions/detour.rs @@ -9,7 +9,7 @@ use crate::functions::detour::userdata::Detour; use anyhow::Context; use autorun_lua::{LuaApi, LuaCFunction, LuaTypeId, RawHandle, RawLuaReturn}; use autorun_luajit::bytecode::{BCWriter, Op}; -use autorun_luajit::{BCIns, GCfunc, LJState, get_gcobj, get_gcobj_mut}; +use autorun_luajit::{BCIns, GCfunc, LJState, get_gcobj, get_gcobj_mut, index2adr}; use autorun_types::LuaState; use retour::GenericDetour; use std::ffi::c_int; @@ -130,22 +130,16 @@ pub fn test_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> a let lj_state = state as *mut LJState; let lj_state = unsafe { lj_state.as_mut().context("Failed to dereference LJState.")? }; let gcfunc = get_gcobj::(lj_state, 1).context("Failed to get GCfunc for target function.")?; + let replacement_tv = unsafe { + index2adr(lj_state, 2) + .context("Failed to get TValue for replacement upvalue.")? + .read() + }; let gcfunc_l = gcfunc.as_l().context("Must be a Lua function.")?; - let proto = unsafe { gcfunc_l.get_proto()?.as_mut() }.context("Failed to get prototype.")?; - - if proto.sizebc < 3 { - autorun_log::debug!("Bytecode instruction count: {}, cannot patch test function.", proto.sizebc); - anyhow::bail!("Not enough bytecode instructions to patch."); - } - - let mut bc_writer = BCWriter::from_gcfunc_l(gcfunc_l)?; - bc_writer.set_offset(1)?; // skip after the FUNCF opcode - let old_ins = bc_writer.replace(BCIns::from_ad(Op::KSHORT, 0, 1337))?; // KSHORT - let old_ins2 = bc_writer.replace(BCIns::from_ad(Op::RET1, 0, 2))?; // RET1 - - autorun_log::debug!("Patched bytecode instructions."); - autorun_log::debug!("Old instructions: \n{:#?}\n{:#?}", old_ins, old_ins2); + autorun_log::debug!("Patching upvalue..."); + lua::upvalue::replace(gcfunc_l, 0, replacement_tv)?; + lua::trampoline::overwrite_with_trampoline(gcfunc_l)?; Ok(RawLuaReturn(0)) } diff --git a/packages/autorun-env/src/functions/detour/lua.rs b/packages/autorun-env/src/functions/detour/lua.rs index 574a2c7..67f84b2 100644 --- a/packages/autorun-env/src/functions/detour/lua.rs +++ b/packages/autorun-env/src/functions/detour/lua.rs @@ -1 +1,2 @@ -mod upvalue; +pub mod trampoline; +pub mod upvalue; diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs new file mode 100644 index 0000000..19d13b6 --- /dev/null +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -0,0 +1,43 @@ +//! This module emits the necessary trampoline LJ bytecode for detouring Lua functions. + +use anyhow::Context; +use autorun_luajit::bytecode::{BCWriter, Op}; +use autorun_luajit::{BCIns, GCProto, GCfuncL}; + +/// Assumes detour function is in UV 0. +pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL) -> anyhow::Result<()> { + let mut writer = BCWriter::from_gcfunc_l(gcfunc_l).context("Failed to create BCWriter from GCfuncL")?; + let proto = gcfunc_l.get_proto().context("Failed to get proto from GCfuncL")?; + let proto = unsafe { proto.as_mut().context("Failed to dereference proto")? }; + + if proto.sizebc < 15 { + anyhow::bail!("Target function's proto is too small to overwrite with trampoline."); + } + + if proto.sizeuv < 1 { + anyhow::bail!("Target function's proto does not have enough upvalues for detour trampoline."); + } + + let nargs = proto.numparams; + let maxslots = 2 * nargs + 2; + proto.framesize = maxslots; // update framesize to accommodate trampoline + + writer.write(BCIns::from_ad(Op::FUNCF, maxslots, 0))?; + let mut free_register = nargs; // 0-indexed register after arguments + let detour_register = free_register; + + writer.write(BCIns::from_ad(Op::UGET, free_register, 0))?; // get detour function from upvalue 0 + + // Begin allocating and setting up the argument registers, they are all at 0-nargs, we need them to nargs+1-2*nargs + free_register += 1; // No idea why, but a register needs to be skipped here. Maybe something to do with frame linkage? + for i in 0..nargs { + writer.write(BCIns::from_ad(Op::MOV, free_register + i, i as i16))?; + free_register += 1; + } + + // write final callt + writer.write(BCIns::from_ad(Op::CALLT, detour_register, (nargs + 1) as i16))?; + + // all done, no return necessary as CALLT handles it + Ok(()) +} diff --git a/packages/autorun-env/src/functions/detour/lua/upvalue.rs b/packages/autorun-env/src/functions/detour/lua/upvalue.rs index 4496838..bd073c7 100644 --- a/packages/autorun-env/src/functions/detour/lua/upvalue.rs +++ b/packages/autorun-env/src/functions/detour/lua/upvalue.rs @@ -5,7 +5,7 @@ use anyhow::Context; use autorun_luajit::{GCUpval, GCfuncL, TValue}; -pub fn replace(func: GCfuncL, target_index: u32, replacement_tv: TValue) -> anyhow::Result<()> { +pub fn replace(func: &GCfuncL, target_index: u32, replacement_tv: TValue) -> anyhow::Result<()> { if target_index >= func.header.nupvalues as u32 { anyhow::bail!( "Upvalue replacement index out of bounds: target_index {} exceeds number of upvalues {}.", From 306764511eaa2a4a0aedc3c258752dab46e0e1ed Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:38:51 -0800 Subject: [PATCH 04/22] Basic explanation --- packages/autorun-env/src/functions/detour/lua/trampoline.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs index 19d13b6..0440d6c 100644 --- a/packages/autorun-env/src/functions/detour/lua/trampoline.rs +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -5,6 +5,11 @@ use autorun_luajit::bytecode::{BCWriter, Op}; use autorun_luajit::{BCIns, GCProto, GCfuncL}; /// Assumes detour function is in UV 0. +/// # Detouring +/// This emits a trampoline which sets up a new function that specifically +/// pulls the detour function from upvalue 0, moves all arguments into +/// their correct registers, and then calls the detour function with CALLT. +/// This trampoline completely replaces the target function's bytecode and does not consume an extra level of stack. pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL) -> anyhow::Result<()> { let mut writer = BCWriter::from_gcfunc_l(gcfunc_l).context("Failed to create BCWriter from GCfuncL")?; let proto = gcfunc_l.get_proto().context("Failed to get proto from GCfuncL")?; From e6e1336cccb757119174302664afd4a1dc5f0086 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:51:20 -0800 Subject: [PATCH 05/22] Fix register allocation --- packages/autorun-env/src/functions/detour/lua/trampoline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs index 0440d6c..4acd7e0 100644 --- a/packages/autorun-env/src/functions/detour/lua/trampoline.rs +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -36,8 +36,8 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL) -> anyhow::Result<()> { // Begin allocating and setting up the argument registers, they are all at 0-nargs, we need them to nargs+1-2*nargs free_register += 1; // No idea why, but a register needs to be skipped here. Maybe something to do with frame linkage? for i in 0..nargs { - writer.write(BCIns::from_ad(Op::MOV, free_register + i, i as i16))?; free_register += 1; + writer.write(BCIns::from_ad(Op::MOV, free_register, i as i16))?; } // write final callt From 6a50ae186466ef7b5734dc93f2272821e6f489d9 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Tue, 11 Nov 2025 21:58:33 -0800 Subject: [PATCH 06/22] Check for varags --- .../src/functions/detour/lua/trampoline.rs | 6 +++++- packages/autorun-luajit/src/types/common.rs | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs index 4acd7e0..17ec7ec 100644 --- a/packages/autorun-env/src/functions/detour/lua/trampoline.rs +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -2,7 +2,7 @@ use anyhow::Context; use autorun_luajit::bytecode::{BCWriter, Op}; -use autorun_luajit::{BCIns, GCProto, GCfuncL}; +use autorun_luajit::{BCIns, GCProto, GCfuncL, ProtoFlags}; /// Assumes detour function is in UV 0. /// # Detouring @@ -23,6 +23,10 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL) -> anyhow::Result<()> { anyhow::bail!("Target function's proto does not have enough upvalues for detour trampoline."); } + if proto.has_flag(ProtoFlags::Vararg) { + anyhow::bail!("Detour trampoline does not support vararg functions."); + } + let nargs = proto.numparams; let maxslots = 2 * nargs + 2; proto.framesize = maxslots; // update framesize to accommodate trampoline diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index 5b5f272..d86799e 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -478,6 +478,13 @@ impl Debug for BCIns { } pub type BCLine = u32; +pub enum ProtoFlags { + Child = 0x01, + Vararg = 0x02, + FFI = 0x04, + NoJIT = 0x08, + ILoop = 0x10, +} #[repr(C)] #[derive(Debug, Clone, Copy)] @@ -517,6 +524,10 @@ impl GCProto { chunk_name.as_str() } + + pub fn has_flag(&self, flag: ProtoFlags) -> bool { + (self.flags & (flag as u8)) != 0 + } } impl IntoLJType for GCProto { From b0475434226adff315d129de021504e1e4040dba Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Tue, 11 Nov 2025 22:34:16 -0800 Subject: [PATCH 07/22] Clean up code --- .../src/functions/detour/lua/trampoline.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs index 17ec7ec..914ef87 100644 --- a/packages/autorun-env/src/functions/detour/lua/trampoline.rs +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -4,6 +4,9 @@ use anyhow::Context; use autorun_luajit::bytecode::{BCWriter, Op}; use autorun_luajit::{BCIns, GCProto, GCfuncL, ProtoFlags}; +const MINIMUM_BYTECODES: u32 = 15; +const MINIMUM_UPVALUES: u8 = 1; + /// Assumes detour function is in UV 0. /// # Detouring /// This emits a trampoline which sets up a new function that specifically @@ -15,11 +18,11 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL) -> anyhow::Result<()> { let proto = gcfunc_l.get_proto().context("Failed to get proto from GCfuncL")?; let proto = unsafe { proto.as_mut().context("Failed to dereference proto")? }; - if proto.sizebc < 15 { + if proto.sizebc < MINIMUM_BYTECODES { anyhow::bail!("Target function's proto is too small to overwrite with trampoline."); } - if proto.sizeuv < 1 { + if proto.sizeuv < MINIMUM_UPVALUES { anyhow::bail!("Target function's proto does not have enough upvalues for detour trampoline."); } @@ -31,6 +34,12 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL) -> anyhow::Result<()> { let maxslots = 2 * nargs + 2; proto.framesize = maxslots; // update framesize to accommodate trampoline + // Trampoline basics: + // FUNCF maxslots + // UGET detour_function_register, 0 + // MOV arg_registers... + // CALLT detour_function_register, nargs+1 + writer.write(BCIns::from_ad(Op::FUNCF, maxslots, 0))?; let mut free_register = nargs; // 0-indexed register after arguments let detour_register = free_register; @@ -48,5 +57,7 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL) -> anyhow::Result<()> { writer.write(BCIns::from_ad(Op::CALLT, detour_register, (nargs + 1) as i16))?; // all done, no return necessary as CALLT handles it + // CALLT also jumps directly to the function, so no need for us to fix up + // the sizebc field Ok(()) } From e002389c99ac9ce2d99a9fc57c22b123a75cc61b Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:41:23 -0800 Subject: [PATCH 08/22] Add WIP detour restoration --- packages/autorun-env/src/env.rs | 1 + packages/autorun-env/src/functions/detour.rs | 29 +++++++- .../autorun-env/src/functions/detour/lua.rs | 1 + .../src/functions/detour/lua/state.rs | 67 +++++++++++++++++++ .../src/functions/detour/lua/trampoline.rs | 9 ++- .../src/functions/detour/lua/upvalue.rs | 22 +++++- .../autorun-luajit/src/bytecode/writer.rs | 4 ++ packages/autorun-luajit/src/types/common.rs | 4 ++ 8 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 packages/autorun-env/src/functions/detour/lua/state.rs diff --git a/packages/autorun-env/src/env.rs b/packages/autorun-env/src/env.rs index 3e3d83a..4389498 100644 --- a/packages/autorun-env/src/env.rs +++ b/packages/autorun-env/src/env.rs @@ -109,6 +109,7 @@ impl EnvHandle { lua.set(state, &t, "isFunctionAuthorized", wrap!(functions::is_function_authorized)); lua.set(state, &t, "isProtoAuthorized", wrap!(functions::is_proto_authorized)); lua.set(state, &t, "testLua", wrap!(functions::test_lua)); + lua.set(state, &t, "restoreLua", wrap!(functions::restore_lua)); lua.set(state, &t, "VERSION", env!("CARGO_PKG_VERSION")); return t; diff --git a/packages/autorun-env/src/functions/detour.rs b/packages/autorun-env/src/functions/detour.rs index 2614b8b..7913eb9 100644 --- a/packages/autorun-env/src/functions/detour.rs +++ b/packages/autorun-env/src/functions/detour.rs @@ -4,6 +4,7 @@ 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; @@ -138,8 +139,32 @@ pub fn test_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> a let gcfunc_l = gcfunc.as_l().context("Must be a Lua function.")?; autorun_log::debug!("Patching upvalue..."); - lua::upvalue::replace(gcfunc_l, 0, replacement_tv)?; - lua::trampoline::overwrite_with_trampoline(gcfunc_l)?; + let mut original_detour_state = OriginalDetourState::new(); + lua::upvalue::replace(gcfunc_l, 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::() }.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 { + 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::() }.context("Failed to get GCfunc pointer.")?; + + lua::state::restore_func(original_function_ptr)?; Ok(RawLuaReturn(0)) } diff --git a/packages/autorun-env/src/functions/detour/lua.rs b/packages/autorun-env/src/functions/detour/lua.rs index 67f84b2..f1de8f6 100644 --- a/packages/autorun-env/src/functions/detour/lua.rs +++ b/packages/autorun-env/src/functions/detour/lua.rs @@ -1,2 +1,3 @@ +pub mod state; pub mod trampoline; pub mod upvalue; diff --git a/packages/autorun-env/src/functions/detour/lua/state.rs b/packages/autorun-env/src/functions/detour/lua/state.rs new file mode 100644 index 0000000..c78d40e --- /dev/null +++ b/packages/autorun-env/src/functions/detour/lua/state.rs @@ -0,0 +1,67 @@ +//! 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, + pub original_frame_size: u8, + pub original_upvalue_0: TValue, +} + +pub static ORIGINAL_DETOUR_STATES: LazyLock>> = + 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 { + 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."); + + Ok(()) +} diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs index 914ef87..e1d3fd0 100644 --- a/packages/autorun-env/src/functions/detour/lua/trampoline.rs +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -1,5 +1,6 @@ //! This module emits the necessary trampoline LJ bytecode for detouring Lua functions. +use crate::functions::detour::lua::state::OriginalDetourState; use anyhow::Context; use autorun_luajit::bytecode::{BCWriter, Op}; use autorun_luajit::{BCIns, GCProto, GCfuncL, ProtoFlags}; @@ -13,7 +14,7 @@ const MINIMUM_UPVALUES: u8 = 1; /// pulls the detour function from upvalue 0, moves all arguments into /// their correct registers, and then calls the detour function with CALLT. /// This trampoline completely replaces the target function's bytecode and does not consume an extra level of stack. -pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL) -> anyhow::Result<()> { +pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL, original_detour_state: &mut OriginalDetourState) -> anyhow::Result<()> { let mut writer = BCWriter::from_gcfunc_l(gcfunc_l).context("Failed to create BCWriter from GCfuncL")?; let proto = gcfunc_l.get_proto().context("Failed to get proto from GCfuncL")?; let proto = unsafe { proto.as_mut().context("Failed to dereference proto")? }; @@ -30,6 +31,12 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL) -> anyhow::Result<()> { anyhow::bail!("Detour trampoline does not support vararg functions."); } + // Save original fields for restoration later + let bytecode_slice = unsafe { std::slice::from_raw_parts(writer.get_ptr(), proto.sizebc as usize) }; + let original_bytecode = bytecode_slice.to_vec(); + original_detour_state.original_bytecode = original_bytecode; + original_detour_state.original_frame_size = proto.framesize; + let nargs = proto.numparams; let maxslots = 2 * nargs + 2; proto.framesize = maxslots; // update framesize to accommodate trampoline diff --git a/packages/autorun-env/src/functions/detour/lua/upvalue.rs b/packages/autorun-env/src/functions/detour/lua/upvalue.rs index bd073c7..00db1a5 100644 --- a/packages/autorun-env/src/functions/detour/lua/upvalue.rs +++ b/packages/autorun-env/src/functions/detour/lua/upvalue.rs @@ -2,10 +2,24 @@ //! This module provides the ability to overwrite upvalues of Lua functions, //! which is essential for our detouring mechanism to pull the target function into a register +use crate::functions::detour::lua::state::OriginalDetourState; use anyhow::Context; use autorun_luajit::{GCUpval, GCfuncL, TValue}; -pub fn replace(func: &GCfuncL, target_index: u32, replacement_tv: TValue) -> anyhow::Result<()> { +pub fn replace( + func: &GCfuncL, + target_index: u32, + replacement_tv: TValue, + original_detour_state: &mut OriginalDetourState, +) -> anyhow::Result<()> { + let old_uv = overwrite_upvalue(func, target_index, replacement_tv)?; + // TODO: Support multiple upvalues in the future + original_detour_state.original_upvalue_0 = old_uv; + + Ok(()) +} + +pub fn overwrite_upvalue(func: &GCfuncL, target_index: u32, replacement_tv: TValue) -> anyhow::Result { if target_index >= func.header.nupvalues as u32 { anyhow::bail!( "Upvalue replacement index out of bounds: target_index {} exceeds number of upvalues {}.", @@ -25,10 +39,12 @@ pub fn replace(func: &GCfuncL, target_index: u32, replacement_tv: TValue) -> any // #define uvval(uv_) (mref((uv_)->v, TValue)) let tvalue_ptr = unsafe { target_uv.v.as_mut_ptr::() }; + let original_tv = unsafe { std::ptr::read(tvalue_ptr) }; + // We want to *actually* overwrite the TValue at this location, not just reassign the pointer unsafe { std::ptr::write(tvalue_ptr, replacement_tv); - } + }; - Ok(()) + Ok(original_tv) } diff --git a/packages/autorun-luajit/src/bytecode/writer.rs b/packages/autorun-luajit/src/bytecode/writer.rs index 402116f..ae3a6e8 100644 --- a/packages/autorun-luajit/src/bytecode/writer.rs +++ b/packages/autorun-luajit/src/bytecode/writer.rs @@ -65,4 +65,8 @@ impl BCWriter { self.offset += 1; Ok(old_instruction) } + + pub fn get_ptr(&self) -> *mut BCIns { + self.ptr + } } diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index d86799e..ee628d6 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -150,6 +150,10 @@ macro_rules! impl_tvalue_type_check { } impl TValue { + pub fn nil() -> Self { + Self { it64: -1 } + } + pub fn as_ptr(&self) -> anyhow::Result<*mut T> { if self.itype() != T::LJ_TYPE { anyhow::bail!("TValue type mismatch: expected {}, got {}", T::LJ_TYPE, self.itype()); From f084fd417b180a656760d6a0a5c404a431f77511 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:19:09 -0800 Subject: [PATCH 09/22] Remove extraneous state --- packages/autorun-env/src/functions/detour/lua/state.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/autorun-env/src/functions/detour/lua/state.rs b/packages/autorun-env/src/functions/detour/lua/state.rs index c78d40e..d578e24 100644 --- a/packages/autorun-env/src/functions/detour/lua/state.rs +++ b/packages/autorun-env/src/functions/detour/lua/state.rs @@ -63,5 +63,8 @@ pub fn restore_func(func: *mut GCfunc) -> anyhow::Result<()> { 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(()) } From f0aacf50d1ad3dd19301a87529510b356c240752 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:54:55 -0800 Subject: [PATCH 10/22] Replace proto's debug info with the target --- packages/autorun-env/src/functions/detour.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/autorun-env/src/functions/detour.rs b/packages/autorun-env/src/functions/detour.rs index 7913eb9..d463e29 100644 --- a/packages/autorun-env/src/functions/detour.rs +++ b/packages/autorun-env/src/functions/detour.rs @@ -131,12 +131,28 @@ pub fn test_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> a let lj_state = state as *mut LJState; let lj_state = unsafe { lj_state.as_mut().context("Failed to dereference LJState.")? }; let gcfunc = get_gcobj::(lj_state, 1).context("Failed to get GCfunc for target function.")?; + 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::(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; + let gcfunc_l = gcfunc.as_l().context("Must be a Lua function.")?; autorun_log::debug!("Patching upvalue..."); let mut original_detour_state = OriginalDetourState::new(); From e1507f61690c2094a9fe6373e7f6aed3d4c97dad Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:44:51 -0800 Subject: [PATCH 11/22] Add preliminary varg handling --- .../src/functions/detour/lua/trampoline.rs | 47 ++++++++++++++----- packages/autorun-luajit/src/types/common.rs | 6 ++- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs index e1d3fd0..05f409a 100644 --- a/packages/autorun-env/src/functions/detour/lua/trampoline.rs +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -27,10 +27,6 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL, original_detour_state: &mut anyhow::bail!("Target function's proto does not have enough upvalues for detour trampoline."); } - if proto.has_flag(ProtoFlags::Vararg) { - anyhow::bail!("Detour trampoline does not support vararg functions."); - } - // Save original fields for restoration later let bytecode_slice = unsafe { std::slice::from_raw_parts(writer.get_ptr(), proto.sizebc as usize) }; let original_bytecode = bytecode_slice.to_vec(); @@ -41,12 +37,19 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL, original_detour_state: &mut let maxslots = 2 * nargs + 2; proto.framesize = maxslots; // update framesize to accommodate trampoline - // Trampoline basics: - // FUNCF maxslots - // UGET detour_function_register, 0 - // MOV arg_registers... - // CALLT detour_function_register, nargs+1 + if proto.has_flag(ProtoFlags::Vararg) { + write_varg_trampoline_bytecode(&mut writer, nargs, maxslots)?; + } else { + write_trampoline_bytecode(&mut writer, nargs, maxslots)?; + } + + // all done, no return necessary as CALLT handles it + // CALLT also jumps directly to the function, so no need for us to fix up + // the sizebc field + Ok(()) +} +fn write_trampoline_bytecode(writer: &mut BCWriter, nargs: u8, maxslots: u8) -> anyhow::Result<()> { writer.write(BCIns::from_ad(Op::FUNCF, maxslots, 0))?; let mut free_register = nargs; // 0-indexed register after arguments let detour_register = free_register; @@ -62,9 +65,29 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL, original_detour_state: &mut // write final callt writer.write(BCIns::from_ad(Op::CALLT, detour_register, (nargs + 1) as i16))?; + Ok(()) +} - // all done, no return necessary as CALLT handles it - // CALLT also jumps directly to the function, so no need for us to fix up - // the sizebc field +fn write_varg_trampoline_bytecode(writer: &mut BCWriter, nargs: u8, maxslots: u8) -> anyhow::Result<()> { + // Add an extra slot for vararg handling + writer.write(BCIns::from_ad(Op::FUNCV, maxslots + 1, 0))?; + + let mut free_register = nargs; // 0-indexed register after arguments + let detour_register = free_register; + + writer.write(BCIns::from_ad(Op::UGET, free_register, 0))?; // get detour function from upvalue 0 + + // Do the same allocation and setup of argument registers + free_register += 1; + for i in 0..nargs { + free_register += 1; + writer.write(BCIns::from_ad(Op::MOV, free_register, i as i16))?; + } + + // Set up vararg handling + writer.write(BCIns::from_abc(Op::VARG, nargs * 2 + 2, 0, nargs))?; + + // Use a metatable call to deal with the vararg pseudo-frame accordingly + writer.write(BCIns::from_ad(Op::CALLMT, detour_register, nargs as i16))?; Ok(()) } diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index ee628d6..a4b734d 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -482,6 +482,9 @@ impl Debug for BCIns { } pub type BCLine = u32; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProtoFlags { Child = 0x01, Vararg = 0x02, @@ -505,6 +508,7 @@ pub struct GCProto { pub sizekn: MSize, pub sizept: MSize, pub sizeuv: u8, + pub pad: [u8; 4], pub flags: u8, pub trace: u16, pub chunkname: GCRef, @@ -513,8 +517,6 @@ pub struct GCProto { pub lineinfo: MRef, pub uvinfo: MRef, pub varinfo: MRef, - // padding for alignment - pub _pad: [u8; 4], } impl GCProto { From 034c96662e9b650fb4e9eee77158b64156d8744e Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:49:01 -0800 Subject: [PATCH 12/22] Cleanup code --- .../src/functions/detour/lua/trampoline.rs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs index 05f409a..147b2b0 100644 --- a/packages/autorun-env/src/functions/detour/lua/trampoline.rs +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -56,12 +56,7 @@ fn write_trampoline_bytecode(writer: &mut BCWriter, nargs: u8, maxslots: u8) -> writer.write(BCIns::from_ad(Op::UGET, free_register, 0))?; // get detour function from upvalue 0 - // Begin allocating and setting up the argument registers, they are all at 0-nargs, we need them to nargs+1-2*nargs - free_register += 1; // No idea why, but a register needs to be skipped here. Maybe something to do with frame linkage? - for i in 0..nargs { - free_register += 1; - writer.write(BCIns::from_ad(Op::MOV, free_register, i as i16))?; - } + allocate_arguments(writer, nargs, free_register)?; // write final callt writer.write(BCIns::from_ad(Op::CALLT, detour_register, (nargs + 1) as i16))?; @@ -77,12 +72,7 @@ fn write_varg_trampoline_bytecode(writer: &mut BCWriter, nargs: u8, maxslots: u8 writer.write(BCIns::from_ad(Op::UGET, free_register, 0))?; // get detour function from upvalue 0 - // Do the same allocation and setup of argument registers - free_register += 1; - for i in 0..nargs { - free_register += 1; - writer.write(BCIns::from_ad(Op::MOV, free_register, i as i16))?; - } + allocate_arguments(writer, nargs, free_register)?; // Set up vararg handling writer.write(BCIns::from_abc(Op::VARG, nargs * 2 + 2, 0, nargs))?; @@ -91,3 +81,12 @@ fn write_varg_trampoline_bytecode(writer: &mut BCWriter, nargs: u8, maxslots: u8 writer.write(BCIns::from_ad(Op::CALLMT, detour_register, nargs as i16))?; Ok(()) } + +fn allocate_arguments(writer: &mut BCWriter, nargs: u8, mut free_register: u8) -> anyhow::Result<()> { + free_register += 1; // No idea why, but a register needs to be skipped here. Maybe something to do with frame linkage? + for i in 0..nargs { + free_register += 1; + writer.write(BCIns::from_ad(Op::MOV, free_register, i as i16))?; + } + Ok(()) +} From fbdc3610fbe010973bba3b34c755fd80e3eb9f38 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:12:52 -0800 Subject: [PATCH 13/22] Add comment --- packages/autorun-env/src/functions/detour/lua/trampoline.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs index 147b2b0..31f4cf7 100644 --- a/packages/autorun-env/src/functions/detour/lua/trampoline.rs +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -72,6 +72,7 @@ fn write_varg_trampoline_bytecode(writer: &mut BCWriter, nargs: u8, maxslots: u8 writer.write(BCIns::from_ad(Op::UGET, free_register, 0))?; // get detour function from upvalue 0 + // Vararg functions can still have fixed arguments, so we need to allocate those first before handling varargs allocate_arguments(writer, nargs, free_register)?; // Set up vararg handling From f32c871212b1037f8fa1bfd2bf658441e62157d8 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:31:05 -0800 Subject: [PATCH 14/22] Write initial implementation of function cloning --- .../autorun-env/src/functions/detour/lua.rs | 1 + .../src/functions/detour/lua/clone.rs | 58 +++++++++++++++++++ packages/autorun-luajit/src/helpers.rs | 46 +++++++++++++++ packages/autorun-luajit/src/types/common.rs | 29 ++++++++++ 4 files changed, 134 insertions(+) create mode 100644 packages/autorun-env/src/functions/detour/lua/clone.rs diff --git a/packages/autorun-env/src/functions/detour/lua.rs b/packages/autorun-env/src/functions/detour/lua.rs index f1de8f6..0cc6e87 100644 --- a/packages/autorun-env/src/functions/detour/lua.rs +++ b/packages/autorun-env/src/functions/detour/lua.rs @@ -1,3 +1,4 @@ +mod clone; pub mod state; pub mod trampoline; pub mod upvalue; diff --git a/packages/autorun-env/src/functions/detour/lua/clone.rs b/packages/autorun-env/src/functions/detour/lua/clone.rs new file mode 100644 index 0000000..c58fa87 --- /dev/null +++ b/packages/autorun-env/src/functions/detour/lua/clone.rs @@ -0,0 +1,58 @@ +//! 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, GCfunc, GCfuncL, LJState, TValue, mem_newgco, push_tvalue}; + +/// 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: &GCfuncL) -> anyhow::Result<()> { + // Proto must be cloned first. + let proto = target_func.get_proto()?; + let proto_size = unsafe { (*proto).sizept } as GCSize; + let proto_uv_size = unsafe { (*proto).sizeuv } as GCSize; + + // 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::(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::()), + new_proto_ptr as *mut u8, + proto_size as usize, + ); + }; + + // 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::() as GCSize - size_of::() as GCSize + size_of::() as GCSize * proto_uv_size; + let new_func_ptr = unsafe { mem_newgco::(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::()), + new_func_ptr as *mut u8, + func_size as usize, + ); + }; + + // 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::()) as *mut BCIns; + (*new_func_ptr).header_mut().pc.set_ptr(bc_ptr); + } + + // 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(()) +} diff --git a/packages/autorun-luajit/src/helpers.rs b/packages/autorun-luajit/src/helpers.rs index 45a617f..b093653 100644 --- a/packages/autorun-luajit/src/helpers.rs +++ b/packages/autorun-luajit/src/helpers.rs @@ -65,3 +65,49 @@ pub fn push_frame_func(state: &mut LJState, frame: &Frame) -> anyhow::Result<()> push_tvalue(state, unsafe { &*frame.get_func_tv() }); Ok(()) } + +// Our number one goal is to avoid having to depend on things like sigscanning to find internal functions. +// Therefore we re-implement the functionality we need here. +/** +/* Allocate new GC object and link it to the root set. */ +void * LJ_FASTCALL lj_mem_newgco(lua_State *L, GCSize size) +{ + global_State *g = G(L); + GCobj *o = (GCobj *)g->allocf(g->allocd, NULL, 0, size); + if (o == NULL) + lj_err_mem(L); + lj_assertG(checkptrGC(o), + "allocated memory address %p outside required range", o); + g->gc.total += size; + setgcrefr(o->gch.nextgc, g->gc.root); + setgcref(g->gc.root, o); + newwhite(g, o); + return o; +} + +#define newwhite(g, x) (obj2gco(x)->gch.marked = (uint8_t)curwhite(g)) +#define curwhite(g) ((g)->gc.currentwhite & LJ_GC_WHITES) +*/ + +pub fn mem_newgco(state: &mut LJState, size: GCSize) -> anyhow::Result<*mut T> { + let global_state = global_state(state); + let global_state = unsafe { global_state.as_mut().context("Failed to dereference GlobalState")? }; + let allocf = global_state.allocf; + let allocd = global_state.allocd; + let obj_ptr = unsafe { allocf(allocd, std::ptr::null_mut(), 0, size as usize) }; + if obj_ptr.is_null() { + anyhow::bail!("Memory allocation failed in lj_mem_newgco."); + } + + unsafe { + global_state.gc.total += size; + let gc_header_ptr = obj_ptr as *mut GCHeader; + (*gc_header_ptr).nextgc = global_state.gc.root; + global_state.gc.root.gcptr64 = obj_ptr as u64; + + // newwhite + (*gc_header_ptr).marked = (global_state.gc.currentwhite & LJ_GC_WHITES) as u8; + } + + Ok(obj_ptr as *mut T) +} diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index a4b734d..b519fa2 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -75,6 +75,10 @@ impl MRef { (self.ptr64 & LJ_GCVMASK) as *mut T } + pub fn set_ptr(&mut self, ptr: *mut T) { + self.ptr64 = (ptr as u64) & LJ_GCVMASK; + } + pub fn tvref(&self) -> *mut TValue { self.as_ptr::() } @@ -154,6 +158,15 @@ impl TValue { Self { it64: -1 } } + pub fn from_ptr(ptr: *mut T) -> Self { + let gcr = GCRef::from_ptr(ptr); + let itype = T::LJ_TYPE as u64; + let mut tv = Self { gcr }; + + tv.set_itype(itype); + tv + } + pub fn as_ptr(&self) -> anyhow::Result<*mut T> { if self.itype() != T::LJ_TYPE { anyhow::bail!("TValue type mismatch: expected {}, got {}", T::LJ_TYPE, self.itype()); @@ -174,6 +187,11 @@ impl TValue { unsafe { ((self.it64 >> 47) & 0xFFFFFFFF) as u32 } } + pub fn set_itype(&mut self, itype: u64) { + let it64 = unsafe { self.it64 & 0x00007FFFFFFFFFFF }; // clear the type bits + self.it64 = it64 | ((itype as i64) << 47); + } + impl_tvalue_type_check!(is_nil, LJ_TNIL); impl_tvalue_type_check!(is_false, LJ_TFALSE); impl_tvalue_type_check!(is_true, LJ_TTRUE); @@ -328,6 +346,17 @@ pub struct Sbuf { pub l: MRef, } +pub const LJ_GC_WHITE0: u8 = 0x01; +pub const LJ_GC_WHITE1: u8 = 0x02; +pub const LJ_GC_BLACK: u8 = 0x04; +pub const LJ_GC_FINALIZED: u8 = 0x08; +pub const LJ_GC_WEAKKEY: u8 = 0x08; +pub const LJ_GC_WEAKVAL: u8 = 0x10; +pub const LJ_GC_CDATA_FIN: u8 = 0x10; +pub const LJ_GC_FIXED: u8 = 0x20; +pub const LJ_GC_SFIXED: u8 = 0x40; +pub const LJ_GC_WHITES: u8 = LJ_GC_WHITE0 | LJ_GC_WHITE1; + #[repr(C)] pub struct GCState { pub total: GCSize, From 1b4407ac00919e5e3875a2149e26fb580d94bff8 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:01:05 -0800 Subject: [PATCH 15/22] Fixup offsets and fix other issues --- packages/autorun-env/src/env.rs | 1 + packages/autorun-env/src/functions/detour.rs | 15 +++++++++ .../autorun-env/src/functions/detour/lua.rs | 4 ++- .../src/functions/detour/lua/clone.rs | 33 +++++++++++++++++++ packages/autorun-luajit/src/helpers.rs | 6 ++++ packages/autorun-luajit/src/types/common.rs | 4 ++- 6 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/autorun-env/src/env.rs b/packages/autorun-env/src/env.rs index 4389498..ab62ab7 100644 --- a/packages/autorun-env/src/env.rs +++ b/packages/autorun-env/src/env.rs @@ -110,6 +110,7 @@ impl EnvHandle { lua.set(state, &t, "isProtoAuthorized", wrap!(functions::is_proto_authorized)); lua.set(state, &t, "testLua", wrap!(functions::test_lua)); lua.set(state, &t, "restoreLua", wrap!(functions::restore_lua)); + lua.set(state, &t, "cloneLua", wrap!(functions::clone_lua)); lua.set(state, &t, "VERSION", env!("CARGO_PKG_VERSION")); return t; diff --git a/packages/autorun-env/src/functions/detour.rs b/packages/autorun-env/src/functions/detour.rs index d463e29..812f9f0 100644 --- a/packages/autorun-env/src/functions/detour.rs +++ b/packages/autorun-env/src/functions/detour.rs @@ -184,3 +184,18 @@ pub fn restore_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) - Ok(RawLuaReturn(0)) } + +pub fn clone_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result { + 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::(lj_state, 1).context("Failed to get GCfunc for target function.")?; + let gcfunc_l = gcfunc.as_l().context("Target function must be a Lua function.")?; + + lua::clone(lj_state, gcfunc_l)?; + Ok(RawLuaReturn(1)) +} diff --git a/packages/autorun-env/src/functions/detour/lua.rs b/packages/autorun-env/src/functions/detour/lua.rs index 0cc6e87..0aaa0c8 100644 --- a/packages/autorun-env/src/functions/detour/lua.rs +++ b/packages/autorun-env/src/functions/detour/lua.rs @@ -1,4 +1,6 @@ -mod clone; +pub mod clone; pub mod state; pub mod trampoline; pub mod upvalue; + +pub use clone::clone; diff --git a/packages/autorun-env/src/functions/detour/lua/clone.rs b/packages/autorun-env/src/functions/detour/lua/clone.rs index c58fa87..406793f 100644 --- a/packages/autorun-env/src/functions/detour/lua/clone.rs +++ b/packages/autorun-env/src/functions/detour/lua/clone.rs @@ -13,6 +13,9 @@ pub fn clone(lj_state: &mut LJState, target_func: &GCfuncL) -> anyhow::Result<() 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::(lj_state, proto_size)? }; @@ -25,6 +28,11 @@ pub fn clone(lj_state: &mut LJState, target_func: &GCfuncL) -> anyhow::Result<() ); }; + 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. @@ -56,3 +64,28 @@ pub fn clone(lj_state: &mut LJState, target_func: &GCfuncL) -> anyhow::Result<() 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(()) +} diff --git a/packages/autorun-luajit/src/helpers.rs b/packages/autorun-luajit/src/helpers.rs index b093653..bd6dfa0 100644 --- a/packages/autorun-luajit/src/helpers.rs +++ b/packages/autorun-luajit/src/helpers.rs @@ -92,6 +92,12 @@ void * LJ_FASTCALL lj_mem_newgco(lua_State *L, GCSize size) pub fn mem_newgco(state: &mut LJState, size: GCSize) -> anyhow::Result<*mut T> { let global_state = global_state(state); let global_state = unsafe { global_state.as_mut().context("Failed to dereference GlobalState")? }; + + dbg!(&global_state.gc); + dbg!(&global_state.allocd); + dbg!(&global_state.allocf); + dbg!(&size); + let allocf = global_state.allocf; let allocd = global_state.allocd; let obj_ptr = unsafe { allocf(allocd, std::ptr::null_mut(), 0, size as usize) }; diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index b519fa2..f16ce0a 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -326,6 +326,7 @@ pub struct LJState { pub header: GCHeader, pub dummy_ffid: u8, pub status: u8, + pub pad: [u8; 4], pub glref: MRef, pub gclist: GCRef, pub base: *mut TValue, @@ -358,6 +359,7 @@ pub const LJ_GC_SFIXED: u8 = 0x40; pub const LJ_GC_WHITES: u8 = LJ_GC_WHITE0 | LJ_GC_WHITE1; #[repr(C)] +#[derive(Debug)] pub struct GCState { pub total: GCSize, pub threshold: GCSize, @@ -530,6 +532,7 @@ pub struct GCProto { pub framesize: u8, pub sizebc: MSize, pub unused: u32, + pub pad: [u8; 4], pub gclist: GCRef, pub k: MRef, pub uv: MRef, @@ -537,7 +540,6 @@ pub struct GCProto { pub sizekn: MSize, pub sizept: MSize, pub sizeuv: u8, - pub pad: [u8; 4], pub flags: u8, pub trace: u16, pub chunkname: GCRef, From b3c02c83b1b3f06002d38e97e015043637af3c0b Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:00:43 -0800 Subject: [PATCH 16/22] Fix copying garbage data, function cloning somewhat working. --- .../autorun-env/src/functions/detour/lua/clone.rs | 11 +++++++---- packages/autorun-luajit/src/helpers.rs | 1 + packages/autorun-luajit/src/types/common.rs | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/autorun-env/src/functions/detour/lua/clone.rs b/packages/autorun-env/src/functions/detour/lua/clone.rs index 406793f..975fc89 100644 --- a/packages/autorun-env/src/functions/detour/lua/clone.rs +++ b/packages/autorun-env/src/functions/detour/lua/clone.rs @@ -8,6 +8,7 @@ use autorun_luajit::{BCIns, GCHeader, GCProto, GCRef, GCSize, GCfunc, GCfuncL, L /// 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: &GCfuncL) -> anyhow::Result<()> { + dbg!(&target_func); // Proto must be cloned first. let proto = target_func.get_proto()?; let proto_size = unsafe { (*proto).sizept } as GCSize; @@ -23,8 +24,8 @@ pub fn clone(lj_state: &mut LJState, target_func: &GCfuncL) -> anyhow::Result<() // Treat every pointer as raw bytes, since sizept is specified as bytes. std::ptr::copy_nonoverlapping( (proto as *const u8).byte_add(size_of::()), - new_proto_ptr as *mut u8, - proto_size as usize, + (new_proto_ptr as *mut u8).byte_add(size_of::()), + proto_size as usize - size_of::(), ); }; @@ -43,11 +44,12 @@ pub fn clone(lj_state: &mut LJState, target_func: &GCfuncL) -> anyhow::Result<() unsafe { let target_func = target_func as *const GCfuncL as *const u8; + dbg!(&target_func); std::ptr::copy_nonoverlapping( target_func.byte_add(size_of::()), - new_func_ptr as *mut u8, - func_size as usize, + (new_func_ptr as *mut u8).byte_add(size_of::()), + func_size as usize - size_of::(), ); }; @@ -58,6 +60,7 @@ pub fn clone(lj_state: &mut LJState, target_func: &GCfuncL) -> anyhow::Result<() (*new_func_ptr).header_mut().pc.set_ptr(bc_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); diff --git a/packages/autorun-luajit/src/helpers.rs b/packages/autorun-luajit/src/helpers.rs index bd6dfa0..cfb8b2b 100644 --- a/packages/autorun-luajit/src/helpers.rs +++ b/packages/autorun-luajit/src/helpers.rs @@ -113,6 +113,7 @@ pub fn mem_newgco(state: &mut LJState, size: GCSize) -> anyhow::R // newwhite (*gc_header_ptr).marked = (global_state.gc.currentwhite & LJ_GC_WHITES) as u8; + (*gc_header_ptr).gct = !T::LJ_TYPE as u8; } Ok(obj_ptr as *mut T) diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index f16ce0a..bbd6cff 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -230,7 +230,7 @@ pub struct GCfuncC { } #[repr(C, packed)] -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy)] pub struct GCfuncL { pub header: GCFuncHeader, pub uvptr: [GCRef; 1], From 3c433e3470107c0cc7d4344570756f217f444549 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:45:57 -0800 Subject: [PATCH 17/22] Change upvalue replacement to create a TValue pointer as opposed to overwriting the TValue in place. --- .../src/functions/detour/lua/upvalue.rs | 17 ++++++++++++----- packages/autorun-luajit/src/types/common.rs | 12 +++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/autorun-env/src/functions/detour/lua/upvalue.rs b/packages/autorun-env/src/functions/detour/lua/upvalue.rs index 00db1a5..0281fd7 100644 --- a/packages/autorun-env/src/functions/detour/lua/upvalue.rs +++ b/packages/autorun-env/src/functions/detour/lua/upvalue.rs @@ -4,7 +4,7 @@ use crate::functions::detour::lua::state::OriginalDetourState; use anyhow::Context; -use autorun_luajit::{GCUpval, GCfuncL, TValue}; +use autorun_luajit::{GCRef, GCUpval, GCfuncL, MRef, TValue}; pub fn replace( func: &GCfuncL, @@ -33,18 +33,25 @@ pub fn overwrite_upvalue(func: &GCfuncL, target_index: u32, replacement_tv: TVal let target_uv = unsafe { target_uv_gcr .as_ptr::() - .as_ref() + .as_mut() .context("Failed to deref GCUpval.")? }; // #define uvval(uv_) (mref((uv_)->v, TValue)) + dbg!(&target_uv); + autorun_log::debug!("UV pointer: {:p}", target_uv.v.as_ptr::()); + autorun_log::debug!("Actual GCUpval pointer: {:p}", target_uv); + let tvalue_ptr = unsafe { target_uv.v.as_mut_ptr::() }; let original_tv = unsafe { std::ptr::read(tvalue_ptr) }; - // We want to *actually* overwrite the TValue at this location, not just reassign the pointer + // We actually re-assign the pointer. We do not want to do it in-place as it will affect even clones of the function. + // Unfortunately, it's a tad ugly. And also leaks memory. We can fix that later. unsafe { - std::ptr::write(tvalue_ptr, replacement_tv); - }; + let new_tv = Box::new(replacement_tv); + let new_tv_ptr = Box::into_raw(new_tv); + target_uv.v.set_ptr(new_tv_ptr); + } Ok(original_tv) } diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index bbd6cff..bd9b2b5 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -426,7 +426,7 @@ pub struct Node { } #[repr(C)] -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy)] pub struct GCUpvalUVLink { pub next: GCRef, pub prev: GCRef, @@ -438,7 +438,17 @@ pub union GCUpvalUV { pub link: GCUpvalUVLink, } +impl Debug for GCUpvalUV { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // we can't know which field is valid here, so just print both + write!(f, "GCUpvalUV {{ tv: {:?}, link: {:?} }}", unsafe { self.tv }, unsafe { + self.link + }) + } +} + #[repr(C)] +#[derive(Debug)] pub struct GCUpval { pub header: GCHeader, pub closed: u8, From 7e978b2c69ec3e8e150b1b560f75d5cf20cd4c40 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:14:07 -0800 Subject: [PATCH 18/22] Add deep upvalue cloning --- .../src/functions/detour/lua/clone.rs | 63 ++++++++++++++++++- packages/autorun-luajit/src/types/common.rs | 4 ++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/autorun-env/src/functions/detour/lua/clone.rs b/packages/autorun-env/src/functions/detour/lua/clone.rs index 975fc89..d6b43fb 100644 --- a/packages/autorun-env/src/functions/detour/lua/clone.rs +++ b/packages/autorun-env/src/functions/detour/lua/clone.rs @@ -3,7 +3,10 @@ //! including its upvalues, bytecode, and other internal structures. use anyhow::Context; -use autorun_luajit::{BCIns, GCHeader, GCProto, GCRef, GCSize, GCfunc, GCfuncL, LJState, TValue, mem_newgco, push_tvalue}; +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. @@ -60,6 +63,8 @@ pub fn clone(lj_state: &mut LJState, target_func: &GCfuncL) -> anyhow::Result<() (*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); @@ -92,3 +97,59 @@ pub fn fixup_proto_offsets(original_proto: *mut GCProto, new_proto: *mut GCProto 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); + + 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_ptr::() }; + let new_upvalue_ptr = unsafe { mem_newgco::(lj_state, size_of::() as GCSize)? }; + + autorun_log::debug!("Original upvalue pointer: {:p}", upvalue); + autorun_log::debug!("New upvalue pointer: {:p}", new_upvalue_ptr); + + // copy excluding GCHeader + unsafe { + std::ptr::copy_nonoverlapping( + (upvalue as *const u8).byte_add(size_of::()), + (new_upvalue_ptr as *mut u8).byte_add(size_of::()), + size_of::() - size_of::(), + ); + } + + // Close it out + close_upvalue(new_upvalue_ptr)?; + + // update the reference to point to the new upvalue + upvalue_gcr.set_ptr(new_upvalue_ptr); + autorun_log::debug!("Upvalue {} cloned.", i); + } + + Ok(()) +} + +fn close_upvalue(new_upvalue_ptr: *mut GCUpval) -> anyhow::Result<()> { + unsafe { + let new_upvalue = new_upvalue_ptr.as_mut().context("Failed to deref new upvalue.")?; + new_upvalue.closed = 1; + + // copy TV from wherever it was pointing to + new_upvalue.uv.tv = new_upvalue.v.as_ptr::().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(()) +} diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index bd9b2b5..76fcf2f 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -101,6 +101,10 @@ impl GCRef { pub fn as_ptr(&self) -> *mut T { (self.gcptr64 & LJ_GCVMASK) as *mut T } + + pub fn set_ptr(&mut self, ptr: *mut T) { + self.gcptr64 = (ptr as u64) & LJ_GCVMASK; + } } impl PartialEq for GCRef { From fe9a3c65583cdc86bae48b94790a86e51a205c14 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:02:30 -0800 Subject: [PATCH 19/22] Fix a major bug causing tons of problems with upvalue clones --- packages/autorun-env/src/functions/detour.rs | 7 ++--- .../src/functions/detour/lua/clone.rs | 28 ++++++++++++++++--- .../src/functions/detour/lua/upvalue.rs | 4 +-- packages/autorun-luajit/src/helpers.rs | 8 ++++++ packages/autorun-luajit/src/types/common.rs | 9 ++++-- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/packages/autorun-env/src/functions/detour.rs b/packages/autorun-env/src/functions/detour.rs index 812f9f0..938c5eb 100644 --- a/packages/autorun-env/src/functions/detour.rs +++ b/packages/autorun-env/src/functions/detour.rs @@ -10,7 +10,7 @@ use crate::functions::detour::userdata::Detour; use anyhow::Context; use autorun_lua::{LuaApi, LuaCFunction, LuaTypeId, RawHandle, RawLuaReturn}; use autorun_luajit::bytecode::{BCWriter, Op}; -use autorun_luajit::{BCIns, GCfunc, LJState, get_gcobj, get_gcobj_mut, index2adr}; +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; @@ -193,9 +193,8 @@ pub fn clone_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> let lj_state = state as *mut LJState; let lj_state = unsafe { lj_state.as_mut().context("Failed to dereference LJState.")? }; - let gcfunc = get_gcobj::(lj_state, 1).context("Failed to get GCfunc for target function.")?; - let gcfunc_l = gcfunc.as_l().context("Target function must be a Lua function.")?; - + let gcfunc = get_gcobj_ptr::(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)) } diff --git a/packages/autorun-env/src/functions/detour/lua/clone.rs b/packages/autorun-env/src/functions/detour/lua/clone.rs index d6b43fb..6b616c0 100644 --- a/packages/autorun-env/src/functions/detour/lua/clone.rs +++ b/packages/autorun-env/src/functions/detour/lua/clone.rs @@ -10,10 +10,10 @@ 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: &GCfuncL) -> anyhow::Result<()> { +pub fn clone(lj_state: &mut LJState, target_func: *mut GCfuncL) -> anyhow::Result<()> { dbg!(&target_func); // Proto must be cloned first. - let proto = target_func.get_proto()?; + let proto = unsafe { (*target_func).get_proto()? }; let proto_size = unsafe { (*proto).sizept } as GCSize; let proto_uv_size = unsafe { (*proto).sizeuv } as GCSize; @@ -47,7 +47,6 @@ pub fn clone(lj_state: &mut LJState, target_func: &GCfuncL) -> anyhow::Result<() unsafe { let target_func = target_func as *const GCfuncL as *const u8; - dbg!(&target_func); std::ptr::copy_nonoverlapping( target_func.byte_add(size_of::()), @@ -106,18 +105,37 @@ pub fn clone_upvalue_list(lj_state: &mut LJState, func: *mut GCfunc) -> anyhow:: 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_ptr::() }; + let upvalue = unsafe { upvalue_gcr.as_direct_ptr::() }; + if upvalue.is_null() { + anyhow::bail!("Upvalue pointer is null."); + } + let new_upvalue_ptr = unsafe { mem_newgco::(lj_state, size_of::() 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( @@ -141,9 +159,11 @@ pub fn clone_upvalue_list(lj_state: &mut LJState, func: *mut GCfunc) -> anyhow:: fn close_upvalue(new_upvalue_ptr: *mut GCUpval) -> 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::()); new_upvalue.uv.tv = new_upvalue.v.as_ptr::().read(); // point v to the new TV new_upvalue diff --git a/packages/autorun-env/src/functions/detour/lua/upvalue.rs b/packages/autorun-env/src/functions/detour/lua/upvalue.rs index 0281fd7..e8a2b17 100644 --- a/packages/autorun-env/src/functions/detour/lua/upvalue.rs +++ b/packages/autorun-env/src/functions/detour/lua/upvalue.rs @@ -28,8 +28,8 @@ pub fn overwrite_upvalue(func: &GCfuncL, target_index: u32, replacement_tv: TVal ); } - let upvalue_array_ptr = func.uvptr.as_ptr(); - let target_uv_gcr = unsafe { *upvalue_array_ptr.add(target_index as usize) }; + let upvalue_array_ptr = func.uvptr; + let target_uv_gcr = unsafe { *upvalue_array_ptr.as_ptr().add(target_index as usize) }; let target_uv = unsafe { target_uv_gcr .as_ptr::() diff --git a/packages/autorun-luajit/src/helpers.rs b/packages/autorun-luajit/src/helpers.rs index cfb8b2b..0001dbb 100644 --- a/packages/autorun-luajit/src/helpers.rs +++ b/packages/autorun-luajit/src/helpers.rs @@ -54,6 +54,14 @@ pub fn get_gcobj_mut(state: &mut LJState, idx: i32) -> anyhow::Re } } +pub fn get_gcobj_ptr(state: &LJState, idx: i32) -> anyhow::Result<*mut T> { + unsafe { + let tv = index2adr(state, idx).context("Failed to get TValue for given index.")?; + let gcobj_ptr = (*tv).as_ptr::()?; + Ok(gcobj_ptr) + } +} + pub fn push_tvalue(state: &mut LJState, tvalue: &TValue) { unsafe { std::ptr::write(state.top, *tvalue); diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index 76fcf2f..42f86d8 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -102,6 +102,10 @@ impl GCRef { (self.gcptr64 & LJ_GCVMASK) as *mut T } + pub fn as_direct_ptr(&self) -> *mut T { + self.gcptr64 as *mut T // without masking + } + pub fn set_ptr(&mut self, ptr: *mut T) { self.gcptr64 = (ptr as u64) & LJ_GCVMASK; } @@ -218,10 +222,9 @@ pub struct GCFuncHeader { pub header: GCHeader, pub ffid: u8, pub nupvalues: u8, + pub _pad: [u8; 4], pub env: GCRef, pub gclist: GCRef, - // Compiler randomly adds 4 bytes of padding here for alignment, not too sure why since it is packed - pub _pad: [u8; 4], pub pc: MRef, } @@ -233,7 +236,7 @@ pub struct GCfuncC { pub upvalue: [TValue; 1], } -#[repr(C, packed)] +#[repr(C)] #[derive(Debug, Clone, Copy)] pub struct GCfuncL { pub header: GCFuncHeader, From ca1de7f2bcb44d8f7fdf88105aced62ce6b7417a Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:33:06 -0800 Subject: [PATCH 20/22] Clone upvalue before replacing, instead of writing to the upvalue in-place. This caused really nasty errors if upvalues were shared between protos, which most are if they're in the same source file. --- packages/autorun-env/src/env.rs | 4 +- packages/autorun-env/src/functions/detour.rs | 9 ++- .../src/functions/detour/lua/clone.rs | 11 ++- .../src/functions/detour/lua/state.rs | 2 +- .../src/functions/detour/lua/upvalue.rs | 72 +++++++++++-------- packages/autorun-luajit/src/types/common.rs | 4 +- 6 files changed, 61 insertions(+), 41 deletions(-) diff --git a/packages/autorun-env/src/env.rs b/packages/autorun-env/src/env.rs index ab62ab7..70ff8ef 100644 --- a/packages/autorun-env/src/env.rs +++ b/packages/autorun-env/src/env.rs @@ -108,9 +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, "testLua", wrap!(functions::test_lua)); + lua.set(state, &t, "detourLua", wrap!(functions::detour_lua)); lua.set(state, &t, "restoreLua", wrap!(functions::restore_lua)); - lua.set(state, &t, "cloneLua", wrap!(functions::clone_lua)); + lua.set(state, &t, "cloneFunction", wrap!(functions::clone_lua_function)); lua.set(state, &t, "VERSION", env!("CARGO_PKG_VERSION")); return t; diff --git a/packages/autorun-env/src/functions/detour.rs b/packages/autorun-env/src/functions/detour.rs index 938c5eb..943dd82 100644 --- a/packages/autorun-env/src/functions/detour.rs +++ b/packages/autorun-env/src/functions/detour.rs @@ -122,7 +122,7 @@ pub fn copy_fast_function(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHa Ok(RawLuaReturn(1)) } -pub fn test_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result { +pub fn detour_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result { if lua.raw.typeid(state, 1) != LuaTypeId::Function { anyhow::bail!("First argument must be a function."); } @@ -131,6 +131,9 @@ pub fn test_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> a let lj_state = state as *mut LJState; let lj_state = unsafe { lj_state.as_mut().context("Failed to dereference LJState.")? }; let gcfunc = get_gcobj::(lj_state, 1).context("Failed to get GCfunc for target function.")?; + let gcfunc_ptr = get_gcobj_ptr::(lj_state, 1).context("Failed to get GCfunc pointer for target function.")?; + 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.")? }; @@ -156,7 +159,7 @@ pub fn test_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> a 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(gcfunc_l, 0, replacement_tv, &mut original_detour_state)?; + 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.")?; @@ -185,7 +188,7 @@ pub fn restore_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) - Ok(RawLuaReturn(0)) } -pub fn clone_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result { +pub fn clone_lua_function(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result { if lua.raw.typeid(state, 1) != LuaTypeId::Function { anyhow::bail!("First argument must be a function."); } diff --git a/packages/autorun-env/src/functions/detour/lua/clone.rs b/packages/autorun-env/src/functions/detour/lua/clone.rs index 6b616c0..e453707 100644 --- a/packages/autorun-env/src/functions/detour/lua/clone.rs +++ b/packages/autorun-env/src/functions/detour/lua/clone.rs @@ -146,7 +146,7 @@ pub fn clone_upvalue_list(lj_state: &mut LJState, func: *mut GCfunc) -> anyhow:: } // Close it out - close_upvalue(new_upvalue_ptr)?; + close_upvalue(new_upvalue_ptr, None)?; // update the reference to point to the new upvalue upvalue_gcr.set_ptr(new_upvalue_ptr); @@ -156,7 +156,7 @@ pub fn clone_upvalue_list(lj_state: &mut LJState, func: *mut GCfunc) -> anyhow:: Ok(()) } -fn close_upvalue(new_upvalue_ptr: *mut GCUpval) -> anyhow::Result<()> { +pub fn close_upvalue(new_upvalue_ptr: *mut GCUpval, replacement_value: Option) -> anyhow::Result<()> { unsafe { let new_upvalue = new_upvalue_ptr.as_mut().context("Failed to deref new upvalue.")?; dbg!(&new_upvalue); @@ -164,7 +164,12 @@ fn close_upvalue(new_upvalue_ptr: *mut GCUpval) -> anyhow::Result<()> { // copy TV from wherever it was pointing to autorun_log::debug!("Reading original TV pointer: {:p}", new_upvalue.v.as_ptr::()); - new_upvalue.uv.tv = new_upvalue.v.as_ptr::().read(); + new_upvalue.uv.tv = if let Some(replacement) = replacement_value { + replacement + } else { + new_upvalue.v.as_ptr::().read() + }; + // point v to the new TV new_upvalue .v diff --git a/packages/autorun-env/src/functions/detour/lua/state.rs b/packages/autorun-env/src/functions/detour/lua/state.rs index d578e24..e18ab1b 100644 --- a/packages/autorun-env/src/functions/detour/lua/state.rs +++ b/packages/autorun-env/src/functions/detour/lua/state.rs @@ -42,7 +42,7 @@ pub fn restore_func(func: *mut GCfunc) -> anyhow::Result<()> { // Fix upvalues autorun_log::debug!("Fixing upvalues..."); - overwrite_upvalue(gcfunc_l, 0, state.original_upvalue_0)?; + //overwrite_upvalue(gcfunc_l, 0, state.original_upvalue_0)?; autorun_log::debug!("Upvalues fixed."); // Restore bytecode and frame size diff --git a/packages/autorun-env/src/functions/detour/lua/upvalue.rs b/packages/autorun-env/src/functions/detour/lua/upvalue.rs index e8a2b17..231705e 100644 --- a/packages/autorun-env/src/functions/detour/lua/upvalue.rs +++ b/packages/autorun-env/src/functions/detour/lua/upvalue.rs @@ -2,56 +2,70 @@ //! This module provides the ability to overwrite upvalues of Lua functions, //! which is essential for our detouring mechanism to pull the target function into a register +use crate::functions::detour::lua::clone::close_upvalue; use crate::functions::detour::lua::state::OriginalDetourState; use anyhow::Context; -use autorun_luajit::{GCRef, GCUpval, GCfuncL, MRef, TValue}; +use autorun_luajit::{GCHeader, GCRef, GCSize, GCUpval, GCfuncL, LJState, MRef, TValue, mem_newgco}; pub fn replace( - func: &GCfuncL, + lj_state: &mut LJState, + func: *mut GCfuncL, target_index: u32, replacement_tv: TValue, original_detour_state: &mut OriginalDetourState, ) -> anyhow::Result<()> { - let old_uv = overwrite_upvalue(func, target_index, replacement_tv)?; + let old_uv = overwrite_upvalue(lj_state, func, target_index, replacement_tv)?; // TODO: Support multiple upvalues in the future original_detour_state.original_upvalue_0 = old_uv; Ok(()) } -pub fn overwrite_upvalue(func: &GCfuncL, target_index: u32, replacement_tv: TValue) -> anyhow::Result { - if target_index >= func.header.nupvalues as u32 { +pub fn overwrite_upvalue( + lj_state: &mut LJState, + func: *mut GCfuncL, + target_index: u32, + replacement_tv: TValue, +) -> anyhow::Result { + let nupvalues = unsafe { (*func).header.nupvalues }; + + if target_index >= nupvalues as u32 { anyhow::bail!( "Upvalue replacement index out of bounds: target_index {} exceeds number of upvalues {}.", target_index, - func.header.nupvalues + nupvalues ); } - let upvalue_array_ptr = func.uvptr; - let target_uv_gcr = unsafe { *upvalue_array_ptr.as_ptr().add(target_index as usize) }; - let target_uv = unsafe { - target_uv_gcr - .as_ptr::() - .as_mut() - .context("Failed to deref GCUpval.")? - }; - - // #define uvval(uv_) (mref((uv_)->v, TValue)) - dbg!(&target_uv); - autorun_log::debug!("UV pointer: {:p}", target_uv.v.as_ptr::()); - autorun_log::debug!("Actual GCUpval pointer: {:p}", target_uv); - - let tvalue_ptr = unsafe { target_uv.v.as_mut_ptr::() }; - let original_tv = unsafe { std::ptr::read(tvalue_ptr) }; - - // We actually re-assign the pointer. We do not want to do it in-place as it will affect even clones of the function. - // Unfortunately, it's a tad ugly. And also leaks memory. We can fix that later. + let mut upvalue_array_ptr = unsafe { (*func).uvptr.as_mut_ptr() }; + + let target_uv_gcr = unsafe { *upvalue_array_ptr.add(target_index as usize) }; + // We create a new GCupval to replace the existing one. + // We can't modify it in-place or it will affect any other shared upvalues. + // This is most prominent in a `hook.Call` detour (very common scenario). Modifiying in place would + // cause hook.Add to see the modified upvalue as well. + + let target_uv = unsafe { target_uv_gcr.as_direct_ptr::() }; + let new_target_uv = unsafe { mem_newgco::(lj_state, size_of::() as GCSize)? }; + + // copy + unsafe { + std::ptr::copy_nonoverlapping( + target_uv.byte_add(size_of::()) as *const u8, + new_target_uv.byte_add(size_of::()) as *mut u8, + size_of::() - size_of::(), + ); + } + + // close it, and replace the value + close_upvalue(new_target_uv, Some(replacement_tv))?; + + // update the GCRef to point to the new upvalue unsafe { - let new_tv = Box::new(replacement_tv); - let new_tv_ptr = Box::into_raw(new_tv); - target_uv.v.set_ptr(new_tv_ptr); + upvalue_array_ptr + .add(target_index as usize) + .write(GCRef::from_ptr(new_target_uv as *mut ())); } - Ok(original_tv) + Ok(TValue::nil()) } diff --git a/packages/autorun-luajit/src/types/common.rs b/packages/autorun-luajit/src/types/common.rs index 42f86d8..d1dc47e 100644 --- a/packages/autorun-luajit/src/types/common.rs +++ b/packages/autorun-luajit/src/types/common.rs @@ -92,9 +92,7 @@ pub struct GCRef { impl GCRef { pub fn from_ptr(ptr: *mut T) -> Self { - Self { - gcptr64: (ptr as u64) & LJ_GCVMASK, - } + Self { gcptr64: ptr as u64 } } // equivalent to the gcref macro in LuaJIT From 903afe968cbece2cf95eadd03f93599ee7502dc8 Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:12:17 -0800 Subject: [PATCH 21/22] Update dhash --- packages/autorun-env/src/functions/detour/lua/upvalue.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/autorun-env/src/functions/detour/lua/upvalue.rs b/packages/autorun-env/src/functions/detour/lua/upvalue.rs index 231705e..79438db 100644 --- a/packages/autorun-env/src/functions/detour/lua/upvalue.rs +++ b/packages/autorun-env/src/functions/detour/lua/upvalue.rs @@ -59,6 +59,7 @@ pub fn overwrite_upvalue( // close it, and replace the value close_upvalue(new_target_uv, Some(replacement_tv))?; + unsafe { (*new_target_uv).dhash = 0x41414141 }; // for debugging purposes // update the GCRef to point to the new upvalue unsafe { From 03f450b2663f503829a996fa3a48e252614ad66c Mon Sep 17 00:00:00 2001 From: yogwoggf <100450992+yogwoggf@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:15:26 -0800 Subject: [PATCH 22/22] Add an experimental method of detouring upvalue-less functions by forcing one --- packages/autorun-env/src/functions/detour.rs | 11 +++++++++++ .../src/functions/detour/lua/trampoline.rs | 18 ++++++++++++++++-- .../src/functions/detour/lua/upvalue.rs | 10 +++------- packages/autorun-luajit/src/bytecode/writer.rs | 4 ++++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/autorun-env/src/functions/detour.rs b/packages/autorun-env/src/functions/detour.rs index 943dd82..690bfc4 100644 --- a/packages/autorun-env/src/functions/detour.rs +++ b/packages/autorun-env/src/functions/detour.rs @@ -132,6 +132,7 @@ pub fn detour_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> let lj_state = unsafe { lj_state.as_mut().context("Failed to dereference LJState.")? }; let gcfunc = get_gcobj::(lj_state, 1).context("Failed to get GCfunc for target function.")?; let gcfunc_ptr = get_gcobj_ptr::(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.")?; @@ -156,6 +157,16 @@ pub fn detour_lua(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> 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(); diff --git a/packages/autorun-env/src/functions/detour/lua/trampoline.rs b/packages/autorun-env/src/functions/detour/lua/trampoline.rs index 31f4cf7..a204905 100644 --- a/packages/autorun-env/src/functions/detour/lua/trampoline.rs +++ b/packages/autorun-env/src/functions/detour/lua/trampoline.rs @@ -5,7 +5,6 @@ use anyhow::Context; use autorun_luajit::bytecode::{BCWriter, Op}; use autorun_luajit::{BCIns, GCProto, GCfuncL, ProtoFlags}; -const MINIMUM_BYTECODES: u32 = 15; const MINIMUM_UPVALUES: u8 = 1; /// Assumes detour function is in UV 0. @@ -19,7 +18,20 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL, original_detour_state: &mut let proto = gcfunc_l.get_proto().context("Failed to get proto from GCfuncL")?; let proto = unsafe { proto.as_mut().context("Failed to dereference proto")? }; - if proto.sizebc < MINIMUM_BYTECODES { + let min_required_sizebc = if proto.has_flag(ProtoFlags::Vararg) { + 1 // FUNCV + + 1 // UGET + + proto.numparams as u32 + + 1 // VARG + + 1 // CALLMT + } else { + 1 // FUNCF + + 1 // UGET + + proto.numparams as u32 + + 1 // CALLT + }; + + if proto.sizebc < min_required_sizebc { anyhow::bail!("Target function's proto is too small to overwrite with trampoline."); } @@ -43,6 +55,8 @@ pub fn overwrite_with_trampoline(gcfunc_l: &GCfuncL, original_detour_state: &mut write_trampoline_bytecode(&mut writer, nargs, maxslots)?; } + autorun_log::debug!("Total trampoline bytecodes written: {}", writer.total_written()); + // all done, no return necessary as CALLT handles it // CALLT also jumps directly to the function, so no need for us to fix up // the sizebc field diff --git a/packages/autorun-env/src/functions/detour/lua/upvalue.rs b/packages/autorun-env/src/functions/detour/lua/upvalue.rs index 79438db..14d9144 100644 --- a/packages/autorun-env/src/functions/detour/lua/upvalue.rs +++ b/packages/autorun-env/src/functions/detour/lua/upvalue.rs @@ -48,18 +48,14 @@ pub fn overwrite_upvalue( let target_uv = unsafe { target_uv_gcr.as_direct_ptr::() }; let new_target_uv = unsafe { mem_newgco::(lj_state, size_of::() as GCSize)? }; - // copy + // no copy needed, its simple enough to just fill it out. unsafe { - std::ptr::copy_nonoverlapping( - target_uv.byte_add(size_of::()) as *const u8, - new_target_uv.byte_add(size_of::()) as *mut u8, - size_of::() - size_of::(), - ); + (*new_target_uv).immutable = 0; + (*new_target_uv).dhash = 0x41414141; } // close it, and replace the value close_upvalue(new_target_uv, Some(replacement_tv))?; - unsafe { (*new_target_uv).dhash = 0x41414141 }; // for debugging purposes // update the GCRef to point to the new upvalue unsafe { diff --git a/packages/autorun-luajit/src/bytecode/writer.rs b/packages/autorun-luajit/src/bytecode/writer.rs index ae3a6e8..4d32cb1 100644 --- a/packages/autorun-luajit/src/bytecode/writer.rs +++ b/packages/autorun-luajit/src/bytecode/writer.rs @@ -69,4 +69,8 @@ impl BCWriter { pub fn get_ptr(&self) -> *mut BCIns { self.ptr } + + pub fn total_written(&self) -> usize { + self.offset + } }