diff --git a/ldblib.c b/ldblib.c index d045a82e..513a13cb 100644 --- a/ldblib.c +++ b/ldblib.c @@ -437,6 +437,17 @@ static int db_traceback (lua_State *L) { } +static int db_setCstacklimit (lua_State *L) { + int limit = (int)luaL_checkinteger(L, 1); + int res = lua_setCstacklimit(L, limit); + if (res == 0) + lua_pushboolean(L, 0); + else + lua_pushinteger(L, res); + return 1; +} + + static const luaL_Reg dblib[] = { {"debug", db_debug}, {"getuservalue", db_getuservalue}, @@ -454,6 +465,7 @@ static const luaL_Reg dblib[] = { {"setmetatable", db_setmetatable}, {"setupvalue", db_setupvalue}, {"traceback", db_traceback}, + {"setCstacklimit", db_setCstacklimit}, {NULL, NULL} }; diff --git a/ldo.c b/ldo.c index 0ad3120b..1a327ffd 100644 --- a/ldo.c +++ b/ldo.c @@ -139,7 +139,8 @@ l_noret luaD_throw (lua_State *L, int errcode) { int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) { - l_uint32 oldnCcalls = L->nCcalls + L->nci; + global_State *g = G(L); + l_uint32 oldnCcalls = g->Cstacklimit - (L->nCcalls + L->nci); struct lua_longjmp lj; lj.status = LUA_OK; lj.previous = L->errorJmp; /* chain new error handler */ @@ -148,7 +149,7 @@ int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) { (*f)(L, ud); ); L->errorJmp = lj.previous; /* restore old error handler */ - L->nCcalls = oldnCcalls - L->nci; + L->nCcalls = g->Cstacklimit - oldnCcalls - L->nci; return lj.status; } @@ -671,7 +672,7 @@ LUA_API int lua_resume (lua_State *L, lua_State *from, int nargs, else if (L->status != LUA_YIELD) /* ended with errors? */ return resume_error(L, "cannot resume dead coroutine", nargs); if (from == NULL) - L->nCcalls = LUAI_MAXCSTACK; + L->nCcalls = CSTACKTHREAD; else /* correct 'nCcalls' for this thread */ L->nCcalls = getCcalls(from) + from->nci - L->nci - CSTACKCF; if (L->nCcalls <= CSTACKERR) diff --git a/lstate.c b/lstate.c index 296cec2a..92d5165a 100644 --- a/lstate.c +++ b/lstate.c @@ -96,6 +96,29 @@ void luaE_setdebt (global_State *g, l_mem debt) { } +LUA_API int lua_setCstacklimit (lua_State *L, unsigned int limit) { + global_State *g = G(L); + int ccalls; + luaE_freeCI(L); /* release unused CIs */ + ccalls = getCcalls(L); + if (limit >= 40000) + return 0; /* out of bounds */ + limit += CSTACKERR; + if (L != g-> mainthread) + return 0; /* only main thread can change the C stack */ + else if (ccalls <= CSTACKERR) + return 0; /* handling overflow */ + else { + int diff = limit - g->Cstacklimit; + if (ccalls + diff <= CSTACKERR) + return 0; /* new limit would cause an overflow */ + g->Cstacklimit = limit; /* set new limit */ + L->nCcalls += diff; /* correct 'nCcalls' */ + return limit - diff - CSTACKERR; /* success; return previous limit */ + } +} + + /* ** Decrement count of "C calls" and check for overflows. In case of ** a stack overflow, check appropriate error ("regular" overflow or @@ -121,7 +144,7 @@ void luaE_enterCcall (lua_State *L) { else if (ncalls >= CSTACKMARK) { /* not in error-handling zone; raise the error now */ L->nCcalls = (CSTACKMARK - 1); /* enter error-handling zone */ - luaG_runerror(L, "C stack overflow1"); + luaG_runerror(L, "C stack overflow"); } /* else stack is in the error-handling zone; allow message handler to work */ @@ -263,7 +286,7 @@ static void preinit_thread (lua_State *L, global_State *g) { L->stacksize = 0; L->twups = L; /* thread has no upvalues */ L->errorJmp = NULL; - L->nCcalls = LUAI_MAXCSTACK + CSTACKERR; + L->nCcalls = CSTACKTHREAD; L->hook = NULL; L->hookmask = 0; L->basehookcount = 0; @@ -365,6 +388,7 @@ LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) { preinit_thread(L, g); g->allgc = obj2gco(L); /* by now, only object is the main thread */ L->next = NULL; + g->Cstacklimit = L->nCcalls = LUAI_MAXCSTACK; g->frealloc = f; g->ud = ud; g->warnf = NULL; diff --git a/lstate.h b/lstate.h index 858da5be..d3a64f94 100644 --- a/lstate.h +++ b/lstate.h @@ -103,6 +103,10 @@ #define CSTACKERRMARK (CSTACKCF + 2) +/* initial limit for the C-stack of threads */ +#define CSTACKTHREAD (2 * CSTACKERR) + + /* true if this thread does not have non-yieldable calls in the stack */ #define yieldable(L) (((L)->nCcalls & 0xffff0000) == 0) @@ -267,6 +271,7 @@ typedef struct global_State { TString *strcache[STRCACHE_N][STRCACHE_M]; /* cache for strings in API */ lua_WarnFunction warnf; /* warning function */ void *ud_warn; /* auxiliary data to 'warnf' */ + unsigned int Cstacklimit; /* current limit for the C stack */ } global_State; diff --git a/ltests.h b/ltests.h index a22c98e1..0ae6afc4 100644 --- a/ltests.h +++ b/ltests.h @@ -31,7 +31,7 @@ /* compiled with -O0, Lua uses a lot of C stack space... */ #undef LUAI_MAXCSTACK -#define LUAI_MAXCSTACK 400 +#define LUAI_MAXCSTACK (400 + CSTACKERR) /* to avoid warnings, and to make sure value is really unused */ #define UNUSED(x) (x=0, (void)(x)) diff --git a/lua.h b/lua.h index ec31c781..d3575fd9 100644 --- a/lua.h +++ b/lua.h @@ -462,6 +462,7 @@ LUA_API lua_Hook (lua_gethook) (lua_State *L); LUA_API int (lua_gethookmask) (lua_State *L); LUA_API int (lua_gethookcount) (lua_State *L); +LUA_API int (lua_setCstacklimit) (lua_State *L, unsigned int limit); struct lua_Debug { int event; diff --git a/manual/manual.of b/manual/manual.of index 725b12ad..e9416956 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -4803,6 +4803,20 @@ calling @Lid{lua_yield} with @id{nresults} equal to zero } +@APIEntry{int (lua_setCstacklimit) (lua_State *L, unsigned int limit);| +@apii{0,0,-} + +Sets a new limit for the C stack. +This limit controls how deeply nested calls can go in Lua, +with the intent of avoiding a stack overflow. +Returns the old limit in case of success, +or zero in case of error. +For more details about this function, +see @Lid{debug.setCstacklimit}, +its equivalent in the standard library. + +} + @APIEntry{void lua_sethook (lua_State *L, lua_Hook f, int mask, int count);| @apii{0,0,-} @@ -8516,6 +8530,34 @@ to the userdata @id{u} plus a boolean, } +@LibEntry{debug.setCstacklimit (limit)| + +Sets a new limit for the C stack. +This limit controls how deeply nested calls can go in Lua, +with the intent of avoiding a stack overflow. +A limit too small restricts recursive calls pointlessly; +a limit too large exposes the interpreter to stack-overflow crashes. +Unfortunately, there is no way to know a priori +the maximum safe limit for a platform. + +Each call made from Lua code counts one unit. +Other operations (e.g., calls made from C to Lua or resuming a coroutine) +may have a higher cost. + +This function has the following restrictions: +@description{ +@item{It can only be called from the main coroutine (thread);} +@item{It cannot be called while handling a stack-overflow error;} +@item{@id{limit} must be less than 40000;} +@item{@id{limit} cannot be less than the amount of C stack in use.} +} +In case of success, +this function returns the old limit. +In case of error, +it returns @false. + +} + @LibEntry{debug.sethook ([thread,] hook, mask [, count])| Sets the given function as the debug hook. diff --git a/testes/cstack.lua b/testes/cstack.lua index 3cb1e4ce..c7aa740f 100644 --- a/testes/cstack.lua +++ b/testes/cstack.lua @@ -1,8 +1,14 @@ -- $Id: testes/cstack.lua $ -- See Copyright Notice in file all.lua +local debug = require "debug" + print"testing C-stack overflow detection" +local origlimit = debug.setCstacklimit(400) +print("current stack limit: " .. origlimit) +debug.setCstacklimit(origlimit) + -- Segmentation faults in these tests probably result from a C-stack -- overflow. To avoid these errors, recompile Lua with a smaller -- value for the constant 'LUAI_MAXCCALLS' or else ensure a larger @@ -79,4 +85,46 @@ do print("testing stack-overflow in recursive 'gsub'") print("\tfinal count: ", count) end + +do print("testing changes in C-stack limit") + + assert(not debug.setCstacklimit(0)) -- limit too small + assert(not debug.setCstacklimit(50000)) -- limit too large + local co = coroutine.wrap (function () + return debug.setCstacklimit(400) + end) + assert(co() == false) -- cannot change C stack inside coroutine + + local n + local function foo () n = n + 1; foo () end + + local function check () + n = 0 + pcall(foo) + return n + end + + assert(debug.setCstacklimit(400) == origlimit) + local lim400 = check() + -- a very low limit (given that the several calls to arive here) + local lowlimit = 38 + assert(debug.setCstacklimit(lowlimit) == 400) + assert(check() < lowlimit - 30) + assert(debug.setCstacklimit(600) == lowlimit) + local lim600 = check() + assert(lim600 == lim400 + 200) + + + -- 'setCstacklimit' works inside protected calls. (The new stack + -- limit is kept when 'pcall' returns.) + assert(pcall(function () + assert(debug.setCstacklimit(400) == 600) + assert(check() <= lim400) + end)) + + assert(check() == lim400) + assert(debug.setCstacklimit(origlimit) == 400) -- restore original limit +end + + print'OK' diff --git a/testes/errors.lua b/testes/errors.lua index 0b12410e..6e7b8004 100644 --- a/testes/errors.lua +++ b/testes/errors.lua @@ -523,9 +523,13 @@ end -- testing syntax limits -local function testrep (init, rep, close, repc) +local function testrep (init, rep, close, repc, finalresult) local s = init .. string.rep(rep, 100) .. close .. string.rep(repc, 100) - assert(load(s)) -- 100 levels is OK + local res, msg = load(s) + assert(res) -- 100 levels is OK + if (finalresult) then + assert(res() == finalresult) + end s = init .. string.rep(rep, 10000) local res, msg = load(s) -- 10000 levels not ok assert(not res and (string.find(msg, "too many registers") or @@ -534,14 +538,14 @@ end testrep("local a; a", ",a", "= 1", ",1") -- multiple assignment testrep("local a; a=", "{", "0", "}") -testrep("local a; a=", "(", "2", ")") -testrep("local a; ", "a(", "2", ")") +testrep("return ", "(", "2", ")", 2) +testrep("local function a (x) return x end; return ", "a(", "2.2", ")", 2.2) testrep("", "do ", "", " end") testrep("", "while a do ", "", " end") testrep("local a; ", "if a then else ", "", " end") testrep("", "function foo () ", "", " end") -testrep("local a; a=", "a..", "a", "") -testrep("local a; a=", "a^", "a", "") +testrep("local a = ''; return ", "a..", "'a'", "", "a") +testrep("local a = 1; return ", "a^", "a", "", 1) checkmessage("a = f(x" .. string.rep(",x", 260) .. ")", "too many registers")