lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


Updated patch attached "emergency_gc_fixes.patch".

The patch fixes some bugs where the garbage collector (GC) will free an object 
that is being created and hasn't been put onto the Lua stack or added to a 
table yet.  Also it protects the GC from recursive calls 
(allocator->GC->allocator->GC or GC->allocator->GC).

Since this patch only fixes bug related to running the garbage collector from 
the allocator function, I only recommend using it if you need to restrict the 
amount of memory used by a Lua script.  Also there maybe more bugs that I 
haven't found yet, so use at your own risk.

Also attached is a new version of lua_memlimit.c + some lua scripts for stress 
testing.  The new version adds a lot of checks and debug messages to the 
allocator for debugging GC bugs.  There are some #define's at the top to 
control how many debug messages are enabled and what checks to run.  Does 
anyone know a good set of Lua scripts for testing all Lua features?  Do the 
scripts that come with lua 5.1.3 test all the language features?

Summery of bug that the patch fixes (warning long read):
Function "lua_setfield" in lapi.c
  This function created a Lua string and stored it in a variable on the C 
stack, had to move the string to the Lua stack so the GC wouldn't free the 
string before the key/value was added to the table.  This is something that 
can effect other C libraries that create temporary Lua objects on the C 
stack.  If another allocation happens before the object is put on the Lua 
stack or added to a table the GC might free the object.

Function "lua_gc" in lapi.c
  Added protection from recursive LUA_GCCOLLECT calls.  Also LUA_GCSTOP & 
LUA_GCRESTART can be used to stop a full GC.

Function "f_parser" in ldo.c
  I haven't look to deeply into the Lua parser yet, but it looks like there 
are a lot of places where objects are created and not placed somewhere that 
the GC can find.  So for now I just stop the GC before the parser runs, then 
restart it after.

Function "checkSizes" in lgc.c
  This function was shrinking a temp. buffer used to concatenate strings.  Now 
it will not shrink the size below the in use size needed by the string 
concat. function.

Function "luaE_newthread" in lstate.c
  The new lua_State object wasn't fully allocated before a link to it was 
added to the GC.  I only had to move the call to "luaC_link" to the end of 
the function.

Function "luaH_new" in ltable.c
  The table object wasn't fully allocated before a link to it was added to the 
GC.  I only had to move the call to "luaC_link" to the end of the function.

Function "newlstr" in lstring.c
  This function creates a new Lua string object and adds it to the global 
string hashtable, it also calls "luaS_resize" to expand the string hashtable 
when it gets too crowded.  The change needed here was to delay adding the 
string to the hashtable until after the hashtable was resized.

Function "luaV_concat" in lvm.c
  Same as last patch, the temp. buffer has it's "in use" size updated so the 
GC will not shrink the buffer below the "in use" size.  The "in use" size is 
reset to 0 after the buffer's contents are copied to a new Lua string.

-- 
Robert G. Jakabosky
--- lua-5.1.3.orig_patched/src/lapi.c	2008-05-03 17:08:11.000000000 -0700
+++ lua-5.1.3/src/lapi.c	2008-05-03 16:52:03.000000000 -0700
@@ -656,14 +656,14 @@
 
 LUA_API void lua_setfield (lua_State *L, int idx, const char *k) {
   StkId t;
-  TValue key;
   lua_lock(L);
   api_checknelems(L, 1);
   t = index2adr(L, idx);
   api_checkvalidindex(L, t);
-  setsvalue(L, &key, luaS_new(L, k));
-  luaV_settable(L, t, &key, L->top - 1);
-  L->top--;  /* pop value */
+  setsvalue2s(L, L->top, luaS_new(L, k));
+  api_incr_top(L);
+  luaV_settable(L, t, L->top - 1, L->top - 2);
+  L->top -= 2;  /* pop key and value */
   lua_unlock(L);
 }
 
