Bug: GC is not reentrant

As the GC is not reentrant, finalizers should not be able to invoke it.
This commit is contained in:
Roberto Ierusalimschy
2021-12-13 10:41:17 -03:00
parent 1de95e97ef
commit 0bfc572e51
9 changed files with 57 additions and 27 deletions

17
lapi.c
View File

@@ -1136,18 +1136,19 @@ LUA_API int lua_status (lua_State *L) {
LUA_API int lua_gc (lua_State *L, int what, ...) { LUA_API int lua_gc (lua_State *L, int what, ...) {
va_list argp; va_list argp;
int res = 0; int res = 0;
global_State *g; global_State *g = G(L);
if (g->gcstp & GCSTPGC) /* internal stop? */
return -1; /* all options are invalid when stopped */
lua_lock(L); lua_lock(L);
g = G(L);
va_start(argp, what); va_start(argp, what);
switch (what) { switch (what) {
case LUA_GCSTOP: { case LUA_GCSTOP: {
g->gcrunning = 0; g->gcstp = GCSTPUSR; /* stopeed by the user */
break; break;
} }
case LUA_GCRESTART: { case LUA_GCRESTART: {
luaE_setdebt(g, 0); luaE_setdebt(g, 0);
g->gcrunning = 1; g->gcstp = 0; /* (GCSTPGC must be already zero here) */
break; break;
} }
case LUA_GCCOLLECT: { case LUA_GCCOLLECT: {
@@ -1166,8 +1167,8 @@ LUA_API int lua_gc (lua_State *L, int what, ...) {
case LUA_GCSTEP: { case LUA_GCSTEP: {
int data = va_arg(argp, int); int data = va_arg(argp, int);
l_mem debt = 1; /* =1 to signal that it did an actual step */ l_mem debt = 1; /* =1 to signal that it did an actual step */
lu_byte oldrunning = g->gcrunning; lu_byte oldstp = g->gcstp;
g->gcrunning = 1; /* allow GC to run */ g->gcstp = 0; /* allow GC to run (GCSTPGC must be zero here) */
if (data == 0) { if (data == 0) {
luaE_setdebt(g, 0); /* do a basic step */ luaE_setdebt(g, 0); /* do a basic step */
luaC_step(L); luaC_step(L);
@@ -1177,7 +1178,7 @@ LUA_API int lua_gc (lua_State *L, int what, ...) {
luaE_setdebt(g, debt); luaE_setdebt(g, debt);
luaC_checkGC(L); luaC_checkGC(L);
} }
g->gcrunning = oldrunning; /* restore previous state */ g->gcstp = oldstp; /* restore previous state */
if (debt > 0 && g->gcstate == GCSpause) /* end of cycle? */ if (debt > 0 && g->gcstate == GCSpause) /* end of cycle? */
res = 1; /* signal it */ res = 1; /* signal it */
break; break;
@@ -1195,7 +1196,7 @@ LUA_API int lua_gc (lua_State *L, int what, ...) {
break; break;
} }
case LUA_GCISRUNNING: { case LUA_GCISRUNNING: {
res = g->gcrunning; res = gcrunning(g);
break; break;
} }
case LUA_GCGEN: { case LUA_GCGEN: {

View File

@@ -182,12 +182,20 @@ static int luaB_rawset (lua_State *L) {
static int pushmode (lua_State *L, int oldmode) { static int pushmode (lua_State *L, int oldmode) {
lua_pushstring(L, (oldmode == LUA_GCINC) ? "incremental" if (oldmode == -1)
: "generational"); luaL_pushfail(L); /* invalid call to 'lua_gc' */
else
lua_pushstring(L, (oldmode == LUA_GCINC) ? "incremental"
: "generational");
return 1; return 1;
} }
/*
** check whether call to 'lua_gc' was valid (not inside a finalizer)
*/
#define checkvalres(res) { if (res == -1) break; }
static int luaB_collectgarbage (lua_State *L) { static int luaB_collectgarbage (lua_State *L) {
static const char *const opts[] = {"stop", "restart", "collect", static const char *const opts[] = {"stop", "restart", "collect",
"count", "step", "setpause", "setstepmul", "count", "step", "setpause", "setstepmul",
@@ -200,12 +208,14 @@ static int luaB_collectgarbage (lua_State *L) {
case LUA_GCCOUNT: { case LUA_GCCOUNT: {
int k = lua_gc(L, o); int k = lua_gc(L, o);
int b = lua_gc(L, LUA_GCCOUNTB); int b = lua_gc(L, LUA_GCCOUNTB);
checkvalres(k);
lua_pushnumber(L, (lua_Number)k + ((lua_Number)b/1024)); lua_pushnumber(L, (lua_Number)k + ((lua_Number)b/1024));
return 1; return 1;
} }
case LUA_GCSTEP: { case LUA_GCSTEP: {
int step = (int)luaL_optinteger(L, 2, 0); int step = (int)luaL_optinteger(L, 2, 0);
int res = lua_gc(L, o, step); int res = lua_gc(L, o, step);
checkvalres(res);
lua_pushboolean(L, res); lua_pushboolean(L, res);
return 1; return 1;
} }
@@ -213,11 +223,13 @@ static int luaB_collectgarbage (lua_State *L) {
case LUA_GCSETSTEPMUL: { case LUA_GCSETSTEPMUL: {
int p = (int)luaL_optinteger(L, 2, 0); int p = (int)luaL_optinteger(L, 2, 0);
int previous = lua_gc(L, o, p); int previous = lua_gc(L, o, p);
checkvalres(previous);
lua_pushinteger(L, previous); lua_pushinteger(L, previous);
return 1; return 1;
} }
case LUA_GCISRUNNING: { case LUA_GCISRUNNING: {
int res = lua_gc(L, o); int res = lua_gc(L, o);
checkvalres(res);
lua_pushboolean(L, res); lua_pushboolean(L, res);
return 1; return 1;
} }
@@ -234,10 +246,13 @@ static int luaB_collectgarbage (lua_State *L) {
} }
default: { default: {
int res = lua_gc(L, o); int res = lua_gc(L, o);
checkvalres(res);
lua_pushinteger(L, res); lua_pushinteger(L, res);
return 1; return 1;
} }
} }
luaL_pushfail(L); /* invalid call (inside a finalizer) */
return 1;
} }

11
lgc.c
View File

@@ -906,16 +906,16 @@ static void GCTM (lua_State *L) {
if (!notm(tm)) { /* is there a finalizer? */ if (!notm(tm)) { /* is there a finalizer? */
int status; int status;
lu_byte oldah = L->allowhook; lu_byte oldah = L->allowhook;
int running = g->gcrunning; int oldgcstp = g->gcstp;
g->gcstp = GCSTPGC; /* avoid GC steps */
L->allowhook = 0; /* stop debug hooks during GC metamethod */ L->allowhook = 0; /* stop debug hooks during GC metamethod */
g->gcrunning = 0; /* avoid GC steps */
setobj2s(L, L->top++, tm); /* push finalizer... */ setobj2s(L, L->top++, tm); /* push finalizer... */
setobj2s(L, L->top++, &v); /* ... and its argument */ setobj2s(L, L->top++, &v); /* ... and its argument */
L->ci->callstatus |= CIST_FIN; /* will run a finalizer */ L->ci->callstatus |= CIST_FIN; /* will run a finalizer */
status = luaD_pcall(L, dothecall, NULL, savestack(L, L->top - 2), 0); status = luaD_pcall(L, dothecall, NULL, savestack(L, L->top - 2), 0);
L->ci->callstatus &= ~CIST_FIN; /* not running a finalizer anymore */ L->ci->callstatus &= ~CIST_FIN; /* not running a finalizer anymore */
L->allowhook = oldah; /* restore hooks */ L->allowhook = oldah; /* restore hooks */
g->gcrunning = running; /* restore state */ g->gcstp = oldgcstp; /* restore state */
if (l_unlikely(status != LUA_OK)) { /* error while running __gc? */ if (l_unlikely(status != LUA_OK)) { /* error while running __gc? */
luaE_warnerror(L, "__gc metamethod"); luaE_warnerror(L, "__gc metamethod");
L->top--; /* pops error object */ L->top--; /* pops error object */
@@ -1502,9 +1502,11 @@ static void deletelist (lua_State *L, GCObject *p, GCObject *limit) {
*/ */
void luaC_freeallobjects (lua_State *L) { void luaC_freeallobjects (lua_State *L) {
global_State *g = G(L); global_State *g = G(L);
g->gcstp = GCSTPGC;
luaC_changemode(L, KGC_INC); luaC_changemode(L, KGC_INC);
separatetobefnz(g, 1); /* separate all objects with finalizers */ separatetobefnz(g, 1); /* separate all objects with finalizers */
lua_assert(g->finobj == NULL); lua_assert(g->finobj == NULL);
g->gcstp = 0;
callallpendingfinalizers(L); callallpendingfinalizers(L);
deletelist(L, g->allgc, obj2gco(g->mainthread)); deletelist(L, g->allgc, obj2gco(g->mainthread));
deletelist(L, g->finobj, NULL); deletelist(L, g->finobj, NULL);
@@ -1647,6 +1649,7 @@ void luaC_runtilstate (lua_State *L, int statesmask) {
} }
/* /*
** Performs a basic incremental step. The debt and step size are ** Performs a basic incremental step. The debt and step size are
** converted from bytes to "units of work"; then the function loops ** converted from bytes to "units of work"; then the function loops
@@ -1678,7 +1681,7 @@ static void incstep (lua_State *L, global_State *g) {
void luaC_step (lua_State *L) { void luaC_step (lua_State *L) {
global_State *g = G(L); global_State *g = G(L);
lua_assert(!g->gcemergency); lua_assert(!g->gcemergency);
if (g->gcrunning) { /* running? */ if (gcrunning(g)) { /* running? */
if(isdecGCmodegen(g)) if(isdecGCmodegen(g))
genstep(L, g); genstep(L, g);
else else

9
lgc.h
View File

@@ -148,6 +148,15 @@
*/ */
#define isdecGCmodegen(g) (g->gckind == KGC_GEN || g->lastatomic != 0) #define isdecGCmodegen(g) (g->gckind == KGC_GEN || g->lastatomic != 0)
/*
** Control when GC is running:
*/
#define GCSTPUSR 1 /* bit true when GC stopped by user */
#define GCSTPGC 2 /* bit true when GC stopped by itself */
#define gcrunning(g) ((g)->gcstp == 0)
/* /*
** Does one step of collection when debt becomes positive. 'pre'/'pos' ** Does one step of collection when debt becomes positive. 'pre'/'pos'
** allows some adjustments to be done only when needed. macro ** allows some adjustments to be done only when needed. macro

View File

@@ -236,7 +236,7 @@ static void f_luaopen (lua_State *L, void *ud) {
luaS_init(L); luaS_init(L);
luaT_init(L); luaT_init(L);
luaX_init(L); luaX_init(L);
g->gcrunning = 1; /* allow gc */ g->gcstp = 0; /* allow gc */
setnilvalue(&g->nilvalue); /* now state is complete */ setnilvalue(&g->nilvalue); /* now state is complete */
luai_userstateopen(L); luai_userstateopen(L);
} }
@@ -373,7 +373,7 @@ LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
g->ud_warn = NULL; g->ud_warn = NULL;
g->mainthread = L; g->mainthread = L;
g->seed = luai_makeseed(L); g->seed = luai_makeseed(L);
g->gcrunning = 0; /* no GC while building state */ g->gcstp = GCSTPGC; /* no GC while building state */
g->strt.size = g->strt.nuse = 0; g->strt.size = g->strt.nuse = 0;
g->strt.hash = NULL; g->strt.hash = NULL;
setnilvalue(&g->l_registry); setnilvalue(&g->l_registry);

View File

@@ -263,7 +263,7 @@ typedef struct global_State {
lu_byte gcstopem; /* stops emergency collections */ lu_byte gcstopem; /* stops emergency collections */
lu_byte genminormul; /* control for minor generational collections */ lu_byte genminormul; /* control for minor generational collections */
lu_byte genmajormul; /* control for major generational collections */ lu_byte genmajormul; /* control for major generational collections */
lu_byte gcrunning; /* true if GC is running */ lu_byte gcstp; /* control whether GC is running */
lu_byte gcemergency; /* true if this is an emergency collection */ lu_byte gcemergency; /* true if this is an emergency collection */
lu_byte gcpause; /* size of pause between successive GCs */ lu_byte gcpause; /* size of pause between successive GCs */
lu_byte gcstepmul; /* GC "speed" */ lu_byte gcstepmul; /* GC "speed" */

View File

@@ -787,11 +787,8 @@ following the reverse order that they were marked.
If any finalizer marks objects for collection during that phase, If any finalizer marks objects for collection during that phase,
these marks have no effect. these marks have no effect.
Finalizers cannot yield. Finalizers cannot yield nor run the garbage collector.
Except for that, they can do anything, Because they can run in unpredictable times,
such as raise errors, create new objects,
or even run the garbage collector.
However, because they can run in unpredictable times,
it is good practice to restrict each finalizer it is good practice to restrict each finalizer
to the minimum necessary to properly release to the minimum necessary to properly release
its associated resource. its associated resource.
@@ -3276,6 +3273,8 @@ Returns the previous mode (@id{LUA_GCGEN} or @id{LUA_GCINC}).
For more details about these options, For more details about these options,
see @Lid{collectgarbage}. see @Lid{collectgarbage}.
This function should not be called by a finalizer.
} }
@APIEntry{lua_Alloc lua_getallocf (lua_State *L, void **ud);| @APIEntry{lua_Alloc lua_getallocf (lua_State *L, void **ud);|
@@ -6233,6 +6232,8 @@ A zero means to not change that value.
See @See{GC} for more details about garbage collection See @See{GC} for more details about garbage collection
and some of these options. and some of these options.
This function should not be called by a finalizer.
} }
@LibEntry{dofile ([filename])| @LibEntry{dofile ([filename])|

View File

@@ -804,15 +804,14 @@ F = function (x)
d = nil d = nil
assert(debug.getmetatable(x).__gc == F) assert(debug.getmetatable(x).__gc == F)
assert(load("table.insert({}, {})"))() -- create more garbage assert(load("table.insert({}, {})"))() -- create more garbage
collectgarbage() -- force a GC during GC assert(not collectgarbage()) -- GC during GC (no op)
assert(debug.getmetatable(x).__gc == F) -- previous GC did not mess this?
local dummy = {} -- create more garbage during GC local dummy = {} -- create more garbage during GC
if A ~= nil then if A ~= nil then
assert(type(A) == "userdata") assert(type(A) == "userdata")
assert(T.udataval(A) == B) assert(T.udataval(A) == B)
debug.getmetatable(A) -- just access it debug.getmetatable(A) -- just access it
end end
A = x -- ressucita userdata A = x -- ressurect userdata
B = udval B = udval
return 1,2,3 return 1,2,3
end end

View File

@@ -676,11 +676,13 @@ end
-- just to make sure -- just to make sure
assert(collectgarbage'isrunning') assert(collectgarbage'isrunning')
do -- check that the collector is reentrant in incremental mode do -- check that the collector is not reentrant in incremental mode
local res = true
setmetatable({}, {__gc = function () setmetatable({}, {__gc = function ()
collectgarbage() res = collectgarbage()
end}) end})
collectgarbage() collectgarbage()
assert(not res)
end end