From 6bd2172ba7b59a1f7e7cc5b238e925b94dba4cfb Mon Sep 17 00:00:00 2001 From: nhkeni Date: Fri, 29 Apr 2022 19:46:33 -0400 Subject: [PATCH] Lua sandbox This is enough to prevent abuse by denying access to functions and denial of service (RAM and instruction step limits), but not enough to allow restricted use of things that require finer control (e.g. filesystem access). If something goes wrong, the whole thing can be turned off, for now, in config.h (see NHL_SANDBOX). None of the current functionality requires changes to build systems; some of the possible future functionality may require some #defines - TBD. There is lots of dead code (#ifdef notyet) for bits of that additional functionality; we can rip it out if we don't want those additions or we can complete (parts of) it depending on our needs. All current uses of Lua are connected to sandboxes and guarded with nhl_pcall (sandbox and lua_pcall wrapper); options and limits can be set at the callsites in the passed nhl_sandbox_info. Some of the error handling may be wrong - panic() vs. impossible() vs silence. Memory and instruction step limits should be tuned prior to release; there's no point tuning them now. --- include/config.h | 4 + include/extern.h | 6 +- include/global.h | 61 +++++ src/allmain.c | 2 +- src/cmd.c | 3 +- src/dungeon.c | 3 +- src/mklev.c | 7 +- src/nhlsel.c | 7 +- src/nhlua.c | 690 +++++++++++++++++++++++++++++++++++++++++++++-- src/questpgr.c | 3 +- src/sp_lev.c | 23 +- 11 files changed, 779 insertions(+), 30 deletions(-) diff --git a/include/config.h b/include/config.h index 76c9da0fd..ac08772ab 100644 --- a/include/config.h +++ b/include/config.h @@ -640,6 +640,10 @@ typedef unsigned char uchar; #define USE_ISAAC64 /* Use cross-plattform, bundled RNG */ +/* TEMPORARY - MAKE UNCONDITIONAL BEFORE RELEASE */ +/* undef this to check if sandbox breaks something */ +#define NHL_SANDBOX + /* End of Section 4 */ #ifdef TTY_TILES_ESCCODES diff --git a/include/extern.h b/include/extern.h index 6c37313d1..92e99f964 100644 --- a/include/extern.h +++ b/include/extern.h @@ -1773,10 +1773,11 @@ extern int l_obj_register(lua_State *); extern void l_nhcore_init(void); extern void l_nhcore_done(void); extern void l_nhcore_call(int); -extern lua_State * nhl_init(void); +extern lua_State * nhl_init(nhl_sandbox_info *); extern void nhl_done(lua_State *); extern boolean nhl_loadlua(lua_State *, const char *); -extern boolean load_lua(const char *); +extern int nhl_pcall(lua_State *, int, int); +extern boolean load_lua(const char *, nhl_sandbox_info *); extern void nhl_error(lua_State *, const char *) NORETURN; extern void lcheck_param_table(lua_State *); extern schar get_table_mapchr(lua_State *, const char *); @@ -1802,6 +1803,7 @@ extern int get_table_option(lua_State *, const char *, const char *, extern int str_lines_max_width(const char *); extern char *stripdigits(char *); extern const char *get_lua_version(void); +extern void nhl_pushhooked_open_table(lua_State *L); #endif /* !CROSSCOMPILE || CROSSCOMPILE_TARGET */ /* ### nhregex.c ### */ diff --git a/include/global.h b/include/global.h index 859aad1a4..b05e809c3 100644 --- a/include/global.h +++ b/include/global.h @@ -493,4 +493,65 @@ extern struct nomakedefs_s nomakedefs; #define LL_DUMP 0x4000L /* none of the above but should be in dumplog */ #define LL_DEBUG 0x8000L /* For debugging messages and other spam */ +/* + * Lua sandbox + */ +/* Control block for setting up a Lua state with nhl_init(). */ +typedef struct nhl_sandbox_info { + uint32_t flags; /* see below */ + uint32_t memlimit; /* approximate memory limit */ + uint32_t steps; /* instruction limit for state OR ... */ + uint32_t perpcall; /* ... instruction limit per nhl_pcall */ +} nhl_sandbox_info; + +/* For efficiency, we only check every NHL_SB_STEPSIZE instructions. */ +#ifndef NHL_SB_STEPSIZE +#define NHL_SB_STEPSIZE 1000 +#endif + +/* High level groups. Use these flags. */ + /* Safe functions. */ +#define NHL_SB_SAFE 0x80000000 + /* Access to Lua version information. */ +#define NHL_SB_VERSION 0x40000000 +#ifdef notyet + /* XXX These need to be replaced. */ +#define NHL_SB_CANREAD 0x20000000 +#define NHL_SB_CANWRITE 0x10000000 +#endif + /* Debugging library - highly unsafe. */ +#define NHL_SB_DEBUGGING 0x08000000 + /* Use with memlimit/steps/perpcall to get usage. */ +#define NHL_SB_REPORT 0x04000000 + /* As above, but do full gc on each nhl_pcall. */ +#define NHL_SB_REPORT2 0x02000000 + +/* Low level groups. If you need these, you probably need to define + * a new high level group instead. */ +#define NHL_SB_DB 0x00000001 +#define NHL_SB_STRING 0x00000002 +#define NHL_SB_TABLE 0x00000004 +#define NHL_SB_COROUTINE 0x00000008 +#define NHL_SB_MATH 0x00000010 +#define NHL_SB_UTF8 0x00000020 +#ifdef notyet +#define NHL_SB_PACKAGE 0x00000040 +#define NHL_SB_IO 0x00000080 +#define NHL_SB_OS 0x00000100 +#endif + +#define NHL_SB_BASEMASK 0x0001f000 +#define NHL_SB_BASE_BASE 0x00001000 +#define NHL_SB_BASE_ERROR 0x00002000 +#define NHL_SB_BASE_META 0x00004000 +#define NHL_SB_BASE_GC 0x00008000 +#define NHL_SB_BASE_UNSAFE 0x00010000 + +#define NHL_SB_ALL 0x0000ffff + +/* return codes */ +#define NHL_SBRV_DENY 1 +#define NHL_SBRV_ACCEPT 2 +#define NHL_SBRV_FAIL 3 + #endif /* GLOBAL_H */ diff --git a/src/allmain.c b/src/allmain.c index ef75a7eb6..7505ca3b4 100644 --- a/src/allmain.c +++ b/src/allmain.c @@ -680,7 +680,7 @@ newgame(void) * any artifacts */ u_init(); - l_nhcore_init(); + l_nhcore_init(); /* create a Lua state that lasts until the end of the game */ reset_glyphmap(gm_newgame); #ifndef NO_SIGNAL (void) signal(SIGINT, (SIG_RET_TYPE) done1); diff --git a/src/cmd.c b/src/cmd.c index 9a57f2bfa..19eb0655c 100644 --- a/src/cmd.c +++ b/src/cmd.c @@ -1161,6 +1161,7 @@ wiz_load_lua(void) { if (wizard) { char buf[BUFSZ]; + nhl_sandbox_info sbi = {NHL_SB_SAFE, 0, 0, 0}; buf[0] = '\0'; getlin("Load which lua file?", buf); @@ -1168,7 +1169,7 @@ wiz_load_lua(void) return 0; if (!strchr(buf, '.')) strcat(buf, ".lua"); - (void) load_lua(buf); + (void) load_lua(buf, &sbi); } else pline(unavailcmd, ecname_from_fn(wiz_load_lua)); return ECMD_OK; diff --git a/src/dungeon.c b/src/dungeon.c index e6a393482..b176b36fc 100644 --- a/src/dungeon.c +++ b/src/dungeon.c @@ -748,11 +748,12 @@ init_dungeons(void) struct proto_dungeon pd; struct level_map *lev_map; int tidx; + nhl_sandbox_info sbi = {NHL_SB_SAFE, 0, 0, 0}; (void) memset(&pd, 0, sizeof (struct proto_dungeon)); pd.n_levs = pd.n_brs = 0; - L = nhl_init(); + L = nhl_init(&sbi); /* private Lua state for this function */ if (!L) { panic1("'nhl_init' failed; can't continue."); /*NOTREACHED*/ diff --git a/src/mklev.c b/src/mklev.c index aeff642e6..4a7e35e51 100644 --- a/src/mklev.c +++ b/src/mklev.c @@ -265,10 +265,11 @@ makerooms(void) boolean tried_vault = FALSE; int themeroom_tries = 0; char *fname; + nhl_sandbox_info sbi = {NHL_SB_SAFE, 0, 0, 0}; lua_State *themes = (lua_State *) g.luathemes[u.uz.dnum]; if (!themes && *(fname = g.dungeons[u.uz.dnum].themerms)) { - if ((themes = nhl_init()) != 0) { + if ((themes = nhl_init(&sbi)) != 0) { if (!nhl_loadlua(themes, fname)) { /* loading lua failed, don't use themed rooms */ nhl_done(themes); @@ -303,7 +304,9 @@ makerooms(void) iflags.in_lua = g.in_mk_themerooms = TRUE; g.themeroom_failed = FALSE; lua_getglobal(themes, "themerooms_generate"); - lua_call(themes, 0, 0); + if ( nhl_pcall(themes, 0, 0)){ + impossible("Lua error: %s", lua_tostring(themes, -1)); + } iflags.in_lua = g.in_mk_themerooms = FALSE; if (g.themeroom_failed && ((themeroom_tries++ > 10) diff --git a/src/nhlsel.c b/src/nhlsel.c index edfa5b699..55144eacb 100644 --- a/src/nhlsel.c +++ b/src/nhlsel.c @@ -817,12 +817,17 @@ l_selection_iterate(lua_State *L) lua_pushvalue(L, 2); lua_pushinteger(L, x - g.xstart); lua_pushinteger(L, y - g.ystart); - lua_call(L, 2, 0); + if (nhl_pcall(L, 2, 0)) { + impossible("Lua error: %s", lua_tostring(L, -1)); + /* abort the loops to prevent possible error cascade */ + goto out; + } } } else { nhl_error(L, "wrong parameters"); /*NOTREACHED*/ } +out: return 0; } diff --git a/src/nhlua.c b/src/nhlua.c index 1853b067c..093d3317f 100644 --- a/src/nhlua.c +++ b/src/nhlua.c @@ -5,6 +5,10 @@ #include "hack.h" #include "dlb.h" +#ifdef NHL_SANDBOX +#include +#endif + /* #- include #- include @@ -55,8 +59,15 @@ static int nhl_meta_u_newindex(lua_State *); static int nhl_u_clear_inventory(lua_State *); static int nhl_u_giveobj(lua_State *); static void init_u_data(lua_State *); +#ifdef notyet static int nhl_set_package_path(lua_State *, const char *); +#endif static int traceback_handler(lua_State *); +#ifdef NHL_SANDBOX +static void nhlL_openlibs(lua_State *, uint32_t); +#endif +static lua_State *nhlL_newstate (nhl_sandbox_info *); +static void end_luapat(void); static const char *const nhcore_call_names[NUM_NHCORE_CALLS] = { "start_new_game", @@ -66,10 +77,35 @@ static const char *const nhcore_call_names[NUM_NHCORE_CALLS] = { }; static boolean nhcore_call_available[NUM_NHCORE_CALLS]; +/* internal structure that hangs off L->ud (but use lua_getallocf() ) + * Note that we use it for both memory use tracking and instruction counting. + */ +typedef struct nhl_user_data { + uint32_t flags; /* from nhl_sandbox_info */ + + uint32_t inuse; + uint32_t memlimit; + + uint32_t steps; /* current counter */ + uint32_t osteps; /* original steps value */ + uint32_t perpcall; /* per pcall steps value */ +#ifdef NHL_SANDBOX + jmp_buf jb; +#endif +} nhl_user_data; + +static lua_State *luapat; /* instance for file pattern matching */ + void l_nhcore_init(void) { - if ((g.luacore = nhl_init()) != 0) { +#if 1 + nhl_sandbox_info sbi = {NHL_SB_SAFE, 0, 0, 0}; +#else + /* Sample sbi for getting resource usage information. */ + nhl_sandbox_info sbi = {NHL_SB_SAFE|NHL_SB_REPORT2, 10000000, 10000000, 0}; +#endif + if ((g.luacore = nhl_init(&sbi)) != 0) { if (!nhl_loadlua(g.luacore, "nhcore.lua")) { g.luacore = (lua_State *) 0; } else { @@ -78,7 +114,8 @@ l_nhcore_init(void) for (i = 0; i < NUM_NHCORE_CALLS; i++) nhcore_call_available[i] = TRUE; } - } + } else + impossible("l_nhcore_init failed"); } void @@ -88,6 +125,7 @@ l_nhcore_done(void) nhl_done(g.luacore); g.luacore = 0; } + end_luapat(); } void @@ -112,7 +150,9 @@ l_nhcore_call(int callidx) if (ltyp == LUA_TFUNCTION) { lua_remove(g.luacore, -2); /* nhcore_call_names[callidx] */ lua_remove(g.luacore, -2); /* nhcore */ - lua_call(g.luacore, 0, 1); + if (nhl_pcall(g.luacore, 0, 1)) { + impossible("Lua error: %s", lua_tostring(g.luacore, -1)); + } } else { /*impossible("nhcore.%s is not a lua function", nhcore_call_names[callidx]);*/ @@ -1379,15 +1419,20 @@ init_u_data(lua_State *L) lua_setglobal(L, "u"); } +#ifdef notyet static int nhl_set_package_path(lua_State *L, const char *path) { - lua_getglobal(L, "package"); + if (LUA_TTABLE != lua_getglobal(L, "package")){ + impossible("package not a table in nhl_set_package_path"); + return 1; + }; lua_pushstring(L, path); lua_setfield(L, -2, "path"); lua_pop(L, 1); return 0; } +#endif static int traceback_handler(lua_State *L) @@ -1397,6 +1442,49 @@ traceback_handler(lua_State *L) return 1; } +/* lua_pcall with our traceback handler and instruction step limiting. + * On error, traceback will be on top of stack */ +int +nhl_pcall(lua_State *L, int nargs, int nresults){ + struct nhl_user_data *nud; + int rv; + + lua_pushcfunction(L, traceback_handler); + lua_insert(L, 1); + (void)lua_getallocf(L, (void **)&nud); +#ifdef NHL_SANDBOX + if(nud && (nud->steps || nud->perpcall)){ + if(nud->perpcall) nud->steps = nud->perpcall; + if(setjmp(nud->jb)){ + /* panic, because we don't know if the game state is corrupt */ + panic("time exceeded"); + } + } +#endif + + rv = lua_pcall(L, nargs, nresults, 1); + +#ifdef NHL_SANDBOX + if(nud + && (nud->flags & (NHL_SB_REPORT|NHL_SB_REPORT2)) + && (nud->memlimit || nud->osteps || nud->perpcall) + ){ + if(nud->flags & NHL_SB_REPORT2) + lua_gc(L, LUA_GCCOLLECT); + pline("Lua context=%p RAM: %lu STEPS:%lu", + (void *)L, + (unsigned long)nud->inuse, + (unsigned long)(nud->perpcall + ? (nud->perpcall - nud->steps) + : (nud->osteps - nud->steps)) + ); + } +#endif + + return rv; +} + +/* XXX impossible() should be swappable with pline or nothing via flag */ /* read lua code/data from a dlb module or an external file into a string buffer and feed that to lua */ boolean @@ -1416,7 +1504,7 @@ nhl_loadlua(lua_State *L, const char *fname) Sprintf(altfname, "(%s)", fname); fh = dlb_fopen(fname, RDBMODE); if (!fh) { - impossible("nhl_loadlua: Error loading %s", altfname); + impossible("nhl_loadlua: Error opening %s", altfname); ret = FALSE; goto give_up; } @@ -1492,13 +1580,11 @@ nhl_loadlua(lua_State *L, const char *fname) ret = FALSE; goto give_up; } else { - lua_pushcfunction(L, traceback_handler); - lua_insert(L, 1); - if (lua_pcall(L, 0, LUA_MULTRET, -2)) { + if(nhl_pcall(L, 0, LUA_MULTRET)) { impossible("Lua error: %s", lua_tostring(L, -1)); ret = FALSE; goto give_up; - } + } } give_up: @@ -1510,13 +1596,38 @@ nhl_loadlua(lua_State *L, const char *fname) } lua_State * -nhl_init(void) +nhl_init(nhl_sandbox_info *sbi) { - lua_State *L = luaL_newstate(); + /* It would be nice to import EXPECTED from each build system. XXX */ + /* And it would be nice to do it only once, but it's cheap. */ +#ifndef NHL_VERSION_EXPECTED +#define NHL_VERSION_EXPECTED 50404 +#endif + if(NHL_VERSION_EXPECTED != LUA_VERSION_RELEASE_NUM){ + panic( + "sandbox doesn't know this Lua version: this=%d != expected=%d ", + LUA_VERSION_RELEASE_NUM, NHL_VERSION_EXPECTED); + } + + lua_State *L = nhlL_newstate(sbi); iflags.in_lua = TRUE; + /* Temporary for development XXX */ + /* Turn this off in config.h to disable the sandbox. */ +#ifdef NHL_SANDBOX + nhlL_openlibs(L, sbi->flags); +#else luaL_openlibs(L); - nhl_set_package_path(L, "./?.lua"); +#endif + +#ifdef notyet + if (sbi->flags & NHL_SB_PACKAGE) { + /* XXX Is this still needed? */ + if (nhl_set_package_path(L, "./?.lua")){ + return 0; + } + } +#endif /* register nh -table, and functions for it */ lua_newtable(L); @@ -1534,6 +1645,12 @@ nhl_init(void) l_obj_register(L); + /* nhlib.lua assumes the math table exists. */ + if(LUA_TTABLE != lua_getglobal(L, "math")){ + lua_newtable(L); + lua_setglobal(L, "math"); + } + if (!nhl_loadlua(L, "nhlib.lua")) { nhl_done(L); return (lua_State *) 0; @@ -1551,10 +1668,10 @@ nhl_done(lua_State *L) } boolean -load_lua(const char *name) +load_lua(const char *name, nhl_sandbox_info *sbi) { boolean ret = TRUE; - lua_State *L = nhl_init(); + lua_State *L = nhl_init(sbi); if (!L) { ret = FALSE; @@ -1577,8 +1694,10 @@ DISABLE_WARNING_CONDEXPR_IS_CONSTANT const char * get_lua_version(void) { + nhl_sandbox_info sbi = {NHL_SB_VERSION, 0, 0, 0}; + if (g.lua_ver[0] == 0) { - lua_State *L = nhl_init(); + lua_State *L = nhl_init(&sbi); if (L) { size_t len = 0; @@ -1624,5 +1743,546 @@ get_lua_version(void) RESTORE_WARNINGS +/*** + *** SANDBOX / HARDENING CODE + ***/ +#ifdef NHL_SANDBOX + +/* NHL_BASE_BASE - safe things */ +static const char *ct_base_base[] = { + "ipairs", "next", "pairs", "pcall", "rawequal", "rawlen", "select", + "tonumber", "tostring", "type", "xpcall", NULL +}; + +/* NHL_BASE_ERROR - not really safe - might not want Lua to kill the process */ +static const char *ct_base_error[] = { + "assert", /* ok, calls error */ + "error", /* ok, calls G->panic */ + /* "print", not ok - calls lua_writestring/lua_writeline -> stdout*/ + /* "warn", not ok - calls lua_writestringerror -> stderr */ + NULL +}; + +/* NHL_BASE_META - metatable access */ +static const char *ct_base_meta[] = { + "getmetatable", "rawget", "rawset", "setmetatable", NULL +}; + +/* NHL_BASE_GC - questionable safety */ +static const char *ct_base_iffy[] = { + "collectgarbage", NULL +}; + +/* NHL_BASE_UNSAFE - include only if required */ +static const char *ct_base_unsafe[] = { + "dofile", "loadfile", "load", NULL +}; + +static void +nhl_clearfromtable(lua_State *L, int tndx, const char **todo){ + while(*todo){ + lua_pushnil(L); + lua_setfield(L, tndx, *todo++); + } +} +#endif + +/* +XXX +registry["org.nethack.nethack.sb.fs"][N]= + CODEOBJECT + { + modepat: PATTERN, + filepat: PATTERN + } +CODEOBJECT + if string then if pcall(string,mode, dir, file) + if table then if mode matches pattern and filepat ma.... +or do we use a real regex engine? (which we don't have and I just + argued against adding) + +return values from "call it": + accept - file access granted + reject - file access denied + continue - try next element + fail - error. deny and call impossible/panic +*/ + +/* stack indexes: + * -1 table to index with ename + * params file + * params+1 [mode] + */ +/* + * Problem: NetHack doesn't have a regex engine and Lua doesn't give + * C access to pattern matching. There are 3 poor solutions: + * 1. Import ~5K lines of code in a dozen files from FreeBSD. (Upside - we + * could use it in other places in NetHack.) + * 2. Hack up lstrlib.c to give C direct access to the pattern matching code. + * 3. Create a Lua state just to do pattern matching. + * We're going to do #3. + */ +#ifdef notyet +static boolean +start_luapat(){ + int rv; +/* XXX set memory and step limits */ + nhl_sandbox_info sbi = {NHL_SB_STRING, 0, 0, 0}; + + if((luapat = nhl_init(&sbi)) == NULL) + return FALSE; + + /* load a pattern matching function */ + rv = luaL_loadstring(luapat, + "function matches(s,p) return not not string.match(s,p) end"); + if(rv != LUA_OK){ + panic("start_luapat: %d",rv); + } + + return TRUE; +} +#endif + +static void +end_luapat(){ + if(luapat){ + lua_close(luapat); + luapat = NULL; + } +} + +#ifdef notyet +static int +opencheckpat(lua_State *L, const char *ename, int param){ + /* careful - we're using 2 different and unrelated Lua states */ + const char *string; + int rv; + + lua_pushliteral(luapat, "matches"); /* function -0,+1 */ + + string = lua_tolstring(L, param, NULL); /* mode or filename -0,+0 */ + lua_pushstring(luapat, string); /* -0,+1 */ + (void)lua_getfield(L, -1, ename); /* pattern -0,+1 */ + lua_pop(L, 1); /* -1,+0 */ + string = lua_tolstring(L, -1, NULL); /* -0,+0 */ + lua_pushstring(luapat, string); /* -0,+1 */ + + if(nhl_pcall(luapat, 2, 1)){ /* -3,+1 */ + /* impossible("access check internal error"); */ + return NHL_SBRV_FAIL; + } + rv = lua_toboolean(luapat, -1); /* -0,+0 */ +#if 0 + if(lua_resetthread(luapat) != LUA_OK) + return NHL_SBRV_FAIL; +is pop sufficient? XXX or wrong - look at the balance +#else + lua_pop(luapat, 1); /* -1,+0 */ +#endif + return rv ? NHL_SBRV_ACCEPT : NHL_SBRV_DENY; +} +#endif + +/* put the table open uses to check its arguments on the top of the stack, + * creating it if needed + */ +#define HOOKTBLNAME "org.nethack.nethack.sb.fs" +#ifdef notyet +static int (*io_open)(lua_State *) = NULL; /* XXX this may have to be in g TBD */ +#endif + +void +nhl_pushhooked_open_table(lua_State *L){ + int hot = lua_getfield(L, LUA_REGISTRYINDEX, HOOKTBLNAME); + if (hot == LUA_TNONE) { + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, HOOKTBLNAME); + } +} + +#ifdef notyet +static int +hooked_open(lua_State *L){ + const char *mode; + static boolean never = TRUE; + const char *filename; + int params; + int hot; + + if (never){ + if(!start_luapat()) + return NHL_SBRV_FAIL; + never = FALSE; + } + filename = luaL_checkstring(L, 1); + + /* Unlike io.open, we want to treat mode as non-optional. */ + if (lua_gettop(L) < 2){ + lua_pushstring(L, "r"); + } + mode = luaL_optstring(L, 2, "r"); + + /* sandbox checks */ + /* Do we need some ud from the calling state to let this be different + for each call without redoing the HO table?? Maybe for version 2. XXX */ + + params = lua_gettop(L)-1; /* point at first param */ + nhl_pushhooked_open_table(L); + hot = lua_gettop(L); + + if(lua_type(L, hot) == LUA_TTABLE){ + int idx; + for( + idx=1; + lua_pushinteger(L, idx), + lua_geti(L, hot, idx), + !lua_isnoneornil(L, -1); + idx++ + ){ + /* top of stack is our configtbl[idx] */ + switch(lua_type(L, -1)){ + /* lots of options to expand this with other types XXX */ + case LUA_TTABLE: { + int moderv, filerv; + moderv = opencheckpat(L, "modepat", params+1); + if (moderv == NHL_SBRV_FAIL) + return moderv; + filerv = opencheckpat(L, "filepat", params); + if (filerv == NHL_SBRV_FAIL) + return moderv; + if(filerv == moderv){ + if(filerv == NHL_SBRV_DENY) + return NHL_SBRV_DENY; + if(filerv == NHL_SBRV_ACCEPT) + goto doopen; + } + break; /* try next entry */ + } + default: + return NHL_SBRV_FAIL; + } + } + } else + return NHL_SBRV_DENY; /* default to "no" */ + +doopen: + lua_settop(L, params+1); + return (*io_open)(L); +} + +static boolean +hook_open(lua_State *L){ + boolean rv = FALSE; + if(!io_open){ + int tos = lua_gettop(L); + lua_pushglobaltable(L); + if(lua_getfield(L, -1, "io") != LUA_TTABLE) goto out; + lua_getfield(L, -1, "open"); + /* The only way this can happen is if someone is messing with us, + * and I'm not sure even that is possible. */ + if(!lua_iscfunction(L, -1)) goto out; + /* XXX This is fragile: C11 says casting func* to void* + * doesn't have to work, but POSIX says it does. So it + * _should_ work everywhere but all we can do without messing + * around inside Lua is to try to keep the compiler quiet. */ + io_open = (int (*)(lua_State *))lua_topointer(L, -1); + lua_pushcfunction(L, hooked_open); + lua_setfield(L, -1, "open"); + rv = TRUE; +out: + lua_settop(L, tos); + } + return rv; +} +#endif + +#ifdef NHL_SANDBOX +static void +nhlL_openlibs(lua_State *L, uint32_t lflags){ + uint32_t needbase; + + /* translate lflags from user-friendly to internal */ + if (NHL_SB_DEBUGGING & lflags){ +#if 1 /* XXX */ + lflags |= NHL_SB_DB; +/* XXX +Should these be available as safe or as a low level group? +debug.getinfo +debug.traceback? +*/ +#endif + } + /* only for debugging the sandbox integration */ + if (NHL_SB_ALL & lflags){ + lflags = -1; + } else if ((NHL_SB_SAFE +#ifdef notyet +|NHL_SB_CANREAD +#endif + ) & lflags){ + lflags |= NHL_SB_BASE_BASE; + lflags |= NHL_SB_COROUTINE; + lflags |= NHL_SB_TABLE; + lflags |= NHL_SB_STRING; + lflags |= NHL_SB_MATH; + lflags |= NHL_SB_UTF8; + } else if (NHL_SB_VERSION){ + lflags |= NHL_SB_BASE_BASE; +#ifdef notyet + } else if (NHL_SB_CANREAD & lflags){ +/* QQQ */ +/* +canread may be wrong. +How about: + - sets of fns (as below, as base) + - hooked open; array of tuples of (r/w/rw/a/etc, directory pat, file pat) + +XXX +really don't have anything here +because IO is too broad? +we need to split it like BASE - load then delete: +SAFEIO: +{"close", io_close}, but with no args closes default output, so needs hook +{"flush", io_flush}, +{"lines", io_lines}, hook due to filename +{"open", io_open}, but we need a hooked version: + only safe if mode not present or == "r" + or WRITEIO + only safe if path has no slashes + XXX probably need to be: matches port-specific list of paths + WRITEIO needs a different list + dlb integration????? + may need to #define l_getc (but that wouldn't hook core) + may need to #define fopen/fread/fwrite/feof/ftell (etc?) + ugh: lauxlib.c uses getc() below luaL_loadfilex + override in lua.h? + ugh: liolib.c uses getc() below g_read->test_eof + override in lua.h? +{"read", io_read}, +{"type", io_type}, +{"input", io_input}, safe with a complex hook, but may be needed for read? +WRITEIO: needs changes to hooked open? +{"output", io_output}, do we want to allow access to default output? + {"write", io_write}, +UNSAFEIO: + {"popen", io_popen}, + {"tmpfile", io_tmpfile}, +*/ +#endif + } + +/* +multiple levels - io.*, FILE.* - can we hook FILE.*? + see liolib.c:{meta, createmeta, luaopen_io} +// do we need anything else? meta? +*/ + + needbase = lflags & NHL_SB_BASEMASK; + if(needbase){ + luaL_requiref(L, LUA_GNAME, luaopen_base, 1); + int baselib = lua_gettop(L); + + /* now remove everything not requested */ + uint16_t rejectflags = ~lflags; +#define DROPIF(flag, x, table) \ + if(rejectflags & flag){ nhl_clearfromtable(L, x, table); } + + DROPIF(NHL_SB_BASE_BASE, baselib, ct_base_base); + DROPIF(NHL_SB_BASE_ERROR, baselib, ct_base_error); + DROPIF(NHL_SB_BASE_META, baselib, ct_base_meta); + DROPIF(NHL_SB_BASE_GC, baselib, ct_base_iffy); + DROPIF(NHL_SB_BASE_UNSAFE, baselib, ct_base_unsafe); +#undef DROPIF + lua_pop(L, 1); + } + +#ifdef notyet + if(lflags & NHL_SB_PACKAGE){ + luaL_requiref(L, LUA_LOADLIBNAME, luaopen_package, 1); + lua_pop(L, 1); + } +#endif + if(lflags & NHL_SB_COROUTINE){ + luaL_requiref(L, LUA_COLIBNAME, luaopen_coroutine, 1); + lua_pop(L, 1); + } + if(lflags & NHL_SB_TABLE){ + luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1); + lua_pop(L, 1); + } +#ifdef notyet + if(lflags & NHL_SB_IO){ + luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, 1); + lua_pop(L, 1); + if(!hook_open(L)) + panic("can't hook io.open"); + } +// maybe ok: time, difftime, getenv clock date + if(lflags & NHL_SB_OS){ + luaL_requiref(L, LUA_OSLIBNAME, luaopen_os, 1); + lua_pop(L, 1); + } +#endif + if(lflags & NHL_SB_STRING){ + luaL_requiref(L, LUA_STRLIBNAME, luaopen_string, 1); + lua_pop(L, 1); + } + if(lflags & NHL_SB_MATH){ + luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 1); + /* XXX Note that math.random uses Lua's built-in xoshiro256** + * algorithm regardless of what the rest of the game uses. + * Fixing this would require changing lmathlib.c. */ + lua_pop(L, 1); + } + if(lflags & NHL_SB_UTF8){ + luaL_requiref(L, LUA_UTF8LIBNAME, luaopen_utf8, 1); + lua_pop(L, 1); + } + if(lflags & NHL_SB_DB){ + luaL_requiref(L, LUA_DBLIBNAME, luaopen_debug, 1); + lua_pop(L, 1); + } +} +#endif + +/* + * All we can do is approximate the amount of storage used. Every allocator + * has different overhead and uses that overhead differently. Since we're + * really just trying to prevent egregious use, we default to a minimum + * allocation size of 16 and if you know better about your allocator (and + * it's worth the processing time), it can be overridden. + */ +#ifndef NHL_ALLOC_ADJUST +#define NHL_ALLOC_ADJUST(d) d = (d+15 & ~15) +#endif +static void * +nhl_alloc (void *ud, void *ptr, size_t osize, size_t nsize) { + nhl_user_data *nud = ud; + + if(nud && nud->memlimit){ /* this state is size limited */ + uint32_t delta; + if(!ptr){ + delta = nsize; + } else { + delta = nsize-osize; + } + NHL_ALLOC_ADJUST(delta); + nud->inuse += delta; + if(nud->inuse > nud->memlimit){ + return 0; + } + } + + if (nsize == 0) { + free(ptr); + return NULL; + } else + return realloc(ptr, nsize); +} + +static int +nhl_panic (lua_State *L) { + const char *msg = lua_tostring(L, -1); + if (msg == NULL) msg = "error object is not a string"; + panic("unprotected error in call to Lua API (%s)\n", msg); + return 0; /* return to Lua to abort */ +} + +#ifdef NHL_SANDBOX +static void +nhl_hookfn(lua_State *L, lua_Debug *ar UNUSED){ + nhl_user_data *nud; + + (void)lua_getallocf(L, (void **)&nud); + + if (nud->steps <= NHL_SB_STEPSIZE) + longjmp(nud->jb, 1); + + nud->steps -= NHL_SB_STEPSIZE; +} +#endif + +static lua_State * +nhlL_newstate (nhl_sandbox_info *sbi) { + nhl_user_data *nud = 0; + + if(sbi->memlimit || sbi->steps){ + nud = nhl_alloc(NULL, NULL, 0, sizeof(struct nhl_user_data)); + if(!nud) + return 0; + nud->memlimit = sbi->memlimit; + nud->perpcall = 0; /* set up below, if needed */ + nud->steps = 0; + nud->osteps = 0; + nud->flags = sbi->flags; /* save reporting flags */ + uint32_t sz = sizeof(struct nhl_user_data); + NHL_ALLOC_ADJUST(sz); + nud->inuse = sz; + } + + lua_State *L = lua_newstate(nhl_alloc, nud); + if (luai_likely(L)) { + lua_atpanic(L, &nhl_panic); + /* no warning system at the moment - it requires concatenting + * strings to fit NetHack's API XXX */ + lua_setwarnf(L, 0, L); /* default is warnings off */ + } + +#ifdef NHL_SANDBOX + if (sbi->steps || sbi->perpcall){ + if (sbi->steps && sbi->perpcall) + impossible("steps and perpcall both non-zero"); + if (sbi->perpcall){ + nud->perpcall = sbi->perpcall; + } else { + nud->steps = sbi->steps; + nud->osteps = sbi->steps; + } + lua_sethook(L, nhl_hookfn, LUA_MASKCOUNT, NHL_SB_STEPSIZE); + } +#endif + + return L; +} + +/* +(See end of comment for conclusion.) +to make packages safe, we need something like: + if setuid/setgid (but does NH drop privs before we can check? TBD) + unsetenv LUA_CPATH, LUA_CPATH_5_4 (and this needs to change with + version) maybe more + luaopen_package calls getenv + unsetenv(LUA_PATH_VAR) + unsetenv(LUA_CPATH_VAR) + unsetenv(LUA_PATH_VAR LUA_VERSUFFIX) + unsetenv(LUA_CPATH_VAR LUA_VERSUFFIX) + package.config + oackage[fieldname] = path + NB: LUA_PATH_DEFAULT and LUA_CPATH_DEFAULT must be safe + or we must setenv LUA_PATH_VAR and LUA_CPATH_VAR to something + safe + or we could just clean out the searchers table? + package.searchers[preload,Lua,C,Croot] +also, can setting package.path to something odd get Lua to load files + it shouldn't? (see docs package.searchers) +set (and disallow changing) package.cpath (etc?) +loadlib.c: + lsys_load -> dlopen Kill with undef LUA_USE_DLOPEN LUA_DL_DLL + searchpath -> readable -> fopen + <- ll_searchpath + <- findfile <- {searchers C, Croot, Lua} +Probably the best thing to do is replace G.require with our own function +that does whatever it is we need and completely ignore the package library. +*/ +/* +TODO: +docs +unfinished functionality & design +commit, cleanup, commit with SHA1 of full code version +BUT how do we compact the current history? + new branch, then compress there +XXX +*/ diff --git a/src/questpgr.c b/src/questpgr.c index 11c540599..fde444c22 100644 --- a/src/questpgr.c +++ b/src/questpgr.c @@ -433,11 +433,12 @@ com_pager_core(const char *section, const char *msgid, boolean showerror) lua_State *L; char *text = NULL, *synopsis = NULL, *fallback_msgid = NULL; boolean res = FALSE; + nhl_sandbox_info sbi = {NHL_SB_SAFE, 0, 0, 0}; if (skip_pager(TRUE)) return FALSE; - L = nhl_init(); + L = nhl_init(&sbi); if (!L) { if (showerror) impossible("com_pager: nhl_init() failed"); diff --git a/src/sp_lev.c b/src/sp_lev.c index 34cb45341..1a60b4519 100644 --- a/src/sp_lev.c +++ b/src/sp_lev.c @@ -3195,7 +3195,9 @@ lspo_monster(lua_State *L) if (tmpmons.has_invent && lua_type(L, -1) == LUA_TFUNCTION) { lua_remove(L, -2); - lua_call(L, 0, 0); + if (nhl_pcall(L, 0, 0)){ + impossible("Lua error: %s", lua_tostring(L, -1)); + } spo_end_moninvent(); } else lua_pop(L, 1); @@ -3509,7 +3511,9 @@ lspo_object(lua_State *L) if (lua_type(L, -1) == LUA_TFUNCTION) { lua_remove(L, -2); - lua_call(L, 0, 0); + if (nhl_pcall(L, 0, 0)){ + impossible("Lua error: %s", lua_tostring(L, -1)); + } } else lua_pop(L, 1); @@ -3819,7 +3823,9 @@ lspo_room(lua_State *L) if (lua_type(L, -1) == LUA_TFUNCTION) { lua_remove(L, -2); l_push_mkroom_table(L, tmpcr); - lua_call(L, 1, 0); + if (nhl_pcall(L, 1, 0)){ + impossible("Lua error: %s", lua_tostring(L, -1)); + } } else lua_pop(L, 1); spo_endroom(g.coder); @@ -5758,7 +5764,9 @@ lspo_region(lua_State *L) lua_getfield(L, 1, "contents"); if (lua_type(L, -1) == LUA_TFUNCTION) { lua_remove(L, -2); - lua_call(L, 0, 0); + if (nhl_pcall(L, 0, 0)){ + impossible("Lua error: %s", lua_tostring(L, -1)); + } } else lua_pop(L, 1); spo_endroom(g.coder); @@ -6362,7 +6370,9 @@ TODO: g.coder->croom needs to be updated if (has_contents && !(g.in_mk_themerooms && g.themeroom_failed)) { l_push_wid_hei_table(L, g.xsize, g.ysize); - lua_call(L, 1, 0); + if (nhl_pcall(L, 1, 0)){ + impossible("Lua error: %s", lua_tostring(L, -1)); + } } return 0; @@ -6500,10 +6510,11 @@ boolean load_special(const char *name) { boolean result = FALSE; + nhl_sandbox_info sbi = {NHL_SB_SAFE, 0, 0, 0}; create_des_coder(); - if (!load_lua(name)) + if (!load_lua(name, &sbi)) goto give_up; link_doors_rooms();