Browse Source

Bug: GC is not reentrant

As the GC is not reentrant, finalizers should not be able to invoke it.
Roberto Ierusalimschy 3 years ago
parent
commit
0bfc572e51
9 changed files with 57 additions and 27 deletions
  1. 9 8
      lapi.c
  2. 17 2
      lbaselib.c
  3. 7 4
      lgc.c
  4. 9 0
      lgc.h
  5. 2 2
      lstate.c
  6. 1 1
      lstate.h
  7. 6 5
      manual/manual.of
  8. 2 3
      testes/api.lua
  9. 4 2
      testes/gc.lua

+ 9 - 8
lapi.c

@@ -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;
-      g->gcrunning = 1;  /* allow GC to run */
+      lu_byte oldstp = g->gcstp;
+      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: {

+ 17 - 2
lbaselib.c

@@ -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"
-                                           : "generational");
+  if (oldmode == -1)
+    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;
 }
 }
 
 
 
 

+ 7 - 4
lgc.c

@@ -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 - 0
lgc.h

@@ -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

+ 2 - 2
lstate.c

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

+ 1 - 1
lstate.h

@@ -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" */

+ 6 - 5
manual/manual.of

@@ -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.
-Except for that, they can do anything,
-such as raise errors, create new objects,
-or even run the garbage collector.
-However, because they can run in unpredictable times,
+Finalizers cannot yield nor run the garbage collector.
+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])|

+ 2 - 3
testes/api.lua

@@ -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(debug.getmetatable(x).__gc == F)  -- previous GC did not mess this?
+  assert(not collectgarbage())    -- GC during GC (no op)
   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

+ 4 - 2
testes/gc.lua

@@ -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