@@ -911,7 +911,12 @@
       break;
     }
     case LUA_GCCOLLECT: {
-      luaC_fullgc(L);
+      lu_mem old_thres = g->GCthreshold;
+      if(g->GCthreshold != MAX_LUMEM) {
+        g->GCthreshold = MAX_LUMEM;
+        luaC_fullgc(L);
+        g->GCthreshold = old_thres;
+      }
       break;
     }
     case LUA_GCCOUNT: {
--- lua-5.1.3.orig_patched/src/ldo.c	2008-01-18 14:31:22.000000000 -0800
+++ lua-5.1.3/src/ldo.c	2008-05-02 17:47:55.000000000 -0700
@@ -494,6 +494,7 @@
   struct SParser *p = cast(struct SParser *, ud);
   int c = luaZ_lookahead(p->z);
   luaC_checkGC(L);
+	lua_gc(L, LUA_GCSTOP, 0);  /* stop collector during parsing */
   tf = ((c == LUA_SIGNATURE[0]) ? luaU_undump : luaY_parser)(L, p->z,
                                                              &p->buff, p->name);
   cl = luaF_newLclosure(L, tf->nups, hvalue(gt(L)));
@@ -502,6 +503,7 @@
     cl->l.upvals[i] = luaF_newupval(L);
   setclvalue(L, L->top, cl);
   incr_top(L);
+	lua_gc(L, LUA_GCRESTART, 0);
 }
 
 
--- lua-5.1.3.orig_patched/src/lgc.c	2007-12-27 05:02:25.000000000 -0800
+++ lua-5.1.3/src/lgc.c	2008-05-03 17:37:44.000000000 -0700
@@ -437,7 +437,10 @@
   /* check size of buffer */
   if (luaZ_sizebuffer(&g->buff) > LUA_MINBUFFER*2) {  /* buffer too big? */
     size_t newsize = luaZ_sizebuffer(&g->buff) / 2;
-    luaZ_resizebuffer(L, &g->buff, newsize);
+    /* make sure newsize is larger then the buffer's in use size. */
+    newsize = (luaZ_bufflen(&g->buff) > newsize) ? luaZ_bufflen(&g->buff) : newsize;
+    if(newsize < luaZ_sizebuffer(&g->buff))
+      luaZ_resizebuffer(L, &g->buff, newsize);
   }
 }
 
--- lua-5.1.3.orig_patched/src/lstate.c	2008-01-03 07:20:39.000000000 -0800
+++ lua-5.1.3/src/lstate.c	2008-05-03 11:32:22.000000000 -0700
@@ -118,7 +118,6 @@
 
 lua_State *luaE_newthread (lua_State *L) {
   lua_State *L1 = tostate(luaM_malloc(L, state_size(lua_State)));
-  luaC_link(L, obj2gco(L1), LUA_TTHREAD);
   preinit_state(L1, G(L));
   stack_init(L1, L);  /* init stack */
   setobj2n(L, gt(L1), gt(L));  /* share table of globals */
@@ -126,6 +125,7 @@
   L1->basehookcount = L->basehookcount;
   L1->hook = L->hook;
   resethookcount(L1);
+  luaC_link(L, obj2gco(L1), LUA_TTHREAD);
   lua_assert(iswhite(obj2gco(L1)));
   return L1;
 }
--- lua-5.1.3.orig_patched/src/lstring.c	2007-12-27 05:02:25.000000000 -0800
+++ lua-5.1.3/src/lstring.c	2008-05-03 13:20:31.000000000 -0700
@@ -53,6 +53,9 @@
   stringtable *tb;
   if (l+1 > (MAX_SIZET - sizeof(TString))/sizeof(char))
     luaM_toobig(L);
+  tb = &G(L)->strt;
+  if ((tb->nuse + 1) > cast(lu_int32, tb->size) && tb->size <= MAX_INT/2)
+    luaS_resize(L, tb->size*2);  /* too crowded */
   ts = cast(TString *, luaM_malloc(L, (l+1)*sizeof(char)+sizeof(TString)));
   ts->tsv.len = l;
   ts->tsv.hash = h;
@@ -61,13 +64,10 @@
   ts->tsv.reserved = 0;
   memcpy(ts+1, str, l*sizeof(char));
   ((char *)(ts+1))[l] = '\0';  /* ending 0 */
-  tb = &G(L)->strt;
   h = lmod(h, tb->size);
   ts->tsv.next = tb->hash[h];  /* chain new entry */
   tb->hash[h] = obj2gco(ts);
   tb->nuse++;
