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();