Skip to content

Commit 34ee87a

Browse files
authored
#49 - Proto authorization
49 - Proto authorization
2 parents 4f13e25 + 26ff7c8 commit 34ee87a

File tree

8 files changed

+176
-11
lines changed

8 files changed

+176
-11
lines changed

.editorconfig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
root = true
2+
3+
[*]
4+
indent_style = tab
5+
indent_size = 4
6+
max_line_length = 128

packages/autorun-env/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ libloading = { workspace = true }
1111
anyhow = { workspace = true }
1212
retour = { workspace = true }
1313
cap-std = { workspace = true }
14+
rand = "0.8.5"
1415

1516
autorun-core = { path = "../autorun-core" }
1617
autorun-types = { path = "../autorun-types" }

packages/autorun-env/api.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,42 @@
346346
}
347347
]
348348
},
349+
{
350+
"name": "isFunctionAuthorized",
351+
"description": "Checks if a given function or stack level is authorized by the Autorun environment.",
352+
"realm": "shared",
353+
"parameters": [
354+
{
355+
"name": "funcOrLevel",
356+
"type": "function | number",
357+
"description": "The function to check or the stack level"
358+
}
359+
],
360+
"returns": [
361+
{
362+
"type": "boolean",
363+
"description": "True if the function/level is authorized, false otherwise"
364+
}
365+
]
366+
},
367+
{
368+
"name": "isProtoAuthorized",
369+
"description": "Checks if a given prototype is authorized by the Autorun environment. This can be used to verify if protos from VM events are from Autorun or not.",
370+
"realm": "shared",
371+
"parameters": [
372+
{
373+
"name": "proto",
374+
"type": "proto",
375+
"description": "The prototype to check"
376+
}
377+
],
378+
"returns": [
379+
{
380+
"type": "boolean",
381+
"description": "True if the prototype is authorized, false otherwise"
382+
}
383+
]
384+
},
349385
{
350386
"name": "load",
351387
"description": "Compiles a Lua string into a callable function without executing it. Similar to Lua's loadstring/load function. Use this to dynamically compile code at runtime. NOTE: The environment inside defaults to the global environment, NOT Autorun's environment.",

packages/autorun-env/src/env.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ use autorun_log::*;
66
use autorun_lua::{LuaApi, RawHandle};
77
use autorun_luajit::{GCRef, LJState, index2adr};
88
use autorun_types::{LuaState, Realm};
9-
use std::ffi::{CStr, c_int};
9+
use std::ffi::{CStr, CString, c_int};
1010

1111
#[derive(Debug, Clone, Copy)]
1212
pub struct EnvHandle {
1313
realm: Realm,
1414
env_gcr: GCRef,
15+
chunk_nonce: u64,
1516
handle: RawHandle,
1617
}
1718

@@ -165,13 +166,18 @@ impl EnvHandle {
165166
lua.push(state, as_env_lua_function!(crate::functions::is_function_authorized));
166167
lua.set_table(state, -3);
167168

169+
lua.push(state, c"isProtoAuthorized");
170+
lua.push(state, as_env_lua_function!(crate::functions::is_proto_authorized));
171+
lua.set_table(state, -3);
172+
168173
lua.push(state, c"VERSION");
169174
lua.push(state, env!("CARGO_PKG_VERSION").to_string());
170175
lua.set_table(state, -3);
171176
}
172177

173178
pub fn execute(&self, lua: &LuaApi, state: *mut LuaState, name: &CStr, src: &[u8]) -> anyhow::Result<()> {
174-
if let Err(why) = lua.load_buffer_x(state, src, name, c"t") {
179+
let name = self.format_chunk_name(name)?;
180+
if let Err(why) = lua.load_buffer_x(state, src, &name, c"t") {
175181
anyhow::bail!("Failed to compile: {why}");
176182
}
177183

@@ -206,7 +212,25 @@ impl EnvHandle {
206212
let env_gcr = unsafe { (*env_tvalue).gcr };
207213

208214
let handle = RawHandle::from_stack(lua, state).unwrap();
209-
Ok(Self { realm, env_gcr, handle })
215+
let chunk_nonce = rand::random::<u64>();
216+
Ok(Self {
217+
realm,
218+
env_gcr,
219+
chunk_nonce,
220+
handle,
221+
})
222+
}
223+
224+
pub fn format_chunk_name(&self, base: &CStr) -> anyhow::Result<CString> {
225+
let formatted = format!("{}-{}", self.chunk_nonce, base.to_str()?);
226+
Ok(CString::new(formatted)?)
227+
}
228+
229+
pub fn is_chunk_name_authorized(&self, chunk_name: &CStr) -> bool {
230+
match chunk_name.to_str() {
231+
Ok(name_str) => name_str.starts_with(&self.chunk_nonce.to_string()),
232+
Err(_) => false,
233+
}
210234
}
211235

212236
fn push_autorun_table(&self, lua: &LuaApi, state: *mut LuaState) {

packages/autorun-env/src/functions/auth.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::Context;
22
use autorun_lua::{DebugInfo, LuaApi};
3-
use autorun_luajit::{Frame, LJState, push_tvalue};
3+
use autorun_luajit::{Frame, GCProto, LJ_TPROTO, LJState, get_gcobj, index2adr, push_tvalue};
44
use autorun_types::LuaState;
55

66
pub fn is_function_authorized(lua: &LuaApi, state: *mut LuaState, env: crate::EnvHandle) -> anyhow::Result<bool> {
@@ -34,3 +34,23 @@ pub fn is_function_authorized(lua: &LuaApi, state: *mut LuaState, env: crate::En
3434
env.is_function_authorized(lua, state, None)
3535
.context("Failed to check function authorization.")
3636
}
37+
38+
pub fn is_proto_authorized(_lua: &LuaApi, state: *mut LuaState, env: crate::EnvHandle) -> anyhow::Result<bool> {
39+
// Protos dont play nice with the usual public API types, so we just have to do it manually
40+
let lj_state = state as *mut LJState;
41+
let lj_state = unsafe { lj_state.as_ref().context("Failed to dereference LJState")? };
42+
let proto_tv = index2adr(lj_state, 1).context("Failed to get TValue for given index.")?;
43+
let proto_tv = unsafe { &*proto_tv };
44+
45+
// TODO: When the stack spoof PR is merged, replace this with the new type check helper
46+
if proto_tv.itype() != LJ_TPROTO {
47+
anyhow::bail!("First argument must be a proto.");
48+
}
49+
50+
let proto_gc = get_gcobj::<GCProto>(lj_state, 1).context("Failed to get GCProto for given index.")?;
51+
let proto_chunk_name = proto_gc.chunk_name_str().context("Failed to get chunk name from proto.")?;
52+
let proto_chunk_name_cstr =
53+
std::ffi::CString::new(proto_chunk_name.clone()).context("Failed to convert chunk name to CString.")?;
54+
55+
Ok(env.is_chunk_name_authorized(&proto_chunk_name_cstr))
56+
}

packages/autorun-env/src/functions/load.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use autorun_lua::{LuaApi, RawLuaReturn};
22
use autorun_types::LuaState;
33

4-
pub fn load(lua: &LuaApi, state: *mut LuaState, _env: crate::EnvHandle) -> anyhow::Result<RawLuaReturn> {
4+
pub fn load(lua: &LuaApi, state: *mut LuaState, env: crate::EnvHandle) -> anyhow::Result<RawLuaReturn> {
55
let source = lua.check_string(state, 1);
66
let chunk_name = lua.to::<Option<&[u8]>>(state, 2).unwrap_or(b"loadstring");
77
let chunk_name = std::ffi::CString::new(chunk_name)?;
8+
let chunk_name = env.format_chunk_name(&chunk_name)?;
89

910
if let Err(why) = lua.load_buffer_x(state, source.as_bytes(), &chunk_name, c"t") {
1011
lua.push_nil(state);

packages/autorun-luajit/src/types/common.rs

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Subset of lj_obj.h
22

3+
use anyhow::Context;
34
use std::ffi::{c_int, c_void};
45

56
// IMPORTANT: GMod's LUA_IDSIZE was randomly changed to 128 instead of 60 like in vanilla LuaJIT
@@ -27,7 +28,7 @@ pub trait IntoLJType {
2728
const LJ_TYPE: u32;
2829
}
2930

30-
pub type MSize = u64;
31+
pub type MSize = u32;
3132
pub type GCSize = u64;
3233

3334
#[repr(C)]
@@ -276,15 +277,38 @@ pub struct GCState {
276277
pub pause: MSize,
277278
}
278279

279-
#[repr(C)]
280+
/// NOTE: Incompatibility with LuaJIT 2.1 here.
281+
/// 2.1 has the 'sid' field, while GMod's 2.1.0-beta3 does not.
282+
/// 2.1's hash field is a uint32_t, while GMod's is a MSize.
283+
#[repr(C, packed)]
280284
pub struct GCstr {
281285
pub header: GCHeader,
282-
pub udtype: u8,
286+
pub reserved: u8,
283287
pub unused: u8,
284-
pub env: GCRef,
288+
pub hash: MSize,
285289
pub len: MSize,
286-
pub metatable: GCRef,
287-
pub align1: u32,
290+
pub _padding: u32, // The two bytes (reserved + unused) causes major misalignment, so we need padding here
291+
}
292+
293+
impl GCstr {
294+
fn data(&self) -> *const u8 {
295+
// payload is stored immediately after the GCstr struct
296+
unsafe { (self as *const GCstr).add(1) as *const u8 }
297+
}
298+
299+
fn as_bytes(&self) -> &[u8] {
300+
unsafe { std::slice::from_raw_parts(self.data(), self.len as usize) }
301+
}
302+
303+
pub fn as_str(&self) -> anyhow::Result<&str> {
304+
if self.len == 0 || self.data().is_null() {
305+
return Ok("");
306+
}
307+
308+
let bytes = self.as_bytes();
309+
let s = std::str::from_utf8(bytes).context("GCstr contains invalid UTF-8 data")?;
310+
Ok(s)
311+
}
288312
}
289313

290314
impl IntoLJType for GCstr {
@@ -327,6 +351,50 @@ impl IntoLJType for GCUpval {
327351

328352
pub type BCIns = u32;
329353

354+
pub type BCLine = u32;
355+
356+
#[repr(C)]
357+
#[derive(Debug, Clone, Copy)]
358+
pub struct GCProto {
359+
header: GCHeader,
360+
pub numparams: u8,
361+
pub framesize: u8,
362+
pub sizebc: MSize,
363+
pub unused: u32,
364+
pub gclist: GCRef,
365+
pub k: MRef,
366+
pub uv: MRef,
367+
pub sizekgc: MSize,
368+
pub sizekn: MSize,
369+
pub sizept: MSize,
370+
pub sizeuv: u8,
371+
pub flags: u8,
372+
pub trace: u16,
373+
pub chunkname: GCRef,
374+
pub firstline: BCLine,
375+
pub numline: BCLine,
376+
pub lineinfo: MRef,
377+
pub uvinfo: MRef,
378+
pub varinfo: MRef,
379+
}
380+
381+
impl GCProto {
382+
pub fn chunk_name_str(&self) -> anyhow::Result<&str> {
383+
let chunk_name = unsafe {
384+
self.chunkname
385+
.as_ptr::<GCstr>()
386+
.as_ref()
387+
.context("Failed to dereference chunk name GCstr")?
388+
};
389+
390+
chunk_name.as_str()
391+
}
392+
}
393+
394+
impl IntoLJType for GCProto {
395+
const LJ_TYPE: u32 = LJ_TPROTO;
396+
}
397+
330398
// Metamethod enum
331399
#[repr(u8)]
332400
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

plugins/std/src/shared/builtins.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ bit = {
9898
ror = _G.bit.ror,
9999
}
100100

101+
jit = {
102+
attach = _G.jit.attach,
103+
flush = _G.jit.flush,
104+
on = _G.jit.on,
105+
off = _G.jit.off,
106+
status = _G.jit.status,
107+
version = _G.jit.version,
108+
}
109+
101110
assert = _G.assert
102111
collectgarbage = _G.collectgarbage
103112
dofile = _G.dofile

0 commit comments

Comments
 (0)