-  if (tb->nuse > cast(lu_int32, tb->size) && tb->size <= MAX_INT/2)
-    luaS_resize(L, tb->size*2);  /* too crowded */
   return ts;
 }
 
--- lua-5.1.3.orig_patched/src/ltable.c	2007-12-28 07:32:23.000000000 -0800
+++ lua-5.1.3/src/ltable.c	2008-05-02 16:50:49.000000000 -0700
@@ -357,7 +357,6 @@
 
 Table *luaH_new (lua_State *L, int narray, int nhash) {
   Table *t = luaM_new(L, Table);
-  luaC_link(L, obj2gco(t), LUA_TTABLE);
   t->metatable = NULL;
   t->flags = cast_byte(~0);
   /* temporary values (kept only if some malloc fails) */
@@ -367,6 +366,7 @@
   t->node = cast(Node *, dummynode);
   setarrayvector(L, t, narray);
   setnodevector(L, t, nhash);
+  luaC_link(L, obj2gco(t), LUA_TTABLE);
   return t;
 }
 
--- lua-5.1.3.orig_patched/src/lvm.c	2007-12-28 07:32:23.000000000 -0800
+++ lua-5.1.3/src/lvm.c	2008-05-03 12:41:41.000000000 -0700
@@ -295,6 +295,7 @@
         if (l >= MAX_SIZET - tl) luaG_runerror(L, "string length overflow");
         tl += l;
       }
+      G(L)->buff.n = tl;
       buffer = luaZ_openspace(L, &G(L)->buff, tl);
       tl = 0;
       for (i=n; i>0; i--) {  /* concat all strings */
@@ -303,6 +304,7 @@
         tl += l;
       }
       setsvalue2s(L, top-n, luaS_newlstr(L, buffer, tl));
+      luaZ_resetbuffer(&G(L)->buff);
     }
     total -= n-1;  /* got `n' strings to create 1 new */
     last -= n-1;
-- test array_fill.
--

local function stats()
	-- normal lua runtime doesn't have print_memstats function.
	if print_memstats then
		print_memstats()
	end
end

local size = 100
local val = "test"
local arr
local debug = false

