Revision of stackless implementation

- more organized handling of 'nCcalls'
- comments
- deprecation of 'setcstacklimit'
This commit is contained in:
Roberto Ierusalimschy 2020-09-23 10:18:01 -03:00
parent 5d8ce05b3f
commit 287b302acb
12 changed files with 127 additions and 283 deletions

2
all
View File

@ -1,7 +1,7 @@
make -s -j
cd testes/libs; make -s
cd .. # back to directory 'testes'
ulimit -S -s 2000
ulimit -S -s 1000
if { ../lua -W all.lua; } then
echo -e "\n\n final OK!!!!\n\n"
else

View File

@ -440,10 +440,7 @@ 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);
lua_pushinteger(L, res);
return 1;
}

52
ldo.c
View File

@ -448,10 +448,11 @@ void luaD_pretailcall (lua_State *L, CallInfo *ci, StkId func, int narg1) {
/*
** Call a function (C or Lua). The function to be called is at *func.
** The arguments are on the stack, right after the function.
** When returns, all the results are on the stack, starting at the original
** function position.
** Prepares the call to a function (C or Lua). For C functions, also do
** the call. The function to be called is at '*func'. The arguments are
** on the stack, right after the function. Returns true if the call was
** made (it was a C function). When returns true, all the results are
** on the stack, starting at the original function position.
*/
int luaD_precall (lua_State *L, StkId func, int nresults) {
lua_CFunction f;
@ -511,32 +512,34 @@ int luaD_precall (lua_State *L, StkId func, int nresults) {
}
static void stackerror (lua_State *L) {
if (getCcalls(L) == LUAI_MAXCCALLS)
luaG_runerror(L, "C stack overflow");
else if (getCcalls(L) >= (LUAI_MAXCCALLS + (LUAI_MAXCCALLS>>3)))
luaD_throw(L, LUA_ERRERR); /* error while handing stack error */
}
void luaD_call (lua_State *L, StkId func, int nResults) {
L->nCcalls++;
/*
** Call a function (C or Lua). 'inc' can be 1 (increment number
** of recursive invocations in the C stack) or nyci (the same plus
** increment number of non-yieldable calls).
*/
static void docall (lua_State *L, StkId func, int nResults, int inc) {
L->nCcalls += inc;
if (getCcalls(L) >= LUAI_MAXCCALLS)
stackerror(L);
luaE_checkcstack(L);
if (!luaD_precall(L, func, nResults)) /* is a Lua function? */
luaV_execute(L, L->ci); /* call it */
L->nCcalls--;
L->nCcalls -= inc;
}
/*
** External interface for 'docall'
*/
void luaD_call (lua_State *L, StkId func, int nResults) {
return docall(L, func, nResults, 1);
}
/*
** Similar to 'luaD_call', but does not allow yields during the call.
*/
void luaD_callnoyield (lua_State *L, StkId func, int nResults) {
incnny(L);
luaD_call(L, func, nResults);
decnny(L);
return docall(L, func, nResults, nyci);
}
@ -650,13 +653,12 @@ static void resume (lua_State *L, void *ud) {
int n = *(cast(int*, ud)); /* number of arguments */
StkId firstArg = L->top - n; /* first argument */
CallInfo *ci = L->ci;
if (L->status == LUA_OK) { /* starting a coroutine? */
if (!luaD_precall(L, firstArg - 1, LUA_MULTRET)) /* Lua function? */
luaV_execute(L, L->ci); /* call it */
}
if (L->status == LUA_OK) /* starting a coroutine? */
docall(L, firstArg - 1, LUA_MULTRET, 1); /* just call its body */
else { /* resuming from previous yield */
lua_assert(L->status == LUA_YIELD);
L->status = LUA_OK; /* mark that it is running (again) */
luaE_incCstack(L); /* control the C stack */
if (isLua(ci)) /* yielded inside a hook? */
luaV_execute(L, ci); /* just continue running Lua code */
else { /* 'common' yield */
@ -684,9 +686,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);
L->nCcalls = (from) ? getCcalls(from) + 1 : 1;
if (getCcalls(L) >= LUAI_MAXCCALLS)
return resume_error(L, "C stack overflow", nargs);
L->nCcalls = (from) ? getCcalls(from) : 0;
luai_userstateresume(L, nargs);
api_checknelems(L, (L->status == LUA_OK) ? nargs + 1 : nargs);
status = luaD_rawrunprotected(L, resume, &nargs);

View File

@ -234,6 +234,17 @@ typedef l_uint32 Instruction;
#endif
/*
** Maximum depth for nested C calls, syntactical nested non-terminals,
** and other features implemented through recursion in C. (Value must
** fit in a 16-bit unsigned integer. It must also be compatible with
** the size of the C stack.)
*/
#if !defined(LUAI_MAXCCALLS)
#define LUAI_MAXCCALLS 200
#endif
/*
** macros that are executed whenever program enters the Lua core
** ('lua_lock') and leaves the core ('lua_unlock')

View File

@ -489,11 +489,7 @@ static void adjust_assign (LexState *ls, int nvars, int nexps, expdesc *e) {
}
static void enterlevel (LexState *ls) {
lua_State *L = ls->L;
L->nCcalls++;
checklimit(ls->fs, getCcalls(L), LUAI_MAXCCALLS, "C levels");
}
#define enterlevel(ls) luaE_incCstack(ls->L)
#define leavelevel(ls) ((ls)->L->nCcalls--)

View File

@ -97,25 +97,8 @@ 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 */
}
UNUSED(L); UNUSED(limit);
return LUAI_MAXCCALLS; /* warning?? */
}
@ -172,6 +155,28 @@ void luaE_shrinkCI (lua_State *L) {
}
/*
** Called when 'getCcalls(L)' larger or equal to LUAI_MAXCCALLS.
** If equal, raises an overflow error. If value is larger than
** LUAI_MAXCCALLS (which means it is handling an overflow) but
** not much larger, does not report an error (to allow overflow
** handling to work).
*/
void luaE_checkcstack (lua_State *L) {
if (getCcalls(L) == LUAI_MAXCCALLS)
luaG_runerror(L, "C stack overflow");
else if (getCcalls(L) >= (LUAI_MAXCCALLS / 10 * 11))
luaD_throw(L, LUA_ERRERR); /* error while handing stack error */
}
LUAI_FUNC void luaE_incCstack (lua_State *L) {
L->nCcalls++;
if (getCcalls(L) >= LUAI_MAXCCALLS)
luaE_checkcstack(L);
}
static void stack_init (lua_State *L1, lua_State *L) {
int i; CallInfo *ci;
/* initialize stack array */
@ -357,7 +362,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 = 0;
L->nCcalls = 0;
incnny(L); /* main thread is always non yieldable */
g->frealloc = f;
g->ud = ud;

View File

@ -87,49 +87,13 @@
/*
** About 'nCcalls': each thread in Lua (a lua_State) keeps a count of
** how many "C calls" it still can do in the C stack, to avoid C-stack
** overflow. This count is very rough approximation; it considers only
** recursive functions inside the interpreter, as non-recursive calls
** can be considered using a fixed (although unknown) amount of stack
** space.
**
** The count has two parts: the lower part is the count itself; the
** higher part counts the number of non-yieldable calls in the stack.
** (They are together so that we can change both with one instruction.)
**
** Because calls to external C functions can use an unknown amount
** of space (e.g., functions using an auxiliary buffer), calls
** to these functions add more than one to the count (see CSTACKCF).
**
** The proper count excludes the number of CallInfo structures allocated
** by Lua, as a kind of "potential" calls. So, when Lua calls a function
** (and "consumes" one CallInfo), it needs neither to decrement nor to
** check 'nCcalls', as its use of C stack is already accounted for.
** About 'nCcalls': This count has two parts: the lower 16 bits counts
** the number of recursive invocations in the C stack; the higher
** 16 bits counts the number of non-yieldable calls in the stack.
** (They are together so that we can change and save both with one
** instruction.)
*/
/* number of "C stack slots" used by an external C function */
#define CSTACKCF 10
/*
** The C-stack size is sliced in the following zones:
** - larger than CSTACKERR: normal stack;
** - [CSTACKMARK, CSTACKERR]: buffer zone to signal a stack overflow;
** - [CSTACKCF, CSTACKERRMARK]: error-handling zone;
** - below CSTACKERRMARK: buffer zone to signal overflow during overflow;
** (Because the counter can be decremented CSTACKCF at once, we need
** the so called "buffer zones", with at least that size, to properly
** detect a change from one zone to the next.)
*/
#define CSTACKERR (8 * CSTACKCF)
#define CSTACKMARK (CSTACKERR - (CSTACKCF + 2))
#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)
@ -144,7 +108,8 @@
/* Decrement the number of non-yieldable calls */
#define decnny(L) ((L)->nCcalls -= 0x10000)
/* Non-yieldable call increment */
#define nyci (0x10000 | 1)
@ -290,7 +255,6 @@ 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;
@ -314,7 +278,7 @@ struct lua_State {
CallInfo base_ci; /* CallInfo for first level (C calling Lua) */
volatile lua_Hook hook;
ptrdiff_t errfunc; /* current error handling function (stack index) */
l_uint32 nCcalls; /* number of allowed nested C calls - 'nci' */
l_uint32 nCcalls; /* number of nested (non-yieldable | C) calls */
int oldpc; /* last pc traced */
int stacksize;
int basehookcount;
@ -383,11 +347,11 @@ LUAI_FUNC void luaE_freethread (lua_State *L, lua_State *L1);
LUAI_FUNC CallInfo *luaE_extendCI (lua_State *L);
LUAI_FUNC void luaE_freeCI (lua_State *L);
LUAI_FUNC void luaE_shrinkCI (lua_State *L);
LUAI_FUNC void luaE_checkcstack (lua_State *L);
LUAI_FUNC void luaE_incCstack (lua_State *L);
LUAI_FUNC void luaE_warning (lua_State *L, const char *msg, int tocont);
LUAI_FUNC void luaE_warnerror (lua_State *L, const char *where);
#define luaE_exitCcall(L) ((L)->nCcalls++)
#endif

View File

@ -36,21 +36,6 @@
** =====================================================================
*/
/* >>> move back to llimits.h
@@ LUAI_MAXCCALLS defines the maximum depth for nested calls and
** also limits the maximum depth of other recursive algorithms in
** the implementation, such as syntactic analysis. A value too
** large may allow the interpreter to crash (C-stack overflow).
** The default value seems ok for regular machines, but may be
** too high for restricted hardware.
** The test file 'cstack.lua' may help finding a good limit.
** (It will crash with a limit too high.)
*/
#if !defined(LUAI_MAXCCALLS)
#define LUAI_MAXCCALLS 200
#endif
/*
@@ LUA_USE_C89 controls the use of non-ISO-C89 features.
** Define it if you want Lua to avoid the use of a few C99 features

6
lvm.c
View File

@ -1124,7 +1124,7 @@ void luaV_finishOp (lua_State *L) {
void luaV_execute (lua_State *L, CallInfo *ci) {
const CallInfo *origci = ci;
CallInfo * const origci = ci;
LClosure *cl;
TValue *k;
StkId base;
@ -1624,7 +1624,7 @@ void luaV_execute (lua_State *L, CallInfo *ci) {
vmcase(OP_TAILCALL) {
int b = GETARG_B(i); /* number of arguments + 1 (function) */
int nparams1 = GETARG_C(i);
/* delat is virtual 'func' - real 'func' (vararg functions) */
/* delta is virtual 'func' - real 'func' (vararg functions) */
int delta = (nparams1) ? ci->u.l.nextraargs + nparams1 : 0;
if (b != 0)
L->top = ra + b;
@ -1648,7 +1648,7 @@ void luaV_execute (lua_State *L, CallInfo *ci) {
updatetrap(ci);
updatestack(ci); /* stack may have been relocated */
ci->func -= delta;
luaD_poscall(L, ci, cast_int(L->top - ra));
luaD_poscall(L, ci, cast_int(L->top - ra)); /* finish caller */
goto ret;
}
ci->func -= delta;

View File

@ -2436,8 +2436,16 @@ When you interact with the Lua API,
you are responsible for ensuring consistency.
In particular,
@emph{you are responsible for controlling stack overflow}.
You can use the function @Lid{lua_checkstack}
to ensure that the stack has enough space for pushing new elements.
When you call any API function,
you must ensure the stack has enough room to accommodate the results.
There is one exception to the above rule:
When you call a Lua function
without a fixed number of results @seeF{lua_call},
Lua ensures that the stack has enough space for all results.
However, it does not ensure any extra space.
So, before pushing anything on the stack after such a call
you should use @Lid{lua_checkstack}.
Whenever Lua calls C,
it ensures that the stack has space for
@ -2446,13 +2454,9 @@ that is, you can safely push up to @id{LUA_MINSTACK} values into it.
@id{LUA_MINSTACK} is defined as 20,
so that usually you do not have to worry about stack space
unless your code has loops pushing elements onto the stack.
When you call a Lua function
without a fixed number of results @seeF{lua_call},
Lua ensures that the stack has enough space for all results,
but it does not ensure any extra space.
So, before pushing anything on the stack after such a call
you should use @Lid{lua_checkstack}.
Whenever necessary,
you can use the function @Lid{lua_checkstack}
to ensure that the stack has enough space for pushing new elements.
}
@ -2695,7 +2699,7 @@ Therefore, if a @N{C function} @id{foo} calls an API function
and this API function yields
(directly or indirectly by calling another function that yields),
Lua cannot return to @id{foo} any more,
because the @id{longjmp} removes its frame from the C stack.
because the @id{longjmp} removes its frame from the @N{C stack}.
To avoid this kind of problem,
Lua raises an error whenever it tries to yield across an API call,
@ -2719,7 +2723,7 @@ After the thread resumes,
it eventually will finish running the callee function.
However,
the callee function cannot return to the original function,
because its frame in the C stack was destroyed by the yield.
because its frame in the @N{C stack} was destroyed by the yield.
Instead, Lua calls a @def{continuation function},
which was given as an argument to the callee function.
As the name implies,
@ -2841,7 +2845,7 @@ and therefore may raise any errors.
Converts the @x{acceptable index} @id{idx}
into an equivalent @x{absolute index}
(that is, one that does not depend on the stack top).
(that is, one that does not depend on the stack size).
}
@ -4340,7 +4344,7 @@ as if it was already marked.
Note that, both in case of errors and of a regular return,
by the time the @idx{__close} metamethod runs,
the @N{C stack} was already unwound,
so that any automatic C variable declared in the calling function
so that any automatic @N{C variable} declared in the calling function
will be out of scope.
}
@ -4955,20 +4959,6 @@ 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,-}
@ -8756,34 +8746,6 @@ 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.}
}
If a call does not respect some restriction,
it returns a false value.
Otherwise,
the call returns the old limit.
}
@LibEntry{debug.sethook ([thread,] hook, mask [, count])|
Sets the given function as the debug hook.

View File

@ -1,75 +1,29 @@
-- $Id: testes/cstack.lua $
-- See Copyright Notice in file all.lua
do return end
local debug = require "debug"
print"testing C-stack overflow detection"
print"If this test crashes, see its file ('cstack.lua')"
-- Segmentation faults in these tests probably result from a C-stack
-- overflow. To avoid these errors, you can use the function
-- 'debug.setcstacklimit' to set a smaller limit for the use of
-- C stack by Lua. After finding a reliable limit, you might want
-- to recompile Lua with this limit as the value for
-- the constant 'LUAI_MAXCCALLS', which defines the default limit.
-- (The default limit is printed by this test.)
-- overflow. To avoid these errors, you should set a smaller limit for
-- the use of C stack by Lua, by changing the constant 'LUAI_MAXCCALLS'.
-- Alternatively, you can ensure a larger stack for the program.
-- For Linux, a limit up to 30_000 seems Ok. Windows cannot go much
-- higher than 2_000.
-- get and print original limit
local origlimit <const> = debug.setcstacklimit(400)
print("default stack limit: " .. origlimit)
-- Do the tests using the original limit. Or else you may want to change
-- 'currentlimit' to lower values to avoid a seg. fault or to higher
-- values to check whether they are reliable.
local currentlimit <const> = origlimit
debug.setcstacklimit(currentlimit)
print("current stack limit: " .. currentlimit)
local function checkerror (msg, f, ...)
local s, err = pcall(f, ...)
assert(not s and string.find(err, msg))
end
-- auxiliary function to keep 'count' on the screen even if the program
-- crashes.
local count
local back = string.rep("\b", 8)
local function progress ()
count = count + 1
local n = string.format("%-8d", count)
io.stderr:write(back, n) -- erase previous value and write new one
end
do print("testing simple recursion:")
count = 0
local function foo ()
progress()
foo() -- do recursive calls until a stack error (or crash)
end
checkerror("stack overflow", foo)
print("\tfinal count: ", count)
end
do print("testing stack overflow in message handling")
count = 0
local count = 0
local function loop (x, y, z)
progress()
count = count + 1
return 1 + loop(x, y, z)
end
local res, msg = xpcall(loop, loop)
assert(msg == "error in error handling")
print("\tfinal count: ", count)
print("final count: ", count)
end
@ -82,97 +36,66 @@ do print("testing recursion inside pattern matching")
end
local m = f(80)
assert(#m == 80)
checkerror("too complex", f, 200000)
checkerror("too complex", f, 2000)
end
do print("testing stack-overflow in recursive 'gsub'")
count = 0
local count = 0
local function foo ()
progress()
count = count + 1
string.gsub("a", ".", foo)
end
checkerror("stack overflow", foo)
print("\tfinal count: ", count)
print("final count: ", count)
print("testing stack-overflow in recursive 'gsub' with metatables")
count = 0
local count = 0
local t = setmetatable({}, {__index = foo})
foo = function ()
count = count + 1
progress(count)
string.gsub("a", ".", t)
end
checkerror("stack overflow", foo)
print("\tfinal count: ", count)
print("final count: ", count)
end
do -- bug in 5.4.0
print("testing limits in coroutines inside deep calls")
count = 0
local count = 0
local lim = 1000
local function stack (n)
progress()
if n > 0 then return stack(n - 1) + 1
else coroutine.wrap(function ()
count = count + 1
stack(lim)
end)()
end
end
print(xpcall(stack, function () return "ok" end, lim))
local st, msg = xpcall(stack, function () return "ok" end, lim)
assert(not st and msg == "ok")
print("final count: ", count)
end
do print("testing changes in C-stack limit")
do
print("nesting of resuming yielded coroutines")
local count = 0
-- Just an alternative limit, different from the current one
-- (smaller to avoid stack overflows)
local alterlimit <const> = currentlimit * 8 // 10
assert(not debug.setcstacklimit(0)) -- limit too small
assert(not debug.setcstacklimit(50000)) -- limit too large
local co = coroutine.wrap (function ()
return debug.setcstacklimit(alterlimit)
end)
assert(not co()) -- 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
local function body ()
coroutine.yield()
local f = coroutine.wrap(body)
f(); -- start new coroutine (will stop in previous yield)
count = count + 1
f() -- call it recursively
end
-- set limit to 'alterlimit'
assert(debug.setcstacklimit(alterlimit) == currentlimit)
local limalter <const> = check()
-- set a very low limit (given that there are already several active
-- calls to arrive here)
local lowlimit <const> = 38
assert(debug.setcstacklimit(lowlimit) == alterlimit)
-- usable limit is much lower, due to active calls
local actuallow = check()
assert(actuallow < lowlimit - 30)
-- now, add 'lowlimit' extra slots, which should all be available
assert(debug.setcstacklimit(lowlimit + lowlimit) == lowlimit)
local lim2 <const> = check()
assert(lim2 == actuallow + lowlimit)
-- 'setcstacklimit' works inside protected calls. (The new stack
-- limit is kept when 'pcall' returns.)
assert(pcall(function ()
assert(debug.setcstacklimit(alterlimit) == lowlimit * 2)
assert(check() <= limalter)
end))
assert(check() == limalter)
-- restore original limit
assert(debug.setcstacklimit(origlimit) == alterlimit)
local f = coroutine.wrap(body)
f()
assert(not pcall(f))
print("final count: ", count)
end
print'OK'

View File

@ -532,7 +532,8 @@ local function testrep (init, rep, close, repc, finalresult)
end
s = init .. string.rep(rep, 500)
local res, msg = load(s) -- 500 levels not ok
assert(not res and string.find(msg, "too many"))
assert(not res and (string.find(msg, "too many") or
string.find(msg, "overflow")))
end
testrep("local a; a", ",a", "= 1", ",1") -- multiple assignment