CVE-2020-24371 (published 2020-08-17T17:15Z)

lgc.c in Lua 5.4.0 mishandles the interaction between barriers and the sweep phase, leading to a memory access violation involving collectgarbage.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:N/A:P)
CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L)

Weakness information:
cwe_id: CWE-763
cwe_name: Release of Invalid Pointer or Reference
description: The product attempts to return a memory resource to the system, but it calls the wrong release function or calls the appropriate release function incorrectly.
extended_description: 
            This weakness can take several forms, such as:
               
                  The memory was allocated, explicitly or implicitly, via one memory management method and deallocated using a different, non-compatible function (CWE-762).
                  The function calls or memory management routines chosen are appropriate, however they are used incorrectly, such as in CWE-761.
               
         
         

Fix information:
repo_url: https://github.com/lua/lua
commit_details: repo_url: https://github.com/lua/lua
author: Roberto Ierusalimschy
author_date: 2020-07-27 10:26:20-03:00
msg: Fixed bug: barriers cannot be active during sweep

Barriers cannot be active during sweep, even in generational mode.
(Although gen. mode is not incremental, it can hit a barrier when
deleting a thread and closing its upvalues.)  The colors of objects are
being changed during sweep and, therefore, cannot be trusted.
repository: repo_url: https://github.com/lua/lua
repo_name: lua/lua
description: A copy of the Lua development repository, as seen by the Lua team. Mirrored irregularly. Please DO NOT send pull requests or any other stuff. All communication should be through the Lua mailing list https://www.lua.org/lua-l.html
repo_language: C
owner: lua
file_changes: filename: lgc.c
change_type: ModificationType.MODIFY
diff: @@ -181,14 +181,17 @@ static int iscleared (global_State *g, const GCObject *o) {
 
 
 /*
-** barrier that moves collector forward, that is, mark the white object
-** 'v' being pointed by the black object 'o'. (If in sweep phase, clear
-** the black object to white [sweep it] to avoid other barrier calls for
-** this same object.) In the generational mode, 'v' must also become
-** old, if 'o' is old; however, it cannot be changed directly to OLD,
-** because it may still point to non-old objects. So, it is marked as
-** OLD0. In the next cycle it will become OLD1, and in the next it
-** will finally become OLD (regular old).
+** Barrier that moves collector forward, that is, marks the white object
+** 'v' being pointed by the black object 'o'.  In the generational
+** mode, 'v' must also become old, if 'o' is old; however, it cannot
+** be changed directly to OLD, because it may still point to non-old
+** objects. So, it is marked as OLD0. In the next cycle it will become
+** OLD1, and in the next it will finally become OLD (regular old). By
+** then, any object it points to will also be old.  If called in the
+** incremental sweep phase, it clears the black object to white (sweep
+** it) to avoid other barrier calls for this same object. (That cannot
+** be done is generational mode, as its sweep does not distinguish
+** whites from deads.)
 */
 void luaC_barrier_ (lua_State *L, GCObject *o, GCObject *v) {
   global_State *g = G(L);
@@ -202,7 +205,8 @@ void luaC_barrier_ (lua_State *L, GCObject *o, GCObject *v) {
   }
   else {  /* sweep phase */
     lua_assert(issweepphase(g));
-    makewhite(g, o);  /* mark main obj. as white to avoid other barriers */
+    if (g->gckind == KGC_INC)  /* incremental mode? */
+      makewhite(g, o);  /* mark 'o' as white to avoid other barriers */
   }
 }
 
@@ -324,10 +328,15 @@ static lu_mem markbeingfnz (global_State *g) {
 
 
 /*
-** Mark all values stored in marked open upvalues from non-marked threads.
-** (Values from marked threads were already marked when traversing the
-** thread.) Remove from the list threads that no longer have upvalues and
-** not-marked threads.
+** For each non-marked thread, simulates a barrier between each open
+** upvalue and its value. (If the thread is collected, the value will be
+** assigned to the upvalue, but then it can be too late for the barrier
+** to act. The "barrier" does not need to check colors: A non-marked
+** thread must be young; upvalues cannot be older than their threads; so
+** any visited upvalue must be young too.) Also removes the thread from
+** the list, as it was already visited. Removes also threads with no
+** upvalues, as they have nothing to be checked. (If the thread gets an
+** upvalue later, it will be linked in the list again.)
 */
 static int remarkupvals (global_State *g) {
   lua_State *thread;
@@ -340,9 +349,11 @@ static int remarkupvals (global_State *g) {
       p = &thread->twups;  /* keep marked thread with upvalues in the list */
     else {  /* thread is not marked or without upvalues */
       UpVal *uv;
+      lua_assert(!isold(thread) || thread->openupval == NULL);
       *p = thread->twups;  /* remove thread from the list */
       thread->twups = thread;  /* mark that it is out of list */
       for (uv = thread->openupval; uv != NULL; uv = uv->u.open.next) {
+        lua_assert(getage(uv) <= getage(thread));
         work++;
         if (!iswhite(uv))  /* upvalue already visited? */
           markvalue(g, uv->v);  /* mark its value */
@@ -995,6 +1006,9 @@ static void sweep2old (lua_State *L, GCObject **p) {
 ** during the sweep. So, any white object must be dead.) For
 ** non-dead objects, advance their ages and clear the color of
 ** new objects. (Old objects keep their colors.)
+** The ages of G_TOUCHED1 and G_TOUCHED2 objects will advance
+** in 'correctgraylist'. (That function will also remove objects
+** turned white here from any gray list.)
 */
 static GCObject **sweepgen (lua_State *L, global_State *g, GCObject **p,
                             GCObject *limit) {
@@ -1055,16 +1069,16 @@ static GCObject **correctgraylist (GCObject **p) {
           lua_assert(isgray(curr));
           gray2black(curr);  /* make it black, for next barrier */
           changeage(curr, G_TOUCHED1, G_TOUCHED2);
-          p = next;  /* go to next element */
+          p = next;  /* keep it in the list and go to next element */
         }
-        else {  /* not touched in this cycle */
+        else {  /* everything else is removed */
+          /* white objects are simply removed */
           if (!iswhite(curr)) {  /* not white? */
             lua_assert(isold(curr));
             if (getage(curr) == G_TOUCHED2)  /* advance from G_TOUCHED2... */
               changeage(curr, G_TOUCHED2, G_OLD);  /* ... to G_OLD */
             gray2black(curr);  /* make it black */
           }
-          /* else, object is white: just remove it from this list */
           *p = *next;  /* remove 'curr' from gray list */
         }
         break;
@@ -1143,6 +1157,7 @@ static void youngcollection (lua_State *L, global_State *g) {
   atomic(L);
 
   /* sweep nursery and get a pointer to its last live element */
+  g->gcstate = GCSswpallgc;
   psurvival = sweepgen(L, g, &g->allgc, g->survival);
   /* sweep 'survival' and 'old' */
   sweepgen(L, g, psurvival, g->reallyold);
@@ -1166,6 +1181,7 @@ static void youngcollection (lua_State *L, global_State *g) {
 
 static void atomic2gen (lua_State *L, global_State *g) {
   /* sweep all elements making them old */
+  g->gcstate = GCSswpallgc;
   sweep2old(L, &g->allgc);
   /* everything alive now is old */
   g->reallyold = g->old = g->survival = g->allgc;

programming_language: C
filename: gengc.lua
change_type: ModificationType.MODIFY
diff: @@ -57,13 +57,39 @@ do   -- bug in 5.4.0
   local obj = {}     -- create a new object
   collectgarbage("step", 0)   -- make it a survival
   assert(not T or T.gcage(obj) == "survival")
-  setmetatable(obj, {__gc = gcf, x = "ok"})   -- create its metatable
+  setmetatable(obj, {__gc = gcf, x = "+"})   -- create its metatable
   assert(not T or T.gcage(getmetatable(obj)) == "new")
   obj = nil   -- clear object
   collectgarbage("step", 0)   -- will call obj's finalizer
 end
 
 
+do   -- another bug in 5.4.0
+  local old = {10}
+  collectgarbage()   -- make 'old' old
+  local co = coroutine.create(
+    function ()
+      local x = nil
+      local f = function ()
+                  return x[1]
+                end
+      x = coroutine.yield(f)
+      coroutine.yield()
+    end
+  )
+  local _, f = coroutine.resume(co)   -- create closure over 'x' in coroutine
+  collectgarbage("step", 0)   -- make upvalue a survival
+  old[1] = {"hello"}    -- 'old' go to grayagain as 'touched1'
+  coroutine.resume(co, {123})     -- its value will be new
+  co = nil
+  collectgarbage("step", 0)   -- hit the barrier
+  assert(f() == 123 and old[1][1] == "hello")
+  collectgarbage("step", 0)   -- run the collector once more
+  -- make sure old[1] was not collected
+  assert(f() == 123 and old[1][1] == "hello")
+end
+
+
 if T == nil then
   (Message or print)('\n >>> testC not active: \z
                              skipping some generational tests <<<\n')

programming_language: Lua