stats()
for x=0,10 do
	arr = array_fill(val, size)
	if debug then print("arr len=", #arr) end
	for i=0,#arr do
		local val2 = arr[i]
		assert(val2 == val, string.format("expected '%s' at index %d, instead got '%s'", val, i, val2))
		if debug then print(i, " = ", arr[i]) end
	end
	stats()
end

-- test coroutine.create()
--

local function stats()
	-- normal lua runtime doesn't have print_memstats function.
	if print_memstats then
		print_memstats()
	end
end

function foo()
	print("foo", 1)
	coroutine.yield();
	print("foo", 2)
end

debug = true

stats()
for x=0,10 do
	local co = coroutine.create(foo)
	coroutine.resume(co)
	stats()
	coroutine.resume(co)
	stats()
end

-- test array_fill.
--

local function stats()
	-- normal lua runtime doesn't have print_memstats function.
	collectgarbage("collect")
	if print_memstats then
		print_memstats()
	end
end

local size = 200
local val = "test"
local hashtable
local debug = false
local key

stats()
for x=0,10 do
	hashtable = {}
	for i=0,size do
		key = tostring(i)
		hashtable[key] = val
		if debug then print(key, " = ", hashtable[key]) end
	end
	for i=0,size do
		key = tostring(i)
		local val2 = hashtable[key]
		assert(val2 == val,string.format("expected '%s' at key '%s', instead got '%s'",val,key,val2))
		if debug then print(key, " = ", hashtable[key]) end
	end
	stats()
end

-- test bug: calling lua_gc(L, LUA_GCCOLLECT, 0); from lua_Alloc function.
--

local function stats()
	-- normal lua runtime doesn't have print_memstats function.
	collectgarbage("collect")
	if print_memstats then
		print_memstats()
	end
end

function replace(s, index, char)
	local result = ""
	if index > 1 then
		result = s:sub(1, index - 1)
	end
	result = result .. char
	if index < s:len() then
		result = result .. s:sub(index + 1, -1)
	end
	return result
end

function test(s, index, char, def)
	for i=1,#s do
		local c = s:sub(i,i)
		local expect = def
		if i == index then
			expect = char
		end
		assert(c == expect, string.format("expected '%s' at index %d, but got '%s'",expect,i,c))
	end
end

stats()
local mem_limit = 64 * 1024
local large_string = string.rep("1", mem_limit/10)
local tmp
stats()
for i=50,500,50 do
	tmp = replace(large_string, i, "x")
	--print("tmp len=",#tmp,", tmp=", tmp)
	test(tmp, i, "x", "1")
end
stats()
large_string = nil
tmp = nil
stats()

/*
 * Run Lua scripts with a memory limit.
 *
 * compile:
 * gcc -Wall -llua -o lua_memlimit lua_memlimit.c
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

/*
 * DEBUG_LEVEL -
 * 1  - non-debug GC allocator, minimal debug messages & no checks.  See function 'script_alloc'
 *      it is a very basic memlimit + GC allocator.
 * 5  - debug GC allocator, with some checks enabled.
 * 10 - enables some more debug messages.
 * 20 - enables block size counter & block header.  Extra leak checking + block size validator.
 * 40 - enables a lot of debug messages.
 * 50 - enables all debug messages.
 *
 * ZERO_FREE_MEM -
 * 1 - fills all freed & allocated memory with zeros.
 *
 * ALWAYS_GC -
 * 1 - Do a full garbage collect on all allocations (where nsize > 0)
 *
 * ENABLE_GC -
 * 1 - Enables full garbage collection when max memory limit is hit.
 * 0 - No GC just returns NULL when max memory limit is hit.
 */
#define DEBUG_LEVEL 1
#define ZERO_FREE_MEM 0
#define ALWAYS_GC 0
#define ENABLE_GC 1

#define MAX_BLOCK_SIZE 128 * 1024
#if DEBUG_LEVEL >= 20
#define BLOCK_HEADER	(sizeof(size_t)*2 + 16)
#else
#define BLOCK_HEADER	(0)
#endif
typedef struct {
	char			*name;
	lua_State *L;
	size_t		memused;
	size_t		peak_memused;
	size_t		max_memused;
#if DEBUG_LEVEL >= 20
	void			*gc_ptr;
	int				block_count[MAX_BLOCK_SIZE + 1];
	size_t		max_blocksize;
#endif
} script_info_t;

#if DEBUG_LEVEL >= 20
static void script_check_blocks(script_info_t *info)
{
	int i = 0;
	int c = 0;
	size_t tot = 0;

	for(i = 1; i < MAX_BLOCK_SIZE && i <= info->max_blocksize; i++) {
		c = info->block_count[i];
		if(c > 0) {
#if DEBUG_LEVEL >= 50
			printf("block size=%d, count=%d\n", i, c);
#endif
			tot += i * c;
		}
	}
	c = info->block_count[MAX_BLOCK_SIZE];
	if(c > 0) {
#if DEBUG_LEVEL >= 50
		printf("block size >= %d, count=%d\n", MAX_BLOCK_SIZE, c);
#endif
		tot += i * c;
	}
#if DEBUG_LEVEL >= 40
	printf("mem_total=%zd, max_blocksize=%zd\n", tot, info->max_blocksize);
#endif
	assert(tot == info->memused);
}

static void script_update_block_counts(script_info_t *info, size_t osize, size_t nsize)
{
	if(osize > 0) {
		if(osize < MAX_BLOCK_SIZE) {
			assert(--(info->block_count[osize]) >= 0);
		} else {
			assert(--(info->block_count[MAX_BLOCK_SIZE]) >= 0);
		}
	}
	if(nsize > 0) {
		if(nsize < MAX_BLOCK_SIZE) {
			assert(++(info->block_count[nsize]) >= 0);
		} else {
			assert(++(info->block_count[MAX_BLOCK_SIZE]) >= 0);
		}
	}
}
#endif

static void *script_debug_alloc(void *ud, void *ptr, size_t osize, size_t nsize)
{
	script_info_t *info=(script_info_t *)ud;
	size_t old_size = info->memused;
	int run_gc = 0;
#if DEBUG_LEVEL >= 20
	size_t bsize = -1;
#endif

#if DEBUG_LEVEL >= 40
	if(osize > info->memused) {
		printf("osize(%zd) > memused(%zd)\n", osize, info->memused);
	}
#endif
	assert(osize <= info->memused);

#if DEBUG_LEVEL >= 20
	if(ptr) {
		ptr -= BLOCK_HEADER;
		bsize = *((size_t *)ptr);
		assert(bsize == osize);
		assert(info->gc_ptr != ptr); /* check for second alloc call on same ptr. */
	}
#endif
	info->memused -= osize;
	if (nsize == 0) {
#if DEBUG_LEVEL >= 40
		if(ptr) printf("free 1 (%p), osize=%zd, bsize=%zd\n", ptr, osize, bsize);
#endif
#if DEBUG_LEVEL >= 20
		script_update_block_counts(info, osize, 0);
		script_check_blocks(info);
#endif
#if ZERO_FREE_MEM
		if(osize > 0) {
#if DEBUG_LEVEL >= 45
			printf("1 memset(%p,0,%zd)\n", ptr, osize + BLOCK_HEADER);
#endif
			memset(ptr, 0, osize + BLOCK_HEADER);
		}
#endif
		free(ptr);
		return NULL;
	}
	info->memused += nsize;
	if(nsize > osize && info->memused >= info->max_memused) {
#if ENABLE_GC
		run_gc = 1;
#if ALWAYS_GC
	} else if(info->L != NULL) {
		run_gc = 1;
#endif
	}
	if(run_gc) {
#if DEBUG_LEVEL >= 40
		printf("LOW MEM: 1 osize=%zd, nsize=%zd, used=%zu, peak=%zu, need=%zd\n", osize, nsize,
			info->memused, info->peak_memused, (info->memused - info->max_memused));
#endif
		info->memused = old_size;
#if DEBUG_LEVEL >= 20
		info->gc_ptr = ptr;
#endif
		/* try to free memory by collecting garbage. */
		lua_gc(info->L, LUA_GCCOLLECT, 0);
#if DEBUG_LEVEL >= 20
		info->gc_ptr = NULL;
#endif
#if DEBUG_LEVEL >= 40
		printf("LOW MEM: 2 used=%zu, peak=%zu\n", info->memused, info->peak_memused);
#endif
		/* check memory usage again. */
		old_size = info->memused;
		info->memused -= osize;
		info->memused += nsize;
		if(info->memused >= info->max_memused) {
			info->memused = old_size;
			printf("OUT OF MEMORY: memused=%zd, osize=%zd, nsize=%zd\n", info->memused, osize, nsize);
			return NULL;
		}
#else
		info->memused = old_size;
		return NULL;
#endif
	}
	if(info->memused > info->peak_memused) info->peak_memused = info->memused;
#if ZERO_FREE_MEM
	if(osize > nsize) {
#if DEBUG_LEVEL >= 45
		printf("2 memset(%p,0,%zd)\n", ptr + nsize + BLOCK_HEADER, osize - nsize);
#endif
		memset(ptr + nsize + BLOCK_HEADER, 0, osize - nsize);
	}
#endif
#if DEBUG_LEVEL >= 40
	if(ptr) printf("free 2 (%p), osize=%zd, bsize=%zd\n", ptr, osize, bsize);
#endif
#if DEBUG_LEVEL >= 20
	if(nsize > info->max_blocksize) info->max_blocksize = nsize;
	script_update_block_counts(info, osize, nsize);
	script_check_blocks(info);
#endif
	ptr = realloc(ptr, nsize + BLOCK_HEADER);
#if ZERO_FREE_MEM
	if(osize < nsize) {
#if DEBUG_LEVEL >= 45
		printf("3 memset(%p,0,%zd)\n", ptr + osize + BLOCK_HEADER, nsize - osize);
#endif
		memset(ptr + osize + BLOCK_HEADER, 0, nsize - osize);
	}
#endif
#if DEBUG_LEVEL >= 40
	if(ptr) printf("alloc  (%p), nsize=%zd\n", ptr, nsize);
#endif
#if DEBUG_LEVEL >= 20
	*((size_t *)ptr) = nsize;
	ptr += BLOCK_HEADER;
#endif
	return ptr;
}

static void *script_alloc(void *ud, void *ptr, size_t osize, size_t nsize)
{
	script_info_t *info=(script_info_t *)ud;
	size_t old_size = info->memused;

	info->memused -= osize;
	if (nsize == 0) {
		free(ptr);
		return NULL;
	}
	info->memused += nsize;
	if(nsize > osize && info->memused >= info->max_memused) {
#if ENABLE_GC
		info->memused = old_size;
		/* try to free memory by collecting garbage. */
		lua_gc(info->L, LUA_GCCOLLECT, 0);
		/* check memory usage again. */
		old_size = info->memused;
		info->memused -= osize;
		info->memused += nsize;
		if(info->memused >= info->max_memused) {
			info->memused = old_size;
#if DEBUG_LEVEL >= 1
			printf("OUT OF MEMORY: memused=%zd, osize=%zd, nsize=%zd\n", info->memused, osize, nsize);
#endif
			return NULL;
		}
#else
		info->memused = old_size;
		return NULL;
#endif
	}
	if(info->memused > info->peak_memused) info->peak_memused = info->memused;
	return realloc(ptr, nsize);
}

static int print_memstats(lua_State *L)
{
	script_info_t *info;

	lua_getallocf(L, (void *)(&info));

#if DEBUG_LEVEL >= 10
	printf("%s: before GC memused=%zd, peak_memused=%zd\n", info->name,
		info->memused, info->peak_memused);
	lua_gc(L, LUA_GCCOLLECT, 0);
	printf("%s: after GC  memused=%zd, peak_memused=%zd\n", info->name,
		info->memused, info->peak_memused);
#else
	printf("%s: memused=%zd, peak_memused=%zd\n", info->name,
		info->memused, info->peak_memused);
#endif
	return 0;
}

static int array_fill(lua_State *L)
{
	size_t l;
	const char *s = luaL_checklstring(L, 1, &l);
	int n = luaL_checkint(L, 2);
	int i;
	lua_createtable(L, n, 0);
	for(i=0; i < n; i++) {
		lua_pushstring(L, s);
		lua_rawseti(L, -2, i);
	}
	return 1;
}

static lua_State *create_newstate(script_info_t *info)
{
	lua_State *L;

#if DEBUG_LEVEL >= 5
	L = lua_newstate(script_debug_alloc, info);
#else
	L = lua_newstate(script_alloc, info);
#endif
	if(L == NULL) return L;
	/* load libs */
	info->L = L;
	luaL_openlibs(L);
	lua_register(L,"print_memstats", print_memstats);
	lua_register(L,"array_fill", array_fill);
	return L;
}

static int run_script(char *script_name)
{
	script_info_t *info;
	lua_State *L;
	int status;

	/* run script */
	info = (script_info_t *)calloc(1, sizeof(script_info_t));
	info->max_memused = 64 * 1024;
	info->name = script_name;
	info->memused = 0;
	info->peak_memused = 0;
	/* create lua state & load script file. */
	L=create_newstate(info);
	status = luaL_loadfile(L, info->name);
	if(status != 0) {
		fprintf(stderr,"Failed to load script: %s\n",lua_tostring(L,-1));
		lua_close(L);
		free(info);
		return -1;
	}
	/* execute script. */
	status = lua_pcall(L, 0, LUA_MULTRET, 0);
	if(status != 0) {
		fprintf(stderr,"%s: %s\n", script_name,lua_tostring(L,-1));
	}
	lua_close(L);
	printf("%s: memused=%zd, peak_memused=%zd\n", info->name,
		info->memused, info->peak_memused);
	/* check for memory leak. */
	assert(info->memused == 0);
	free(info);
	return 0;
}

int main(int argc, char *argv[])
{
	int rc = 0;
	int i = 0;

	if(argc < 2) {
		printf("usage: %s <lua script 1> [<lua script 2>]\n", argv[0]);
		exit(1);
	}

	/* run scripts. */
	for(i=1; i < argc && rc == 0; i++) {
		rc = run_script(argv[i]);
	}

	return 0;
}