From 578ae5745cecee56d48795cd4ae1eaf13618715c Mon Sep 17 00:00:00 2001 From: Roberto I Date: Tue, 23 Dec 2025 14:44:06 -0300 Subject: [PATCH 01/47] Details typo in comment + formatting + logical 'and' was written as a bitwise operation (makes code more fragile) --- lapi.c | 2 +- lgc.c | 2 +- lstrlib.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lapi.c b/lapi.c index 27fa5247..42c8fcdd 100644 --- a/lapi.c +++ b/lapi.c @@ -366,7 +366,7 @@ LUA_API int lua_compare (lua_State *L, int index1, int index2, int op) { } -LUA_API unsigned (lua_numbertocstring) (lua_State *L, int idx, char *buff) { +LUA_API unsigned lua_numbertocstring (lua_State *L, int idx, char *buff) { const TValue *o = index2value(L, idx); if (ttisnumber(o)) { unsigned len = luaO_tostringbuff(o, buff); diff --git a/lgc.c b/lgc.c index c64d74b8..f1d9a7ce 100644 --- a/lgc.c +++ b/lgc.c @@ -1672,7 +1672,7 @@ static l_mem singlestep (lua_State *L, int fast) { GCTM(L); /* call one finalizer */ stepresult = CWUFIN; } - else { /* no more finalizers or emergency mode or no enough stack + else { /* no more finalizers or emergency mode or not enough stack to run finalizers */ g->gcstate = GCSpause; /* finish collection */ stepresult = step2pause; diff --git a/lstrlib.c b/lstrlib.c index 23df839e..e26eb1a8 100644 --- a/lstrlib.c +++ b/lstrlib.c @@ -968,7 +968,7 @@ static int str_gsub (lua_State *L) { reprepstate(&ms); /* (re)prepare state for new match */ if ((e = match(&ms, src, p)) != NULL && e != lastmatch) { /* match? */ n++; - changed = add_value(&ms, &b, src, e, tr) | changed; + changed = add_value(&ms, &b, src, e, tr) || changed; src = lastmatch = e; } else if (src < ms.src_end) /* otherwise, skip one character */ From 632a71b24d8661228a726deb5e1698e9638f96d8 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Sat, 27 Dec 2025 16:22:13 -0300 Subject: [PATCH 02/47] BUG: Arithmetic overflow in 'collectgarbage"step"' The computation of a new debt could overflow when we give a too large step to 'collectgarbage"step"' and the current debt was already negative. This is only an issue if your platform cares for it or if you compile Lua with an option like '-fsanitize=undefined'. --- lapi.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lapi.c b/lapi.c index 42c8fcdd..9b6ca1ec 100644 --- a/lapi.c +++ b/lapi.c @@ -1201,11 +1201,16 @@ LUA_API int lua_gc (lua_State *L, int what, ...) { case LUA_GCSTEP: { lu_byte oldstp = g->gcstp; l_mem n = cast(l_mem, va_arg(argp, size_t)); + l_mem newdebt; int work = 0; /* true if GC did some work */ g->gcstp = 0; /* allow GC to run (other bits must be zero here) */ if (n <= 0) - n = g->GCdebt; /* force to run one basic step */ - luaE_setdebt(g, g->GCdebt - n); + newdebt = 0; /* force to run one basic step */ + else if (g->GCdebt >= n - MAX_LMEM) /* no overflow? */ + newdebt = g->GCdebt - n; + else /* overflow */ + newdebt = -MAX_LMEM; /* set debt to miminum value */ + luaE_setdebt(g, newdebt); luaC_condGC(L, (void)0, work = 1); if (work && g->gcstate == GCSpause) /* end of cycle? */ res = 1; /* signal it */ From c4e2c91973fed04e7da940c00c92f10f9eb0f9ec Mon Sep 17 00:00:00 2001 From: Roberto I Date: Tue, 30 Dec 2025 10:50:49 -0300 Subject: [PATCH 03/47] Details Some comments still talked about bit 'isrealasize', which has been removed. --- ltable.c | 7 +++---- ltm.h | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ltable.c b/ltable.c index b7f88f6f..2f61be84 100644 --- a/ltable.c +++ b/ltable.c @@ -651,10 +651,9 @@ static void reinserthash (lua_State *L, Table *ot, Table *t) { /* -** Exchange the hash part of 't1' and 't2'. (In 'flags', only the -** dummy bit must be exchanged: The 'isrealasize' is not related -** to the hash part, and the metamethod bits do not change during -** a resize, so the "real" table can keep their values.) +** Exchange the hash part of 't1' and 't2'. (In 'flags', only the dummy +** bit must be exchanged: The metamethod bits do not change during a +** resize, so the "real" table can keep their values.) */ static void exchangehashpart (Table *t1, Table *t2) { lu_byte lsizenode = t1->lsizenode; diff --git a/ltm.h b/ltm.h index 07fc8c1c..afc7ad00 100644 --- a/ltm.h +++ b/ltm.h @@ -49,7 +49,7 @@ typedef enum { ** Mask with 1 in all fast-access methods. A 1 in any of these bits ** in the flag of a (meta)table means the metatable does not have the ** corresponding metamethod field. (Bit 6 of the flag indicates that -** the table is using the dummy node; bit 7 is used for 'isrealasize'.) +** the table is using the dummy node.) */ #define maskflags cast_byte(~(~0u << (TM_EQ + 1))) From 962f444a755882ecfc24ca7e96ffe193d64ed12d Mon Sep 17 00:00:00 2001 From: Roberto I Date: Sun, 4 Jan 2026 16:27:54 -0300 Subject: [PATCH 04/47] Details In an assignment like 'a = &b', is looks suspicious if 'a' has a scope larger than 'b'. --- ltable.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ltable.c b/ltable.c index 2f61be84..2f2b5c1f 100644 --- a/ltable.c +++ b/ltable.c @@ -1155,14 +1155,15 @@ void luaH_finishset (lua_State *L, Table *t, const TValue *key, lua_assert(hres != HOK); if (hres == HNOTFOUND) { TValue aux; + const TValue *actk = key; /* actual key to insert */ if (l_unlikely(ttisnil(key))) luaG_runerror(L, "table index is nil"); else if (ttisfloat(key)) { lua_Number f = fltvalue(key); lua_Integer k; - if (luaV_flttointeger(f, &k, F2Ieq)) { - setivalue(&aux, k); /* key is equal to an integer */ - key = &aux; /* insert it as an integer */ + if (luaV_flttointeger(f, &k, F2Ieq)) { /* is key equal to an integer? */ + setivalue(&aux, k); + actk = &aux; /* use the integer as the key */ } else if (l_unlikely(luai_numisnan(f))) luaG_runerror(L, "table index is NaN"); @@ -1175,7 +1176,7 @@ void luaH_finishset (lua_State *L, Table *t, const TValue *key, L->top.p--; return; } - luaH_newkey(L, t, key, value); + luaH_newkey(L, t, actk, value); } else if (hres > 0) { /* regular Node? */ setobj2t(L, gval(gnode(t, hres - HFIRSTNODE)), value); From 45c7ae5b1b05069543fe1710454c651350bc1c42 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Sun, 4 Jan 2026 16:31:17 -0300 Subject: [PATCH 05/47] BUG: Possible overflow in 'string.packsize' 'string.packsize' can overflow result in 32-bit machines using 64-bit integers, as LUA_MAXINTEGER may not fit into size_t. --- lstrlib.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lstrlib.c b/lstrlib.c index e26eb1a8..06ea10d9 100644 --- a/lstrlib.c +++ b/lstrlib.c @@ -1726,7 +1726,7 @@ static int str_packsize (lua_State *L) { luaL_argcheck(L, opt != Kstring && opt != Kzstr, 1, "variable-length format"); size += ntoalign; /* total space used by option */ - luaL_argcheck(L, totalsize <= LUA_MAXINTEGER - size, + luaL_argcheck(L, totalsize <= MAX_SIZE - size, 1, "format result too large"); totalsize += size; } From 5cfc725a8b61a6f96c7324f60ac26739315095ba Mon Sep 17 00:00:00 2001 From: Roberto I Date: Sun, 4 Jan 2026 16:39:22 -0300 Subject: [PATCH 06/47] Special case for 'string.rep' over an empty string --- lauxlib.h | 6 +++--- lstrlib.c | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lauxlib.h b/lauxlib.h index 7f1d3ca1..2d015362 100644 --- a/lauxlib.h +++ b/lauxlib.h @@ -81,8 +81,8 @@ LUALIB_API int (luaL_checkoption) (lua_State *L, int arg, const char *def, LUALIB_API int (luaL_fileresult) (lua_State *L, int stat, const char *fname); LUALIB_API int (luaL_execresult) (lua_State *L, int stat); -LUALIB_API void *luaL_alloc (void *ud, void *ptr, size_t osize, - size_t nsize); +LUALIB_API void *(luaL_alloc) (void *ud, void *ptr, size_t osize, + size_t nsize); /* predefined references */ @@ -103,7 +103,7 @@ LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s); LUALIB_API lua_State *(luaL_newstate) (void); -LUALIB_API unsigned luaL_makeseed (lua_State *L); +LUALIB_API unsigned (luaL_makeseed) (lua_State *L); LUALIB_API lua_Integer (luaL_len) (lua_State *L, int idx); diff --git a/lstrlib.c b/lstrlib.c index 06ea10d9..874cec80 100644 --- a/lstrlib.c +++ b/lstrlib.c @@ -141,8 +141,8 @@ static int str_rep (lua_State *L) { const char *s = luaL_checklstring(L, 1, &len); lua_Integer n = luaL_checkinteger(L, 2); const char *sep = luaL_optlstring(L, 3, "", &lsep); - if (n <= 0) - lua_pushliteral(L, ""); + if (n <= 0 || (len | lsep) == 0) + lua_pushliteral(L, ""); /* no repetitions or both strings empty */ else if (l_unlikely(len > MAX_SIZE - lsep || cast_st2S(len + lsep) > cast_st2S(MAX_SIZE) / n)) return luaL_error(L, "resulting string too large"); From 2a7cf4f319fc276f4554a8f6364e6b1ba4eb2ded Mon Sep 17 00:00:00 2001 From: Roberto I Date: Sun, 11 Jan 2026 15:36:03 -0300 Subject: [PATCH 07/47] More effort in avoiding errors in finalizers Before calling a finalizer, Lua not only checks stack limits, but actually ensures that a minimum number of slots are already allocated for the call. (If it cannot ensure that, it postpones the finalizer.) That avoids finalizers not running due to memory errors that the programmer cannot control. --- ldo.c | 20 ++++++++++++++------ lgc.c | 2 +- lstate.c | 17 +++++++++++------ lstate.h | 2 +- ltests.c | 23 +++++++++++++++++++++++ testes/gc.lua | 42 ++++++++++++++++++++++++++++++++++++++++++ testes/memerr.lua | 19 +++++++++++++++++++ testes/tracegc.lua | 9 +++++++-- 8 files changed, 118 insertions(+), 16 deletions(-) diff --git a/ldo.c b/ldo.c index 6d0184ec..12e0364b 100644 --- a/ldo.c +++ b/ldo.c @@ -221,13 +221,21 @@ l_noret luaD_errerr (lua_State *L) { /* -** Check whether stack has enough space to run a simple function (such -** as a finalizer): At least BASIC_STACK_SIZE in the Lua stack and -** 2 slots in the C stack. +** Check whether stacks have enough space to run a simple function (such +** as a finalizer): At least BASIC_STACK_SIZE in the Lua stack, two +** available CallInfos, and two "slots" in the C stack. */ int luaD_checkminstack (lua_State *L) { - return ((stacksize(L) < MAXSTACK - BASIC_STACK_SIZE) && - (getCcalls(L) < LUAI_MAXCCALLS - 2)); + if (getCcalls(L) >= LUAI_MAXCCALLS - 2) + return 0; /* not enough C-stack slots */ + if (L->ci->next == NULL && luaE_extendCI(L, 0) == NULL) + return 0; /* unable to allocate first ci */ + if (L->ci->next->next == NULL && luaE_extendCI(L, 0) == NULL) + return 0; /* unable to allocate second ci */ + if (L->stack_last.p - L->top.p >= BASIC_STACK_SIZE) + return 1; /* enough (BASIC_STACK_SIZE) free slots in the Lua stack */ + else /* try to grow stack to a size with enough free slots */ + return luaD_growstack(L, BASIC_STACK_SIZE, 0); } @@ -616,7 +624,7 @@ void luaD_poscall (lua_State *L, CallInfo *ci, int nres) { -#define next_ci(L) (L->ci->next ? L->ci->next : luaE_extendCI(L)) +#define next_ci(L) (L->ci->next ? L->ci->next : luaE_extendCI(L, 1)) /* diff --git a/lgc.c b/lgc.c index f1d9a7ce..0f89451c 100644 --- a/lgc.c +++ b/lgc.c @@ -1293,7 +1293,7 @@ static void finishgencycle (lua_State *L, global_State *g) { correctgraylists(g); checkSizes(L, g); g->gcstate = GCSpropagate; /* skip restart */ - if (!g->gcemergency && luaD_checkminstack(L)) + if (g->tobefnz != NULL && !g->gcemergency && luaD_checkminstack(L)) callallpendingfinalizers(L); } diff --git a/lstate.c b/lstate.c index 70a11aae..7d341991 100644 --- a/lstate.c +++ b/lstate.c @@ -68,14 +68,19 @@ void luaE_setdebt (global_State *g, l_mem debt) { } -CallInfo *luaE_extendCI (lua_State *L) { +CallInfo *luaE_extendCI (lua_State *L, int err) { CallInfo *ci; - lua_assert(L->ci->next == NULL); - ci = luaM_new(L, CallInfo); - lua_assert(L->ci->next == NULL); - L->ci->next = ci; + ci = luaM_reallocvector(L, NULL, 0, 1, CallInfo); + if (l_unlikely(ci == NULL)) { /* allocation failed? */ + if (err) + luaM_error(L); /* raise the error */ + return NULL; /* else only report it */ + } + ci->next = L->ci->next; ci->previous = L->ci; - ci->next = NULL; + L->ci->next = ci; + if (ci->next) + ci->next->previous = ci; ci->u.l.trap = 0; L->nci++; return ci; diff --git a/lstate.h b/lstate.h index 20dc4d24..01387283 100644 --- a/lstate.h +++ b/lstate.h @@ -438,7 +438,7 @@ union GCUnion { LUAI_FUNC void luaE_setdebt (global_State *g, l_mem debt); LUAI_FUNC void luaE_freethread (lua_State *L, lua_State *L1); LUAI_FUNC lu_mem luaE_threadsize (lua_State *L); -LUAI_FUNC CallInfo *luaE_extendCI (lua_State *L); +LUAI_FUNC CallInfo *luaE_extendCI (lua_State *L, int err); LUAI_FUNC void luaE_shrinkCI (lua_State *L); LUAI_FUNC void luaE_checkcstack (lua_State *L); LUAI_FUNC void luaE_incCstack (lua_State *L); diff --git a/ltests.c b/ltests.c index c4905f94..ce2b20ca 100644 --- a/ltests.c +++ b/ltests.c @@ -1106,6 +1106,27 @@ static int stacklevel (lua_State *L) { } +static int resetCI (lua_State *L) { + CallInfo *ci = L->ci; + while (ci->next != NULL) { + CallInfo *tofree = ci->next; + ci->next = ci->next->next; + luaM_free(L, tofree); + L->nci--; + } + return 0; +} + + +static int reallocstack (lua_State *L) { + int n = cast_int(luaL_checkinteger(L, 1)); + lua_lock(L); + luaD_reallocstack(L, cast_int(L->top.p - L->stack.p) + n, 1); + lua_unlock(L); + return 0; +} + + static int table_query (lua_State *L) { const Table *t; int i = cast_int(luaL_optinteger(L, 2, -1)); @@ -2182,6 +2203,8 @@ static const struct luaL_Reg tests_funcs[] = { {"s2d", s2d}, {"sethook", sethook}, {"stacklevel", stacklevel}, + {"resetCI", resetCI}, + {"reallocstack", reallocstack}, {"sizes", get_sizes}, {"testC", testC}, {"makeCfunc", makeCfunc}, diff --git a/testes/gc.lua b/testes/gc.lua index 62713dac..e50d9029 100644 --- a/testes/gc.lua +++ b/testes/gc.lua @@ -707,4 +707,46 @@ end collectgarbage(oldmode) + +if T then + print("testing stack issues when calling finalizers") + + local X + local obj + + local function initobj () + X = false + obj = setmetatable({}, {__gc = function () X = true end}) + end + + local function loop (n) + if n > 0 then loop(n - 1) end + end + + -- should not try to call finalizer without a CallInfo available + initobj() + loop(20) -- ensure stack space + T.resetCI() -- remove extra CallInfos + T.alloccount(0) -- cannot allocate more CallInfos + obj = nil + collectgarbage() -- will not call finalizer + T.alloccount() + assert(X == false) + collectgarbage() -- now will call finalizer (it was still pending) + assert(X == true) + + -- should not try to call finalizer without stack space available + initobj() + loop(5) -- ensure enough CallInfos + T.reallocstack(0) -- remove extra stack slots + T.alloccount(0) -- cannot reallocate stack + obj = nil + collectgarbage() -- will not call finalizer + T.alloccount() + assert(X == false) + collectgarbage() -- now will call finalizer (it was still pending) + assert(X == true) +end + + print('OK') diff --git a/testes/memerr.lua b/testes/memerr.lua index 9c940ca7..a55514a9 100644 --- a/testes/memerr.lua +++ b/testes/memerr.lua @@ -282,6 +282,25 @@ testamem("growing stack", function () return foo(100) end) + +collectgarbage() +collectgarbage() +global io, T, setmetatable, collectgarbage, print + +local Count = 0 +testamem("finalizers", function () + local X = false + local obj = setmetatable({}, {__gc = function () X = true end}) + obj = nil + T.resetCI() -- remove extra CallInfos + T.reallocstack(18) -- remove extra stack slots + Count = Count + 1 + io.stderr:write(Count, "\n") + T.trick(io) + collectgarbage() + return X +end) + -- }================================================================== diff --git a/testes/tracegc.lua b/testes/tracegc.lua index a8c929df..c1154f90 100644 --- a/testes/tracegc.lua +++ b/testes/tracegc.lua @@ -1,10 +1,15 @@ -- track collections + local M = {} -- import list -local setmetatable, stderr, collectgarbage = - setmetatable, io.stderr, collectgarbage +local stderr, collectgarbage = io.stderr, collectgarbage + +-- the debug version of setmetatable does not create any object (such as +-- a '__metatable' string), and so it is more appropriate to be used in +-- a finalizer +local setmetatable = require"debug".setmetatable global none From f5d1e8639bf5df24c761602354218df21f796a30 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Fri, 16 Jan 2026 16:38:44 -0300 Subject: [PATCH 08/47] New compile option LUA_COMPAT_LOOPVAR When on, this option makes for-loop control variables not read only. --- lparser.c | 13 +++++++++++-- luaconf.h | 7 +++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lparser.c b/lparser.c index b3855d4c..b27463af 100644 --- a/lparser.c +++ b/lparser.c @@ -1682,13 +1682,22 @@ static void forbody (LexState *ls, int base, int line, int nvars, int isgen) { } +/* +** Control whether for-loop control variables are read-only +*/ +#if defined(LUA_COMPAT_LOOPVAR) +#define LOOPVARKIND VDKREG +#else /* by default, these variables are read only */ +#define LOOPVARKIND RDKCONST +#endif + static void fornum (LexState *ls, TString *varname, int line) { /* fornum -> NAME = exp,exp[,exp] forbody */ FuncState *fs = ls->fs; int base = fs->freereg; new_localvarliteral(ls, "(for state)"); new_localvarliteral(ls, "(for state)"); - new_varkind(ls, varname, RDKCONST); /* control variable */ + new_varkind(ls, varname, LOOPVARKIND); /* control variable */ checknext(ls, '='); exp1(ls); /* initial value */ checknext(ls, ','); @@ -1715,7 +1724,7 @@ static void forlist (LexState *ls, TString *indexname) { new_localvarliteral(ls, "(for state)"); /* iterator function */ new_localvarliteral(ls, "(for state)"); /* state */ new_localvarliteral(ls, "(for state)"); /* closing var. (after swap) */ - new_varkind(ls, indexname, RDKCONST); /* control variable */ + new_varkind(ls, indexname, LOOPVARKIND); /* control variable */ /* other declared variables */ while (testnext(ls, ',')) { new_localvar(ls, str_checkname(ls)); diff --git a/luaconf.h b/luaconf.h index 96a77802..f076c984 100644 --- a/luaconf.h +++ b/luaconf.h @@ -342,6 +342,13 @@ #define LUA_COMPAT_GLOBAL +/* +@@ LUA_COMPAT_LOOPVAR makes for-loop control variables not read-only, +** as they were in previous versions. +*/ +/* #define LUA_COMPAT_LOOPVAR */ + + /* @@ LUA_COMPAT_MATHLIB controls the presence of several deprecated ** functions in the mathematical library. From e992c6a95939c8e1fe357bfce481e0d0c762c3c6 Mon Sep 17 00:00:00 2001 From: Roberto Ierusalimschy Date: Tue, 20 Jan 2026 13:06:16 -0300 Subject: [PATCH 09/47] Some compilation options configurable from makefile Compilation options LUA_COMPAT_GLOBAL, LUA_COMPAT_LOOPVAR, and LUA_READLINELIB do not affect the API, so they can be changed through the make file. --- llex.c | 2 +- lparser.c | 4 ++-- ltests.h | 1 + luaconf.h | 12 ++++++++++-- manual/manual.of | 6 ++++++ 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/llex.c b/llex.c index f8bb3ea4..7cd9fcaf 100644 --- a/llex.c +++ b/llex.c @@ -188,7 +188,7 @@ void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source, so they cannot be collected */ ls->envn = luaS_newliteral(L, LUA_ENV); /* get env string */ ls->brkn = luaS_newliteral(L, "break"); /* get "break" string */ -#if defined(LUA_COMPAT_GLOBAL) +#if LUA_COMPAT_GLOBAL /* compatibility mode: "global" is not a reserved word */ ls->glbn = luaS_newliteral(L, "global"); /* get "global" string */ ls->glbn->extra = 0; /* mark it as not reserved */ diff --git a/lparser.c b/lparser.c index b27463af..6b87773e 100644 --- a/lparser.c +++ b/lparser.c @@ -1685,7 +1685,7 @@ static void forbody (LexState *ls, int base, int line, int nvars, int isgen) { /* ** Control whether for-loop control variables are read-only */ -#if defined(LUA_COMPAT_LOOPVAR) +#if LUA_COMPAT_LOOPVAR #define LOOPVARKIND VDKREG #else /* by default, these variables are read only */ #define LOOPVARKIND RDKCONST @@ -2120,7 +2120,7 @@ static void statement (LexState *ls) { gotostat(ls, line); break; } -#if defined(LUA_COMPAT_GLOBAL) +#if LUA_COMPAT_GLOBAL case TK_NAME: { /* compatibility code to parse global keyword when "global" is not reserved */ diff --git a/ltests.h b/ltests.h index 93096da8..f5f14cd6 100644 --- a/ltests.h +++ b/ltests.h @@ -14,6 +14,7 @@ /* test Lua with compatibility code */ #define LUA_COMPAT_MATHLIB #undef LUA_COMPAT_GLOBAL +#define LUA_COMPAT_GLOBAL 0 #define LUA_DEBUG diff --git a/luaconf.h b/luaconf.h index f076c984..1b72e338 100644 --- a/luaconf.h +++ b/luaconf.h @@ -70,15 +70,19 @@ #if defined(LUA_USE_LINUX) #define LUA_USE_POSIX #define LUA_USE_DLOPEN /* needs an extra library: -ldl */ +#if !defined(LUA_READLINELIB) #define LUA_READLINELIB "libreadline.so" #endif +#endif #if defined(LUA_USE_MACOSX) #define LUA_USE_POSIX #define LUA_USE_DLOPEN /* macOS does not need -ldl */ +#if !defined(LUA_READLINELIB) #define LUA_READLINELIB "libedit.dylib" #endif +#endif #if defined(LUA_USE_IOS) @@ -339,14 +343,18 @@ /* @@ LUA_COMPAT_GLOBAL avoids 'global' being a reserved word */ -#define LUA_COMPAT_GLOBAL +#if !defined(LUA_COMPAT_GLOBAL) +#define LUA_COMPAT_GLOBAL 1 +#endif /* @@ LUA_COMPAT_LOOPVAR makes for-loop control variables not read-only, ** as they were in previous versions. */ -/* #define LUA_COMPAT_LOOPVAR */ +#if !defined(LUA_COMPAT_LOOPVAR) +#define LUA_COMPAT_LOOPVAR 0 +#endif /* diff --git a/manual/manual.of b/manual/manual.of index 5fa4e097..09075346 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -9594,12 +9594,18 @@ change between versions. @item{ The word @Rw{global} is a reserved word. Do not use it as a regular name. + +The compilation option @id{LUA_COMPAT_GLOBAL} (see @id{luaconf.h}) +makes @id{global} a regular word. } @item{ The control variable in @Rw{for} loops is read only. If you need to change it, declare a local variable with the same name in the loop body. + +The compilation option @id{LUA_COMPAT_LOOPVAR} (see @id{luaconf.h}) +makes these variables regular (writable). } @item{ From 3360710bd3ea8da06fa5062f9d10c2719083097c Mon Sep 17 00:00:00 2001 From: Roberto I Date: Thu, 22 Jan 2026 13:47:10 -0300 Subject: [PATCH 10/47] Another way to handle option -E A pointer to function 'l_getenv' can point to the regular 'getenv' or to a dumb function (that always returns NULL) to ignore environment variables. --- lua.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lua.c b/lua.c index 5054583d..37fd1cb8 100644 --- a/lua.c +++ b/lua.c @@ -374,12 +374,21 @@ static int runargs (lua_State *L, char **argv, int n) { } +static char *(*l_getenv)(const char *name); + +/* Function to ignore environment variables, used by option -E */ +static char *no_getenv (const char *name) { + UNUSED(name); + return NULL; +} + + static int handle_luainit (lua_State *L) { const char *name = "=" LUA_INITVARVERSION; - const char *init = getenv(name + 1); + const char *init = l_getenv(name + 1); if (init == NULL) { name = "=" LUA_INIT_VAR; - init = getenv(name + 1); /* try alternative name */ + init = l_getenv(name + 1); /* try alternative name */ } if (init == NULL) return LUA_OK; else if (init[0] == '@') @@ -715,17 +724,18 @@ static int pmain (lua_State *L) { if (args & has_v) /* option '-v'? */ print_version(); if (args & has_E) { /* option '-E'? */ + l_getenv = &no_getenv; /* program will ignore environment variables */ lua_pushboolean(L, 1); /* signal for libraries to ignore env. vars. */ lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV"); } + else + l_getenv = &getenv; luai_openlibs(L); /* open standard libraries */ createargtable(L, argv, argc, script); /* create table 'arg' */ lua_gc(L, LUA_GCRESTART); /* start GC... */ lua_gc(L, LUA_GCGEN); /* ...in generational mode */ - if (!(args & has_E)) { /* no option '-E'? */ - if (handle_luainit(L) != LUA_OK) /* run LUA_INIT */ - return 0; /* error running LUA_INIT */ - } + if (handle_luainit(L) != LUA_OK) /* run LUA_INIT */ + return 0; /* error running LUA_INIT */ if (!runargs(L, argv, optlim)) /* execute arguments -e, -l, and -W */ return 0; /* something failed */ if (script > 0) { /* execute main script (if there is one) */ From cfcaa9493b783527e5b5dfb71afb51602b3bccac Mon Sep 17 00:00:00 2001 From: Roberto I Date: Fri, 23 Jan 2026 16:25:18 -0300 Subject: [PATCH 11/47] Explanation about char* parameters in the C API --- manual/manual.of | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/manual/manual.of b/manual/manual.of index 09075346..893592da 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -2692,7 +2692,19 @@ which behaves like a nil value. @sect3{constchar|@title{Pointers to Strings} -Several functions in the API return pointers (@T{const char*}) +Several functions in the API accept pointers (@T{const char*}) +to C strings. +Some of there parameters have an associated length (@T{size_t}). +Unless stated otherwise, +when there is an associated length, +the string can contain embedded zeros; +moreover, the pointer can be @id{NULL} if the length is zero. +When there is no associated length, +the pointer must point to a zero-terminated string. +In any case, the string contents should remain unchanged +until the function returns. + +Several functions in the API also return pointers (@T{const char*}) to Lua strings in the stack. (See @Lid{lua_pushfstring}, @Lid{lua_pushlstring}, @Lid{lua_pushstring}, and @Lid{lua_tolstring}. From efbc29754544dd820bfdc81edf17d7dcfad31d05 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Thu, 29 Jan 2026 14:24:25 -0300 Subject: [PATCH 12/47] New year and (eventual) new release --- lua.h | 6 +++--- manual/2html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lua.h b/lua.h index ab473dc3..6deaed49 100644 --- a/lua.h +++ b/lua.h @@ -13,13 +13,13 @@ #include -#define LUA_COPYRIGHT LUA_RELEASE " Copyright (C) 1994-2025 Lua.org, PUC-Rio" +#define LUA_COPYRIGHT LUA_RELEASE " Copyright (C) 1994-2026 Lua.org, PUC-Rio" #define LUA_AUTHORS "R. Ierusalimschy, L. H. de Figueiredo, W. Celes" #define LUA_VERSION_MAJOR_N 5 #define LUA_VERSION_MINOR_N 5 -#define LUA_VERSION_RELEASE_N 0 +#define LUA_VERSION_RELEASE_N 1 #define LUA_VERSION_NUM (LUA_VERSION_MAJOR_N * 100 + LUA_VERSION_MINOR_N) #define LUA_VERSION_RELEASE_NUM (LUA_VERSION_NUM * 100 + LUA_VERSION_RELEASE_N) @@ -521,7 +521,7 @@ struct lua_Debug { /****************************************************************************** -* Copyright (C) 1994-2025 Lua.org, PUC-Rio. +* Copyright (C) 1994-2026 Lua.org, PUC-Rio. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the diff --git a/manual/2html b/manual/2html index b7afd2a6..d3b88b34 100755 --- a/manual/2html +++ b/manual/2html @@ -30,7 +30,7 @@ by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, Waldemar Celes

Copyright -© 2025 Lua.org, PUC-Rio. All rights reserved. +© 2026 Lua.org, PUC-Rio. All rights reserved.


From c6b484823806e08e1756b1a6066a3ace6f080fae Mon Sep 17 00:00:00 2001 From: Roberto I Date: Fri, 30 Jan 2026 16:47:33 -0300 Subject: [PATCH 13/47] Environment variable for readline library name The name of the readline library can be changed from its default value through environment variable LUA_READLINELIB. --- lua.c | 24 ++++++++++++++++++------ testes/main.lua | 23 ++++++++++++++++++++++- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/lua.c b/lua.c index 37fd1cb8..3f4fc9f7 100644 --- a/lua.c +++ b/lua.c @@ -30,6 +30,12 @@ #define LUA_INIT_VAR "LUA_INIT" #endif +/* Name of the environment variable with the name of the readline library */ +#if !defined(LUA_RLLIB_VAR) +#define LUA_RLLIB_VAR "LUA_READLINELIB" +#endif + + #define LUA_INITVARVERSION LUA_INIT_VAR LUA_VERSUFFIX @@ -507,18 +513,24 @@ static void lua_freeline (char *line) { #include static void lua_initreadline (lua_State *L) { - void *lib = dlopen(LUA_READLINELIB, RTLD_NOW | RTLD_LOCAL); - if (lib == NULL) - lua_warning(L, "library '" LUA_READLINELIB "' not found", 0); - else { + const char *rllib = l_getenv(LUA_RLLIB_VAR); /* name of readline library */ + void *lib; /* library handle */ + if (rllib == NULL) /* no environment variable? */ + rllib = LUA_READLINELIB; /* use default name */ + lib = dlopen(rllib, RTLD_NOW | RTLD_LOCAL); + if (lib != NULL) { const char **name = cast(const char**, dlsym(lib, "rl_readline_name")); if (name != NULL) *name = "lua"; l_readline = cast(l_readlineT, cast_func(dlsym(lib, "readline"))); l_addhist = cast(l_addhistT, cast_func(dlsym(lib, "add_history"))); - if (l_readline == NULL) - lua_warning(L, "unable to load 'readline'", 0); + if (l_readline != NULL) /* could load readline function? */ + return; /* everything ok */ + /* else emit a warning */ } + lua_warning(L, "unable to load readline library '", 1); + lua_warning(L, rllib, 1); + lua_warning(L, "'", 0); } #else /* }{ */ diff --git a/testes/main.lua b/testes/main.lua index dc48dc48..98d36951 100644 --- a/testes/main.lua +++ b/testes/main.lua @@ -78,6 +78,9 @@ end RUN('lua -v') +RUN('lua -v > %s', out) +local release = string.match(getoutput(), "Lua (%d+%.%d+%.%d+)") + print(string.format("(temporary program file used in these tests: %s)", prog)) -- running stdin as a file @@ -167,7 +170,9 @@ checkout("10\n11\n") -- test errors in LUA_INIT NoRun('LUA_INIT:1: msg', 'env LUA_INIT="error(\'msg\')" lua') --- test option '-E' + +print("testing option '-E'") + local defaultpath, defaultCpath do @@ -192,6 +197,22 @@ assert(not string.find(defaultpath, "xxx") and string.find(defaultCpath, "lua")) +-- (LUA_READLINELIB was introduced in 5.5.1) +if release >= "5.5.1" then + print"testing readline library name" + -- should generate a warning when trying to load inexistent library "xuxu" + local env = [[LUA_READLINELIB=xuxu LUA_INIT="warn('@allow')"]] + local code = 'echo " " | env %s lua %s -W -i >%s 2>&1' + RUN(code, env, "", out) -- run code with no extra options + assert(string.find(getoutput(), + "warning: unable to load readline library 'xuxu'")) + + RUN(code, env, "-E", out) -- run again with option -E + -- no warning when LUA_READLINELIB is to be ignored + assert(not string.find(getoutput(), "warning")) +end + + -- test replacement of ';;' to default path local function convert (p) prepfile("print(package.path)") From b60e2bcd7ca4c349bd6ee7a8e929f55e04f7ca87 Mon Sep 17 00:00:00 2001 From: Roberto Ierusalimschy Date: Mon, 9 Feb 2026 13:44:27 -0300 Subject: [PATCH 14/47] Avoid an assignment of values that overlap The original code was like this, where t->u.ind.t and t->u.info overlap: t->u.ind.t = cast_byte((t->k == VLOCAL) ? t->u.var.ridx: t->u.info); --- lcode.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lcode.c b/lcode.c index 4caa8046..33cbd687 100644 --- a/lcode.c +++ b/lcode.c @@ -827,7 +827,7 @@ void luaK_dischargevars (FuncState *fs, expdesc *e) { } /* FALLTHROUGH */ case VLOCAL: { /* already in a register */ int temp = e->u.var.ridx; - e->u.info = temp; /* (can't do a direct assignment; values overlap) */ + e->u.info = temp; /* (avoid a direct assignment; values overlap) */ e->k = VNONRELOC; /* becomes a non-relocatable value */ break; } @@ -1365,7 +1365,7 @@ void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k) { luaK_exp2anyreg(fs, t); /* put it in a register */ if (t->k == VUPVAL) { lu_byte temp = cast_byte(t->u.info); /* upvalue index */ - t->u.ind.t = temp; /* (can't do a direct assignment; values overlap) */ + t->u.ind.t = temp; /* (avoid a direct assignment; values overlap) */ lua_assert(isKstr(fs, k)); fillidxk(t, k->u.info, VINDEXUP); /* literal short string */ } @@ -1373,12 +1373,13 @@ void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k) { int kreg = luaK_exp2anyreg(fs, k); /* put key in some register */ lu_byte vreg = cast_byte(t->u.var.ridx); /* register with vararg param. */ lua_assert(vreg == fs->f->numparams); - t->u.ind.t = vreg; /* (avoid a direct assignment; values may overlap) */ + t->u.ind.t = vreg; /* (avoid a direct assignment; values may overlap?) */ fillidxk(t, kreg, VVARGIND); /* 't' represents 'vararg[k]' */ } else { /* register index of the table */ - t->u.ind.t = cast_byte((t->k == VLOCAL) ? t->u.var.ridx: t->u.info); + lu_byte temp = cast_byte((t->k == VLOCAL) ? t->u.var.ridx: t->u.info); + t->u.ind.t = temp; /* (avoid a direct assignment; values may overlap?) */ if (isKstr(fs, k)) fillidxk(t, k->u.info, VINDEXSTR); /* literal short string */ else if (isCint(k)) /* int. constant in proper range? */ From 7c40c5edb2364745bf0add6feb02c7c90dfbae3e Mon Sep 17 00:00:00 2001 From: Roberto Ierusalimschy Date: Tue, 10 Feb 2026 16:41:02 -0300 Subject: [PATCH 15/47] Details Spaces + added initialization to the documentation of global declarations. --- luaconf.h | 18 +++++++++--------- manual/manual.of | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/luaconf.h b/luaconf.h index 1b72e338..7f2206d0 100644 --- a/luaconf.h +++ b/luaconf.h @@ -228,17 +228,17 @@ #if !defined(LUA_PATH_DEFAULT) #define LUA_PATH_DEFAULT \ - LUA_LDIR"?.lua;" LUA_LDIR"?\\init.lua;" \ - LUA_CDIR"?.lua;" LUA_CDIR"?\\init.lua;" \ - LUA_SHRDIR"?.lua;" LUA_SHRDIR"?\\init.lua;" \ + LUA_LDIR "?.lua;" LUA_LDIR "?\\init.lua;" \ + LUA_CDIR "?.lua;" LUA_CDIR "?\\init.lua;" \ + LUA_SHRDIR "?.lua;" LUA_SHRDIR "?\\init.lua;" \ ".\\?.lua;" ".\\?\\init.lua" #endif #if !defined(LUA_CPATH_DEFAULT) #define LUA_CPATH_DEFAULT \ - LUA_CDIR"?.dll;" \ - LUA_CDIR"..\\lib\\lua\\" LUA_VDIR "\\?.dll;" \ - LUA_CDIR"loadall.dll;" ".\\?.dll" + LUA_CDIR "?.dll;" \ + LUA_CDIR "..\\lib\\lua\\" LUA_VDIR "\\?.dll;" \ + LUA_CDIR "loadall.dll;" ".\\?.dll" #endif #else /* }{ */ @@ -249,14 +249,14 @@ #if !defined(LUA_PATH_DEFAULT) #define LUA_PATH_DEFAULT \ - LUA_LDIR"?.lua;" LUA_LDIR"?/init.lua;" \ - LUA_CDIR"?.lua;" LUA_CDIR"?/init.lua;" \ + LUA_LDIR "?.lua;" LUA_LDIR "?/init.lua;" \ + LUA_CDIR "?.lua;" LUA_CDIR "?/init.lua;" \ "./?.lua;" "./?/init.lua" #endif #if !defined(LUA_CPATH_DEFAULT) #define LUA_CPATH_DEFAULT \ - LUA_CDIR"?.so;" LUA_CDIR"loadall.so;" "./?.so" + LUA_CDIR "?.so;" LUA_CDIR "loadall.so;" "./?.so" #endif #endif /* } */ diff --git a/manual/manual.of b/manual/manual.of index 893592da..18c187f4 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -9743,7 +9743,7 @@ and @bnfNter{LiteralString}, see @See{lexical}.) @OrNL @Rw{local} @Rw{function} @bnfNter{Name} funcbody @OrNL @Rw{global} @Rw{function} @bnfNter{Name} funcbody @OrNL @Rw{local} attnamelist @bnfopt{@bnfter{=} explist} -@OrNL @Rw{global} attnamelist +@OrNL @Rw{global} attnamelist @bnfopt{@bnfter{=} explist} @OrNL @Rw{global} @bnfopt{attrib} @bnfter{*} } From 10eb89d1141dc528806b32401e408e36fb2f3bf5 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 18 Feb 2026 13:24:04 -0300 Subject: [PATCH 16/47] BUG: shift overflow in utf-8 decode An initial byte \xFF will ask for 7 continuation bytes, and then the shift by (count * 5) will try to shift 35 bits. --- lutf8lib.c | 5 ++++- makefile | 2 +- testes/utf8.lua | 14 +++++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lutf8lib.c b/lutf8lib.c index b7f3fe1e..73f0e49b 100644 --- a/lutf8lib.c +++ b/lutf8lib.c @@ -56,6 +56,8 @@ static const char *utf8_decode (const char *s, l_uint32 *val, int strict) { l_uint32 res = 0; /* final result */ if (c < 0x80) /* ASCII? */ res = c; + else if (c >= 0xfe) /* c >= 1111 1110b ? */ + return NULL; /* would need six or more continuation bytes */ else { int count = 0; /* to count number of continuation bytes */ for (; c & 0x40; c <<= 1) { /* while it needs continuation bytes... */ @@ -64,8 +66,9 @@ static const char *utf8_decode (const char *s, l_uint32 *val, int strict) { return NULL; /* invalid byte sequence */ res = (res << 6) | (cc & 0x3F); /* add lower 6 bits from cont. byte */ } + lua_assert(count <= 5); res |= ((l_uint32)(c & 0x7F) << (count * 5)); /* add first byte */ - if (count > 5 || res > MAXUTF || res < limits[count]) + if (res > MAXUTF || res < limits[count]) return NULL; /* invalid byte sequence */ s += count; /* skip continuation bytes read */ } diff --git a/makefile b/makefile index 8674519f..fa165bca 100644 --- a/makefile +++ b/makefile @@ -60,7 +60,7 @@ CWARNS= $(CWARNSCPP) $(CWARNSC) $(CWARNGCC) # create problems; some are only available in newer gcc versions. To # use some of them, we also have to define an environment variable # ASAN_OPTIONS="detect_invalid_pointer_pairs=2". -# -fsanitize=undefined +# -fsanitize=undefined (you may need to add "-lubsan" to libs) # -fsanitize=pointer-subtract -fsanitize=address -fsanitize=pointer-compare # TESTS= -DLUA_USER_H='"ltests.h"' -Og -g diff --git a/testes/utf8.lua b/testes/utf8.lua index 028995a4..8a0213d6 100644 --- a/testes/utf8.lua +++ b/testes/utf8.lua @@ -238,10 +238,18 @@ s = "\0 \x7F\z s = string.gsub(s, " ", "") check(s, {0,0x7F, 0x80,0x7FF, 0x800,0xFFFF, 0x10000,0x10FFFF}) + +-- again, without strictness +s = "\xF0\x90\x80\x80 \xF7\xBF\xBF\xBF\z + \xF8\x88\x80\x80\x80 \xFB\xBF\xBF\xBF\xBF\z + \xFC\x84\x80\x80\x80\x80 \xFD\xBF\xBF\xBF\xBF\xBF" +s = string.gsub(s, " ", "") +check(s, {0x10000,0x1FFFFF, 0x200000,0x3FFFFFF, 0x4000000,0x7FFFFFFF}, true) + do -- original UTF-8 values local s = "\u{4000000}\u{7FFFFFFF}" - assert(#s == 12) + assert(s == "\xFC\x84\x80\x80\x80\x80\xFD\xBF\xBF\xBF\xBF\xBF") check(s, {0x4000000, 0x7FFFFFFF}, true) s = "\u{200000}\u{3FFFFFF}" @@ -257,6 +265,10 @@ local x = "日本語a-4\0éó" check(x, {26085, 26412, 35486, 97, 45, 52, 0, 233, 243}) +-- more than 5 continuation bytes +assert(not utf8.len("\xff\x8f\x8f\x8f\x8f\x8f\x8f\x8f")) + + -- Supplementary Characters check("𣲷𠜎𠱓𡁻𠵼ab𠺢", {0x23CB7, 0x2070E, 0x20C53, 0x2107B, 0x20D7C, 0x61, 0x62, 0x20EA2,}) From 9e501d9855e560b08c50fb0cf6e147af93bb497e Mon Sep 17 00:00:00 2001 From: Roberto I Date: Mon, 9 Mar 2026 16:23:03 -0300 Subject: [PATCH 17/47] Slightly better documentation for LUAI_MAXALIGN --- luaconf.h | 11 +++++++++-- manual/manual.of | 8 ++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/luaconf.h b/luaconf.h index 7f2206d0..5ac9d988 100644 --- a/luaconf.h +++ b/luaconf.h @@ -736,10 +736,17 @@ /* -@@ LUAI_MAXALIGN defines fields that, when used in a union, ensure -** maximum alignment for the other items in that union. +@@ LUAI_MAXALIGN defines fields that ensure proper alignment for +** memory areas offered by Lua (e.g., userdata memory). +** Add fields to it if you need alignment for non-ISO objects. */ +#if defined(LLONG_MAX) +/* use ISO C99 stuff */ +#define LUAI_MAXALIGN long double u; void *s; long long l +#else +/* use only C89 stuff */ #define LUAI_MAXALIGN lua_Number n; double u; void *s; lua_Integer i; long l +#endif /* }================================================================== */ diff --git a/manual/manual.of b/manual/manual.of index 18c187f4..5eb2fb1f 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -3915,8 +3915,12 @@ like any Lua object. This function creates and pushes on the stack a new full userdata, with @id{nuvalue} associated Lua values, called @id{user values}, plus an associated block of raw memory with @id{size} bytes. -(The user values can be set and read with the functions -@Lid{lua_setiuservalue} and @Lid{lua_getiuservalue}.) + +The user values can be set and read with the functions +@Lid{lua_setiuservalue} and @Lid{lua_getiuservalue}. +The block of memory is suitably aligned for any @N{ISO C} object. +(See macro @id{LUAI_MAXALIGN} in file @id{luaconf.h} for other +alignment requirements.) The function returns the address of the block of memory. Lua ensures that this address is valid as long as From 36d5d2b2847906aa3b66e020d5d894a14ba2bf90 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Mon, 9 Mar 2026 16:24:06 -0300 Subject: [PATCH 18/47] Details --- lopcodes.c | 2 +- lutf8lib.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lopcodes.c b/lopcodes.c index 7e182315..c4828bfc 100644 --- a/lopcodes.c +++ b/lopcodes.c @@ -104,7 +104,7 @@ LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = { ,opmode(0, 1, 0, 0, 1, iABC) /* OP_VARARG */ ,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETVARG */ ,opmode(0, 0, 0, 0, 0, iABx) /* OP_ERRNNIL */ - ,opmode(0, 0, 1, 0, 1, iABC) /* OP_VARARGPREP */ + ,opmode(0, 0, 1, 0, 0, iABC) /* OP_VARARGPREP */ ,opmode(0, 0, 0, 0, 0, iAx) /* OP_EXTRAARG */ }; diff --git a/lutf8lib.c b/lutf8lib.c index 73f0e49b..0cd7f9c3 100644 --- a/lutf8lib.c +++ b/lutf8lib.c @@ -149,7 +149,7 @@ static int codepoint (lua_State *L) { static void pushutfchar (lua_State *L, int arg) { lua_Unsigned code = (lua_Unsigned)luaL_checkinteger(L, arg); luaL_argcheck(L, code <= MAXUTF, arg, "value out of range"); - lua_pushfstring(L, "%U", (long)code); + lua_pushfstring(L, "%U", cast(unsigned long, code)); } From 377cbea61b2688b21c7d243fc0f42498851df794 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Mon, 9 Mar 2026 16:24:49 -0300 Subject: [PATCH 19/47] 'table.tunpack' using 'aux_getn' like the others 'table.tunpack' was not checking its first argument, which could result in error messages generated inside the API, without location information. --- ltablib.c | 11 +++++++---- testes/sort.lua | 13 +++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ltablib.c b/ltablib.c index 46ecb5e0..15c3c09f 100644 --- a/ltablib.c +++ b/ltablib.c @@ -42,15 +42,17 @@ static int checkfield (lua_State *L, const char *key, int n) { /* ** Check that 'arg' either is a table or can behave like one (that is, -** has a metatable with the required metamethods) +** has a metatable with the required metamethods). */ static void checktab (lua_State *L, int arg, int what) { - if (lua_type(L, arg) != LUA_TTABLE) { /* is it not a table? */ + int tp = lua_type(L, arg); + if (tp != LUA_TTABLE) { /* is it not a table? */ int n = 1; /* number of elements to pop */ if (lua_getmetatable(L, arg) && /* must have metatable */ (!(what & TAB_R) || checkfield(L, "__index", ++n)) && (!(what & TAB_W) || checkfield(L, "__newindex", ++n)) && - (!(what & TAB_L) || checkfield(L, "__len", ++n))) { + (!(what & TAB_L) || /* strings don't need '__len' to have a length */ + tp == LUA_TSTRING || checkfield(L, "__len", ++n))) { lua_pop(L, n); /* pop metatable and tested metamethods */ } else @@ -204,8 +206,9 @@ static int tpack (lua_State *L) { static int tunpack (lua_State *L) { lua_Unsigned n; + lua_Integer len = aux_getn(L, 1, TAB_R); lua_Integer i = luaL_optinteger(L, 2, 1); - lua_Integer e = luaL_opt(L, luaL_checkinteger, 3, luaL_len(L, 1)); + lua_Integer e = luaL_opt(L, luaL_checkinteger, 3, len); if (i > e) return 0; /* empty range */ n = l_castS2U(e) - l_castS2U(i); /* number of elements minus 1 */ if (l_unlikely(n >= (unsigned int)INT_MAX || diff --git a/testes/sort.lua b/testes/sort.lua index b0127660..92aaca3c 100644 --- a/testes/sort.lua +++ b/testes/sort.lua @@ -72,6 +72,19 @@ assert(a==1 and x==nil) a,x = unpack({1,2}, 1, 1) assert(a==1 and x==nil) + +do -- unpack with non-tables + local debug = require"debug" + local oldmt = debug.getmetatable(0) + local str = "hello" + debug.setmetatable(0, + { __len = function () return #str end, + __index = function (_, i) return string.sub(str, i, i) end}) + assert(table.concat({table.unpack(0)}) == str) + debug.setmetatable(0, oldmt) -- restore original metatable for numbers +end + + do local maxi = (1 << 31) - 1 -- maximum value for an int (usually) local mini = -(1 << 31) -- minimum value for an int (usually) From 51269bd783c9371252947b26cc865239dbb0153d Mon Sep 17 00:00:00 2001 From: Roberto I Date: Sun, 15 Mar 2026 15:14:14 -0300 Subject: [PATCH 20/47] Adjustment in useless parameter L in macros luai_num* --- llimits.h | 12 ++++++------ lvm.c | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/llimits.h b/llimits.h index fc5cb276..3f037255 100644 --- a/llimits.h +++ b/llimits.h @@ -234,12 +234,12 @@ typedef unsigned long l_uint32; /* floor division (defined as 'floor(a/b)') */ #if !defined(luai_numidiv) -#define luai_numidiv(L,a,b) ((void)L, l_floor(luai_numdiv(L,a,b))) +#define luai_numidiv(L,a,b) l_floor(luai_numdiv(L,a,b)) #endif /* float division */ #if !defined(luai_numdiv) -#define luai_numdiv(L,a,b) ((a)/(b)) +#define luai_numdiv(L,a,b) ((void)L, (a)/(b)) #endif /* @@ -267,10 +267,10 @@ typedef unsigned long l_uint32; /* the others are quite standard operations */ #if !defined(luai_numadd) -#define luai_numadd(L,a,b) ((a)+(b)) -#define luai_numsub(L,a,b) ((a)-(b)) -#define luai_nummul(L,a,b) ((a)*(b)) -#define luai_numunm(L,a) (-(a)) +#define luai_numadd(L,a,b) ((void)L, (a)+(b)) +#define luai_numsub(L,a,b) ((void)L, (a)-(b)) +#define luai_nummul(L,a,b) ((void)L, (a)*(b)) +#define luai_numunm(L,a) ((void)L, -(a)) #define luai_numeq(a,b) ((a)==(b)) #define luai_numlt(a,b) ((a)<(b)) #define luai_numle(a,b) ((a)<=(b)) diff --git a/lvm.c b/lvm.c index c70e2b8a..96ae1639 100644 --- a/lvm.c +++ b/lvm.c @@ -268,9 +268,9 @@ static int forprep (lua_State *L, StkId ra) { /* ** Execute a step of a float numerical for loop, returning ** true iff the loop must continue. (The integer case is -** written online with opcode OP_FORLOOP, for performance.) +** written inline with opcode OP_FORLOOP, for performance.) */ -static int floatforloop (StkId ra) { +static int floatforloop (lua_State *L, StkId ra) { lua_Number step = fltvalue(s2v(ra + 1)); lua_Number limit = fltvalue(s2v(ra)); lua_Number idx = fltvalue(s2v(ra + 2)); /* control variable */ @@ -1841,7 +1841,7 @@ void luaV_execute (lua_State *L, CallInfo *ci) { pc -= GETARG_Bx(i); /* jump back */ } } - else if (floatforloop(ra)) /* float loop */ + else if (floatforloop(L, ra)) /* float loop */ pc -= GETARG_Bx(i); /* jump back */ updatetrap(ci); /* allows a signal to break the loop */ vmbreak; From f1bb2773bba8b16f0f01c00e59a7be541ef88cb7 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 1 Apr 2026 14:59:41 -0300 Subject: [PATCH 21/47] Bug: Loading a binary chunk does not run the GC --- lapi.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lapi.c b/lapi.c index 9b6ca1ec..53eb1719 100644 --- a/lapi.c +++ b/lapi.c @@ -1122,6 +1122,7 @@ LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data, ZIO z; TStatus status; lua_lock(L); + luaC_checkGC(L); if (!chunkname) chunkname = "?"; luaZ_init(L, &z, reader, data); status = luaD_protectedparser(L, &z, chunkname, mode); From efddc2309c5ff8a1842bea8a9c0d7d4a5d6e1e60 Mon Sep 17 00:00:00 2001 From: Roberto Ierusalimschy Date: Wed, 1 Apr 2026 15:01:58 -0300 Subject: [PATCH 22/47] Bug: wrong initialization in result from 'gmatch' Function returned by 'string.gmatch' can be left in an inconsistent state after an error. --- lstrlib.c | 10 ++++++++-- testes/pm.lua | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lstrlib.c b/lstrlib.c index 874cec80..dd3c0fd0 100644 --- a/lstrlib.c +++ b/lstrlib.c @@ -757,19 +757,25 @@ static int nospecials (const char *p, size_t l) { } +/* +** Prepare state for matches. These fields are not affected by each match. +*/ static void prepstate (MatchState *ms, lua_State *L, const char *s, size_t ls, const char *p, size_t lp) { ms->L = L; - ms->matchdepth = MAXCCALLS; ms->src_init = s; ms->src_end = s + ls; ms->p_end = p + lp; } +/* +** (Re)prepare state for a match, setting fields that change during +** each match. +*/ static void reprepstate (MatchState *ms) { + ms->matchdepth = MAXCCALLS; ms->level = 0; - lua_assert(ms->matchdepth == MAXCCALLS); } diff --git a/testes/pm.lua b/testes/pm.lua index 720d2a35..feab33db 100644 --- a/testes/pm.lua +++ b/testes/pm.lua @@ -347,6 +347,16 @@ do -- init parameter in gmatch end +do -- bug since 5.3 + local N = 20000 + local iter = string.gmatch(string.rep("a", N), string.rep("a?", N)) + pcall(iter) -- error for pattern too complex + -- calling function again found recursion count ('matchdepth') equal + -- to -1, so it did not detect next C-stack overflow + pcall(iter) +end + + -- tests for `%f' (`frontiers') assert(string.gsub("aaa aa a aaa a", "%f[%w]a", "x") == "xaa xa x xaa x") From c037162a1a657088d722f550e287015525bb2259 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 1 Apr 2026 15:09:07 -0300 Subject: [PATCH 23/47] Stricter test for use of '__builtin_expect' GCC introduced this macro in version 3. --- luaconf.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaconf.h b/luaconf.h index 5ac9d988..0a71370f 100644 --- a/luaconf.h +++ b/luaconf.h @@ -664,7 +664,7 @@ */ #if !defined(luai_likely) -#if defined(__GNUC__) && !defined(LUA_NOBUILTIN) +#if !defined(LUA_NOBUILTIN) && defined(__GNUC__) && (__GNUC__ >= 3) #define luai_likely(x) (__builtin_expect(((x) != 0), 1)) #define luai_unlikely(x) (__builtin_expect(((x) != 0), 0)) #else From 29cf284089d543408d726440a3f1acaecdf73636 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Tue, 7 Apr 2026 13:42:34 -0300 Subject: [PATCH 24/47] Avoid macros luaL_loadbuffer and luaL_loadfile Use luaL_loadbufferx and luaL_loadfilex instead, being explicit about whether to accept binary chunks. --- lauxlib.c | 2 +- lbaselib.c | 8 +++++--- ldblib.c | 2 +- loadlib.c | 2 +- ltests.c | 6 +++--- lua.c | 12 ++++++------ 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lauxlib.c b/lauxlib.c index 7cf90cb7..af44418a 100644 --- a/lauxlib.c +++ b/lauxlib.c @@ -874,7 +874,7 @@ LUALIB_API int luaL_loadbufferx (lua_State *L, const char *buff, size_t size, LUALIB_API int luaL_loadstring (lua_State *L, const char *s) { - return luaL_loadbuffer(L, s, strlen(s), s); + return luaL_loadbufferx(L, s, strlen(s), s, "t"); } /* }====================================================== */ diff --git a/lbaselib.c b/lbaselib.c index 891bb90f..47f7bdec 100644 --- a/lbaselib.c +++ b/lbaselib.c @@ -340,9 +340,11 @@ static int load_aux (lua_State *L, int status, int envidx) { static const char *getMode (lua_State *L, int idx) { - const char *mode = luaL_optstring(L, idx, "bt"); - if (strchr(mode, 'B') != NULL) /* Lua code cannot use fixed buffers */ + const char *mode = luaL_optstring(L, idx, NULL); + if (mode != NULL && strchr(mode, 'B') != NULL) { + /* Lua code cannot use fixed buffers */ luaL_argerror(L, idx, "invalid mode"); + } return mode; } @@ -425,7 +427,7 @@ static int dofilecont (lua_State *L, int d1, lua_KContext d2) { static int luaB_dofile (lua_State *L) { const char *fname = luaL_optstring(L, 1, NULL); lua_settop(L, 1); - if (l_unlikely(luaL_loadfile(L, fname) != LUA_OK)) + if (l_unlikely(luaL_loadfilex(L, fname, "bt") != LUA_OK)) return lua_error(L); lua_callk(L, 0, LUA_MULTRET, 0, dofilecont); return dofilecont(L, 0, 0); diff --git a/ldblib.c b/ldblib.c index c7b74812..051327cc 100644 --- a/ldblib.c +++ b/ldblib.c @@ -427,7 +427,7 @@ static int db_debug (lua_State *L) { if (fgets(buffer, sizeof(buffer), stdin) == NULL || strcmp(buffer, "cont\n") == 0) return 0; - if (luaL_loadbuffer(L, buffer, strlen(buffer), "=(debug command)") || + if (luaL_loadbufferx(L, buffer, strlen(buffer), "=(debug command)", "t") || lua_pcall(L, 0, 0, 0)) lua_writestringerror("%s\n", luaL_tolstring(L, -1, NULL)); lua_settop(L, 0); /* remove eventual returns */ diff --git a/loadlib.c b/loadlib.c index 8d2e68e2..ef09c9a5 100644 --- a/loadlib.c +++ b/loadlib.c @@ -541,7 +541,7 @@ static int searcher_Lua (lua_State *L) { const char *name = luaL_checkstring(L, 1); filename = findfile(L, name, "path", LUA_LSUBSEP); if (filename == NULL) return 1; /* module not found in this path */ - return checkload(L, (luaL_loadfile(L, filename) == LUA_OK), filename); + return checkload(L, (luaL_loadfilex(L, filename, "bt") == LUA_OK), filename); } diff --git a/ltests.c b/ltests.c index ce2b20ca..6ae5f472 100644 --- a/ltests.c +++ b/ltests.c @@ -1302,7 +1302,7 @@ static int doonnewstack (lua_State *L) { lua_State *L1 = lua_newthread(L); size_t l; const char *s = luaL_checklstring(L, 1, &l); - int status = luaL_loadbuffer(L1, s, l, s); + int status = luaL_loadbufferx(L1, s, l, s, "t"); if (status == LUA_OK) status = lua_pcall(L1, 0, 0, 0); lua_pushinteger(L, status); @@ -1382,7 +1382,7 @@ static int doremote (lua_State *L) { const char *code = luaL_checklstring(L, 2, &lcode); int status; lua_settop(L1, 0); - status = luaL_loadbuffer(L1, code, lcode, code); + status = luaL_loadbufferx(L1, code, lcode, code, "t"); if (status == LUA_OK) status = lua_pcall(L1, 0, LUA_MULTRET, 0); if (status != LUA_OK) { @@ -1738,7 +1738,7 @@ static int runC (lua_State *L, lua_State *L1, const char *pc) { lua_pushinteger(L1, luaL_len(L1, getindex)); } else if EQ("loadfile") { - luaL_loadfile(L1, luaL_checkstring(L1, getnum)); + luaL_loadfilex(L1, luaL_checkstring(L1, getnum), "t"); } else if EQ("loadstring") { size_t slen; diff --git a/lua.c b/lua.c index 3f4fc9f7..3107a674 100644 --- a/lua.c +++ b/lua.c @@ -207,12 +207,12 @@ static int dochunk (lua_State *L, int status) { static int dofile (lua_State *L, const char *name) { - return dochunk(L, luaL_loadfile(L, name)); + return dochunk(L, luaL_loadfilex(L, name, "bt")); } static int dostring (lua_State *L, const char *s, const char *name) { - return dochunk(L, luaL_loadbuffer(L, s, strlen(s), name)); + return dochunk(L, luaL_loadbufferx(L, s, strlen(s), name, "t")); } @@ -266,7 +266,7 @@ static int handle_script (lua_State *L, char **argv) { const char *fname = argv[0]; if (strcmp(fname, "-") == 0 && strcmp(argv[-1], "--") != 0) fname = NULL; /* stdin */ - status = luaL_loadfile(L, fname); + status = luaL_loadfilex(L, fname, "bt"); if (status == LUA_OK) { int n = pushargs(L); /* push arguments to script */ status = docall(L, n, LUA_MULTRET); @@ -609,11 +609,11 @@ static int pushline (lua_State *L, int firstline) { static int addreturn (lua_State *L) { const char *line = lua_tostring(L, -1); /* original line */ const char *retline = lua_pushfstring(L, "return %s;", line); - int status = luaL_loadbuffer(L, retline, strlen(retline), "=stdin"); + int status = luaL_loadbufferx(L, retline, strlen(retline), "=stdin", "t"); if (status == LUA_OK) lua_remove(L, -2); /* remove modified line */ else - lua_pop(L, 2); /* pop result from 'luaL_loadbuffer' and modified line */ + lua_pop(L, 2); /* pop result from 'luaL_loadbufferx' and modified line */ return status; } @@ -640,7 +640,7 @@ static int multiline (lua_State *L) { const char *line = lua_tolstring(L, 1, &len); /* get first line */ checklocal(line); for (;;) { /* repeat until gets a complete statement */ - int status = luaL_loadbuffer(L, line, len, "=stdin"); /* try it */ + int status = luaL_loadbufferx(L, line, len, "=stdin", "t"); /* try it */ if (!incomplete(L, status) || !pushline(L, 0)) return status; /* should not or cannot try to add continuation line */ lua_remove(L, -2); /* remove error message (from incomplete line) */ From d0bd25d2e7fb393a6d0a73645a099f9c3b9cc0a8 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Mon, 13 Apr 2026 14:06:23 -0300 Subject: [PATCH 25/47] Better error messages for vararg-table fields --- ldebug.c | 2 +- testes/errors.lua | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ldebug.c b/ldebug.c index 8df5f5f2..2665ec7b 100644 --- a/ldebug.c +++ b/ldebug.c @@ -580,7 +580,7 @@ static const char *getobjname (const Proto *p, int lastpc, int reg, kname(p, k, name); return isEnv(p, lastpc, i, 1); } - case OP_GETTABLE: { + case OP_GETTABLE: case OP_GETVARG: { int k = GETARG_C(i); /* key index */ rname(p, lastpc, k, name); return isEnv(p, lastpc, i, 0); diff --git a/testes/errors.lua b/testes/errors.lua index c9d85099..c82d5b3b 100644 --- a/testes/errors.lua +++ b/testes/errors.lua @@ -159,6 +159,9 @@ assert(not string.find(doit"aaa={13}; local bbbb=1; aaa[bbbb](3)", "'bbbb'")) checkmessage("aaa={13}; local bbbb=1; aaa[bbbb](3)", "number") checkmessage("aaa=(1)..{}", "a table value") +checkmessage("local function foo (...t) return t.xx + 1 end; foo()", + "field 'xx'") + -- bug in 5.4.6 checkmessage("a = {_ENV = {}}; print(a._ENV.x + 1)", "field 'x'") From 0c16a42d61d08266033ff63bc5dfade6312b7359 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Tue, 21 Apr 2026 15:27:22 -0300 Subject: [PATCH 26/47] Details Added compiler option LUA_NODEBUGLIB to make Lua with no open debug library + small improvements in the manual. --- lua.c | 8 +++++++- manual/manual.of | 24 ++++++++++++++---------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/lua.c b/lua.c index 3107a674..858a04c0 100644 --- a/lua.c +++ b/lua.c @@ -714,7 +714,13 @@ static void doREPL (lua_State *L) { /* }================================================================== */ #if !defined(luai_openlibs) -#define luai_openlibs(L) luaL_openselectedlibs(L, ~0, 0) +#if defined(LUA_NODEBUGLIB) +/* With this option, code must require the debug library before using it */ +#define luai_openlibs(L) luaL_openselectedlibs(L, ~LUA_DBLIBK, LUA_DBLIBK) +#else +/* The default is to open all standard libraries */ +#define luai_openlibs(L) luaL_openselectedlibs(L, ~0, 0) +#endif #endif diff --git a/manual/manual.of b/manual/manual.of index 5eb2fb1f..5e113336 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -3962,8 +3962,8 @@ this confuses the next call to @Lid{lua_next}. This function may raise an error if the given key is neither @nil nor present in the table. -See function @Lid{next} for the caveats of modifying -the table during its traversal. + +See function @Lid{next} for more details about the traversal. } @@ -4448,7 +4448,7 @@ Starts and resumes a coroutine in the given thread @id{L}. To start a coroutine, you push the main function plus any arguments onto the empty stack of the thread. -then you call @Lid{lua_resume}, +Then you call @Lid{lua_resume}, with @id{nargs} being the number of arguments. The function returns when the coroutine suspends, finishes its execution, or raises an unprotected error. @@ -4628,7 +4628,7 @@ You can resume threads with status @Lid{LUA_OK} } @APIEntry{size_t lua_stringtonumber (lua_State *L, const char *s);| -@apii{0,1,-} +@apii{0,0|1,-} Converts the zero-terminated string @id{s} to a number, pushes that number into the stack, @@ -4958,7 +4958,7 @@ Lua calls the given @x{continuation function} @id{k} to continue the execution of the @N{C function} that yielded @see{continuations}. This continuation function receives the same stack from the previous function, -with the @id{n} results removed and +with all the results (@id{nresults}) removed and replaced by the arguments passed to @Lid{lua_resume}. Moreover, the continuation function receives the value @id{ctx} @@ -5048,7 +5048,7 @@ the function was defined in a string where } @item{@id{srclen}| -The length of the string @id{source}. +the length of the string @id{source}. } @item{@id{short_src}| @@ -5212,7 +5212,7 @@ running at the given level; } @item{@Char{S}| -fills in the fields @id{source}, @id{short_src}, +fills in the fields @id{source}, @id{srclen}, @id{short_src}, @id{linedefined}, @id{lastlinedefined}, and @id{what}; } @@ -5388,7 +5388,9 @@ Returns @id{NULL} (and pops nothing) when the index is greater than the number of active local variables. -Parameters @id{ar} and @id{n} are as in the function @Lid{lua_getlocal}. +Parameters @id{ar} and @id{n} are as in the function @Lid{lua_getlocal}, +except that @id{ar} cannot be @id{NULL}, +as @id{lua_setlocal} only operates on activation records. } @@ -6832,8 +6834,7 @@ for k,v in pairs(t) do @rep{body} end } will iterate over all key@En{}value pairs of table @id{t}. -See function @Lid{next} for the caveats of modifying -the table during its traversal. +See function @Lid{next} for more details about the traversal. } @@ -9123,6 +9124,7 @@ which automatically removes the file when the program ends. This library provides the functionality of the @link{debugI|debug interface} to Lua programs. + You should exert care when using this library. Several of its functions violate basic assumptions about Lua code @@ -9132,6 +9134,8 @@ that userdata metatables cannot be changed by Lua code; that Lua programs do not crash) and therefore can compromise otherwise secure code. Moreover, some functions in this library may be slow. +It is good practice to always require this library explicitly +before using it. All functions in this library are provided inside the @defid{debug} table. From 3228a97c6a953dcf397944161bb64b12f1ff5384 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Thu, 23 Apr 2026 17:57:42 -0300 Subject: [PATCH 27/47] Bug: 'lua_load' does not preserve the stack 'lua_load' does not preserve the stack through the calls to the reader function, as it should. Immediately after the first call (to detect whether chunk is binary) it adds stuff, and it also adds a new table when starting the compilation of each new function. --- ldo.c | 32 +++++++++++++++++++++++++++++--- ldo.h | 1 + lparser.c | 23 +++++++++++------------ lparser.h | 5 +++-- lundump.c | 13 +++++-------- lundump.h | 4 ++-- testes/calls.lua | 26 ++++++++++++++++++++++++++ 7 files changed, 77 insertions(+), 27 deletions(-) diff --git a/ldo.c b/ldo.c index 12e0364b..ec360ee8 100644 --- a/ldo.c +++ b/ldo.c @@ -1128,28 +1128,54 @@ static void checkmode (lua_State *L, const char *mode, const char *x) { } +/* +** Before the first call to the reader function, Lua reserves a slot +** with a table for anchoring stuff. +*/ static void f_parser (lua_State *L, void *ud) { LClosure *cl; struct SParser *p = cast(struct SParser *, ud); const char *mode = p->mode ? p->mode : "bt"; - int c = zgetc(p->z); /* read first character */ + int c; + Table *anchor; + ptrdiff_t otop = savestack(L, L->top.p); /* original top */ + luaD_checkstack(L, 2); + anchor = luaH_new(L); /* create the anchor table */ + sethvalue2s(L, L->top.p++, anchor); /* anchor the anchor table */ + c = zgetc(p->z); /* read first character */ if (c == LUA_SIGNATURE[0]) { int fixed = 0; if (strchr(mode, 'B') != NULL) fixed = 1; else checkmode(L, mode, "binary"); - cl = luaU_undump(L, p->z, p->name, fixed); + cl = luaU_undump(L, p->z, anchor, p->name, fixed); } else { checkmode(L, mode, "text"); - cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c); + cl = luaY_parser(L, p->z, anchor, &p->buff, &p->dyd, p->name, c); } + L->top.p = restorestack(L, otop); /* restore stack */ + setclLvalue2s(L, L->top.p++, cl); /* push closure */ lua_assert(cl->nupvalues == cl->p->sizeupvalues); luaF_initupvals(L, cl); } +/* +** Anchor an object in a table in the stack. First, anchor the object +** temporarily in the stack, as luaH_set may call an emergency GC. +** Then, add it in the table with itself as its key. +*/ +void luaD_anchorobj (lua_State *L, Table *anchor, GCObject *obj) { + setgcovalue(L, s2v(L->top.p++), obj); /* temporary anchor in the stack */ + luaH_set(L, anchor, s2v(L->top.p - 1), s2v(L->top.p - 1)); + /* Because this is a new key, luaH_set will call the GC barrier, so + we don't need to call the barrier again here */ + L->top.p--; +} + + TStatus luaD_protectedparser (lua_State *L, ZIO *z, const char *name, const char *mode) { struct SParser p; diff --git a/ldo.h b/ldo.h index b6472954..2b3b0db4 100644 --- a/ldo.h +++ b/ldo.h @@ -90,6 +90,7 @@ LUAI_FUNC int luaD_growstack (lua_State *L, int n, int raiseerror); LUAI_FUNC void luaD_shrinkstack (lua_State *L); LUAI_FUNC void luaD_inctop (lua_State *L); LUAI_FUNC int luaD_checkminstack (lua_State *L); +LUAI_FUNC void luaD_anchorobj (lua_State *L, Table *anchor, GCObject *obj); LUAI_FUNC l_noret luaD_throw (lua_State *L, TStatus errcode); LUAI_FUNC l_noret luaD_throwbaselevel (lua_State *L, TStatus errcode); diff --git a/lparser.c b/lparser.c index 6b87773e..1850d6dc 100644 --- a/lparser.c +++ b/lparser.c @@ -821,8 +821,7 @@ static void open_func (LexState *ls, FuncState *fs, BlockCnt *bl) { luaC_objbarrier(L, f, f->source); f->maxstacksize = 2; /* registers 0/1 are always valid */ fs->kcache = luaH_new(L); /* create table for function */ - sethvalue2s(L, L->top.p, fs->kcache); /* anchor it */ - luaD_inctop(L); + luaD_anchorobj(L, ls->h, obj2gco(fs->kcache)); /* anchor it */ enterblock(fs, bl, 0); } @@ -831,6 +830,7 @@ static void close_func (LexState *ls) { lua_State *L = ls->L; FuncState *fs = ls->fs; Proto *f = fs->f; + TValue temp; luaK_ret(fs, luaY_nvarstack(fs), 0); /* final return */ leaveblock(fs); lua_assert(fs->bl == NULL); @@ -843,8 +843,10 @@ static void close_func (LexState *ls) { luaM_shrinkvector(L, f->p, f->sizep, fs->np, Proto *); luaM_shrinkvector(L, f->locvars, f->sizelocvars, fs->ndebugvars, LocVar); luaM_shrinkvector(L, f->upvalues, f->sizeupvalues, fs->nups, Upvaldesc); + /* remove kcache table from scanner table ("weigh" its anchor) */ + sethvalue(L, &temp, fs->kcache); /* key to be set to nil */ + luaH_set(L, ls->h, &temp, &G(L)->nilvalue); ls->fs = fs->prev; - L->top.p--; /* pop kcache table */ luaC_checkGC(L); } @@ -2174,16 +2176,14 @@ static void mainfunc (LexState *ls, FuncState *fs) { } -LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, +LClosure *luaY_parser (lua_State *L, ZIO *z, Table *anchor, Mbuffer *buff, Dyndata *dyd, const char *name, int firstchar) { LexState lexstate; FuncState funcstate; - LClosure *cl = luaF_newLclosure(L, 1); /* create main closure */ - setclLvalue2s(L, L->top.p, cl); /* anchor it (to avoid being collected) */ - luaD_inctop(L); - lexstate.h = luaH_new(L); /* create table for scanner */ - sethvalue2s(L, L->top.p, lexstate.h); /* anchor it */ - luaD_inctop(L); + LClosure *cl; + lexstate.h = anchor; /* table for scanner */ + cl = luaF_newLclosure(L, 1); /* create main closure */ + luaD_anchorobj(L, anchor, obj2gco(cl)); /* anchor it in scanner table */ funcstate.f = cl->p = luaF_newproto(L); luaC_objbarrier(L, cl, cl->p); funcstate.f->source = luaS_new(L, name); /* create and anchor TString */ @@ -2196,7 +2196,6 @@ LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, lua_assert(!funcstate.prev && funcstate.nups == 1 && !lexstate.fs); /* all scopes should be correctly finished */ lua_assert(dyd->actvar.n == 0 && dyd->gt.n == 0 && dyd->label.n == 0); - L->top.p--; /* remove scanner's table */ - return cl; /* closure is on the stack, too */ + return cl; } diff --git a/lparser.h b/lparser.h index a30df04f..4fad6bdc 100644 --- a/lparser.h +++ b/lparser.h @@ -189,8 +189,9 @@ typedef struct FuncState { LUAI_FUNC lu_byte luaY_nvarstack (FuncState *fs); LUAI_FUNC void luaY_checklimit (FuncState *fs, int v, int l, const char *what); -LUAI_FUNC LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, - Dyndata *dyd, const char *name, int firstchar); +LUAI_FUNC LClosure *luaY_parser (lua_State *L, ZIO *z, Table *anchor, + Mbuffer *buff, Dyndata *dyd, + const char *name, int firstchar); #endif diff --git a/lundump.c b/lundump.c index 3b61cc8c..d5fdc64b 100644 --- a/lundump.c +++ b/lundump.c @@ -392,7 +392,8 @@ static void checkHeader (LoadState *S) { /* ** Load precompiled chunk. */ -LClosure *luaU_undump (lua_State *L, ZIO *Z, const char *name, int fixed) { +LClosure *luaU_undump (lua_State *L, ZIO *Z, Table *anchor, const char *name, + int fixed) { LoadState S; LClosure *cl; if (*name == '@' || *name == '=') @@ -405,20 +406,16 @@ LClosure *luaU_undump (lua_State *L, ZIO *Z, const char *name, int fixed) { S.fixed = cast_byte(fixed); S.offset = 1; /* fist byte was already read */ checkHeader(&S); - cl = luaF_newLclosure(L, loadByte(&S)); - setclLvalue2s(L, L->top.p, cl); - luaD_inctop(L); - S.h = luaH_new(L); /* create list of saved strings */ + S.h = anchor; S.nstr = 0; - sethvalue2s(L, L->top.p, S.h); /* anchor it */ - luaD_inctop(L); + cl = luaF_newLclosure(L, loadByte(&S)); + luaD_anchorobj(L, anchor, obj2gco(cl)); cl->p = luaF_newproto(L); luaC_objbarrier(L, cl, cl->p); loadFunction(&S, cl->p); if (cl->nupvalues != cl->p->sizeupvalues) error(&S, "corrupted chunk"); luai_verifycode(L, cl->p); - L->top.p--; /* pop table */ return cl; } diff --git a/lundump.h b/lundump.h index c4e06f9e..186e25f8 100644 --- a/lundump.h +++ b/lundump.h @@ -30,8 +30,8 @@ /* load one chunk; from lundump.c */ -LUAI_FUNC LClosure* luaU_undump (lua_State* L, ZIO* Z, const char* name, - int fixed); +LUAI_FUNC LClosure* luaU_undump (lua_State* L, ZIO* Z, Table *anchor, + const char* name, int fixed); /* dump one chunk; from ldump.c */ LUAI_FUNC int luaU_dump (lua_State* L, const Proto* f, lua_Writer w, diff --git a/testes/calls.lua b/testes/calls.lua index 0dacb85a..cd4510a2 100644 --- a/testes/calls.lua +++ b/testes/calls.lua @@ -372,6 +372,32 @@ do -- another bug (in 5.4.0) end +if T then + -- check stack level when calling reader function + local function get (str) + local pos = 0 + local level = nil + return function () + pos = pos + 1 + local c = string.sub(str, pos, pos) + local newlevel = T.stacklevel() + if not level then + level = newlevel + else + assert(level == newlevel) + end + return #c > 0 and c or nil + end + end + + local str = "local function foo () end; return 121" + assert(assert(load(get(str)))() == 121) + + str = string.dump(load(str)) + assert(assert(load(get(str)))() == 121) +end + + x = string.dump(load("x = 1; return x")) a = assert(load(read1(x), nil, "b")) assert(a() == 1 and _G.x == 1) From 4c5d5063a54c0088729b16fb25a333f0f9f836b0 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Thu, 23 Apr 2026 17:58:55 -0300 Subject: [PATCH 28/47] 'load' reader function doesn't need to preserve stack --- lbaselib.c | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/lbaselib.c b/lbaselib.c index 47f7bdec..3962ea53 100644 --- a/lbaselib.c +++ b/lbaselib.c @@ -366,33 +366,24 @@ static int luaB_loadfile (lua_State *L) { /* -** reserved slot, above all arguments, to hold a copy of the returned -** string to avoid it being collected while parsed. 'load' has four -** optional arguments (chunk, source name, mode, and environment). -*/ -#define RESERVEDSLOT 5 - - -/* -** Reader for generic 'load' function: 'lua_load' uses the -** stack for internal stuff, so the reader cannot change the -** stack top. Instead, it keeps its resulting string in a -** reserved slot inside the stack. +** Reader for generic 'load' function. */ static const char *generic_reader (lua_State *L, void *ud, size_t *size) { - (void)(ud); /* not used */ + int *firstcall = cast(int *, ud); luaL_checkstack(L, 2, "too many nested functions"); + if (*firstcall) + *firstcall = 0; + else + lua_pop(L, 1); /* remove previous result */ lua_pushvalue(L, 1); /* get function */ lua_call(L, 0, 1); /* call it */ if (lua_isnil(L, -1)) { - lua_pop(L, 1); /* pop result */ *size = 0; return NULL; } else if (l_unlikely(!lua_isstring(L, -1))) luaL_error(L, "reader function must return a string"); - lua_replace(L, RESERVEDSLOT); /* save string in reserved slot */ - return lua_tolstring(L, RESERVEDSLOT, size); + return lua_tolstring(L, -1, size); } @@ -407,10 +398,10 @@ static int luaB_load (lua_State *L) { status = luaL_loadbufferx(L, s, l, chunkname, mode); } else { /* loading from a reader function */ + int firstcall = 1; /* userdata for generic_reader */ const char *chunkname = luaL_optstring(L, 2, "=(load)"); luaL_checktype(L, 1, LUA_TFUNCTION); - lua_settop(L, RESERVEDSLOT); /* create reserved slot */ - status = lua_load(L, generic_reader, NULL, chunkname, mode); + status = lua_load(L, generic_reader, &firstcall, chunkname, mode); } return load_aux(L, status, env); } From ae23e726018bd31a25c1279600328d90207ec81c Mon Sep 17 00:00:00 2001 From: Roberto I Date: Thu, 23 Apr 2026 18:00:23 -0300 Subject: [PATCH 29/47] new macro 'setnilvalue2s' --- lapi.c | 12 ++++++------ ldebug.c | 2 +- ldo.c | 10 +++++----- lgc.c | 2 +- lobject.h | 3 ++- lstate.c | 4 ++-- ltests.c | 2 +- ltm.c | 10 +++++----- lvm.c | 2 +- 9 files changed, 24 insertions(+), 23 deletions(-) diff --git a/lapi.c b/lapi.c index 53eb1719..fb994594 100644 --- a/lapi.c +++ b/lapi.c @@ -187,7 +187,7 @@ LUA_API void lua_settop (lua_State *L, int idx) { api_check(L, idx <= ci->top.p - (func + 1), "new top too large"); diff = ((func + 1) + idx) - L->top.p; for (; diff > 0; diff--) - setnilvalue(s2v(L->top.p++)); /* clear new slots */ + setnilvalue2s(L->top.p++); /* clear new slots */ } else { api_check(L, -(idx+1) <= (L->top.p - (func + 1)), "invalid new top"); @@ -210,7 +210,7 @@ LUA_API void lua_closeslot (lua_State *L, int idx) { api_check(L, (L->ci->callstatus & CIST_TBC) && (L->tbclist.p == level), "no variable to close at given level"); level = luaF_close(L, level, CLOSEKTOP, 0); - setnilvalue(s2v(level)); + setnilvalue2s(level); lua_unlock(L); } @@ -513,7 +513,7 @@ LUA_API const void *lua_topointer (lua_State *L, int idx) { LUA_API void lua_pushnil (lua_State *L) { lua_lock(L); - setnilvalue(s2v(L->top.p)); + setnilvalue2s(L->top.p); api_incr_top(L); lua_unlock(L); } @@ -570,7 +570,7 @@ LUA_API const char *lua_pushexternalstring (lua_State *L, LUA_API const char *lua_pushstring (lua_State *L, const char *s) { lua_lock(L); if (s == NULL) - setnilvalue(s2v(L->top.p)); + setnilvalue2s(L->top.p); else { TString *ts; ts = luaS_new(L, s); @@ -743,7 +743,7 @@ LUA_API int lua_geti (lua_State *L, int idx, lua_Integer n) { static int finishrawget (lua_State *L, lu_byte tag) { if (tagisempty(tag)) /* avoid copying empty items to the stack */ - setnilvalue(s2v(L->top.p)); + setnilvalue2s(L->top.p); api_incr_top(L); lua_unlock(L); return novariant(tag); @@ -836,7 +836,7 @@ LUA_API int lua_getiuservalue (lua_State *L, int idx, int n) { o = index2value(L, idx); api_check(L, ttisfulluserdata(o), "full userdata expected"); if (n <= 0 || n > uvalue(o)->nuvalue) { - setnilvalue(s2v(L->top.p)); + setnilvalue2s(L->top.p); t = LUA_TNONE; } else { diff --git a/ldebug.c b/ldebug.c index 2665ec7b..5293ccb9 100644 --- a/ldebug.c +++ b/ldebug.c @@ -291,7 +291,7 @@ static int nextline (const Proto *p, int currentline, int pc) { static void collectvalidlines (lua_State *L, Closure *f) { if (!LuaClosure(f)) { - setnilvalue(s2v(L->top.p)); + setnilvalue2s(L->top.p); api_incr_top(L); } else { diff --git a/ldo.c b/ldo.c index ec360ee8..1f18b186 100644 --- a/ldo.c +++ b/ldo.c @@ -349,7 +349,7 @@ int luaD_reallocstack (lua_State *L, int newsize, int raiseerror) { correctstack(L, oldstack); /* change offsets back to pointers */ L->stack_last.p = L->stack.p + newsize; for (i = oldsize + EXTRA_STACK; i < newsize + EXTRA_STACK; i++) - setnilvalue(s2v(newstack + i)); /* erase new segment */ + setnilvalue2s(newstack + i); /* erase new segment */ return 1; } @@ -554,7 +554,7 @@ l_sinline void genmoveresults (lua_State *L, StkId res, int nres, for (i = 0; i < nres; i++) /* move all results to correct place */ setobjs2s(L, res + i, firstresult + i); for (; i < wanted; i++) /* complete wanted number of results */ - setnilvalue(s2v(res + i)); + setnilvalue2s(res + i); L->top.p = res + wanted; /* top points after the last result */ } @@ -574,7 +574,7 @@ l_sinline void moveresults (lua_State *L, StkId res, int nres, return; case 1 + 1: /* one value needed */ if (nres == 0) /* no results? */ - setnilvalue(s2v(res)); /* adjust with nil */ + setnilvalue2s(res); /* adjust with nil */ else /* at least one result */ setobjs2s(L, res, L->top.p - nres); /* move it to proper place */ L->top.p = res + 1; @@ -694,7 +694,7 @@ int luaD_pretailcall (lua_State *L, CallInfo *ci, StkId func, setobjs2s(L, ci->func.p + i, func + i); func = ci->func.p; /* moved-down function */ for (; narg1 <= nfixparams; narg1++) - setnilvalue(s2v(func + narg1)); /* complete missing arguments */ + setnilvalue2s(func + narg1); /* complete missing arguments */ ci->top.p = func + 1 + fsize; /* top for new function */ lua_assert(ci->top.p <= L->stack_last.p); ci->u.l.savedpc = p->code; /* starting point */ @@ -741,7 +741,7 @@ CallInfo *luaD_precall (lua_State *L, StkId func, int nresults) { L->ci = ci = prepCallInfo(L, func, status, func + 1 + fsize); ci->u.l.savedpc = p->code; /* starting point */ for (; narg < nfixparams; narg++) - setnilvalue(s2v(L->top.p++)); /* complete missing arguments */ + setnilvalue2s(L->top.p++); /* complete missing arguments */ lua_assert(ci->top.p <= L->stack_last.p); return ci; } diff --git a/lgc.c b/lgc.c index 0f89451c..a463e41e 100644 --- a/lgc.c +++ b/lgc.c @@ -709,7 +709,7 @@ static l_mem traversethread (global_State *g, lua_State *th) { if (!g->gcemergency) luaD_shrinkstack(th); /* do not change stack in emergency cycle */ for (o = th->top.p; o < th->stack_last.p + EXTRA_STACK; o++) - setnilvalue(s2v(o)); /* clear dead stack slice */ + setnilvalue2s(o); /* clear dead stack slice */ /* 'remarkupvals' may have removed thread from 'twups' list */ if (!isintwups(th) && th->openupval != NULL) { th->twups = g->twups; /* link it back to the list */ diff --git a/lobject.h b/lobject.h index 156c942f..bf5b65b4 100644 --- a/lobject.h +++ b/lobject.h @@ -208,7 +208,8 @@ typedef union { #define ttisstrictnil(o) checktag((o), LUA_VNIL) -#define setnilvalue(obj) settt_(obj, LUA_VNIL) +#define setnilvalue(obj) settt_(obj, LUA_VNIL) +#define setnilvalue2s(stk) setnilvalue(s2v(stk)) #define isabstkey(v) checktag((v), LUA_VABSTKEY) diff --git a/lstate.c b/lstate.c index 7d341991..ada0b856 100644 --- a/lstate.c +++ b/lstate.c @@ -151,7 +151,7 @@ LUAI_FUNC void luaE_incCstack (lua_State *L) { static void resetCI (lua_State *L) { CallInfo *ci = L->ci = &L->base_ci; ci->func.p = L->stack.p; - setnilvalue(s2v(ci->func.p)); /* 'function' entry for basic 'ci' */ + setnilvalue2s(ci->func.p); /* 'function' entry for basic 'ci' */ ci->top.p = ci->func.p + 1 + LUA_MINSTACK; /* +1 for 'function' entry */ ci->u.c.k = NULL; ci->callstatus = CIST_C; @@ -166,7 +166,7 @@ static void stack_init (lua_State *L1, lua_State *L) { L1->stack.p = luaM_newvector(L, BASIC_STACK_SIZE + EXTRA_STACK, StackValue); L1->tbclist.p = L1->stack.p; for (i = 0; i < BASIC_STACK_SIZE + EXTRA_STACK; i++) - setnilvalue(s2v(L1->stack.p + i)); /* erase new stack */ + setnilvalue2s(L1->stack.p + i); /* erase new stack */ L1->stack_last.p = L1->stack.p + BASIC_STACK_SIZE; /* initialize first ci */ resetCI(L1); diff --git a/ltests.c b/ltests.c index 6ae5f472..2bf5545a 100644 --- a/ltests.c +++ b/ltests.c @@ -1145,7 +1145,7 @@ static int table_query (lua_State *L) { if (!tagisempty(*getArrTag(t, i))) arr2obj(t, cast_uint(i), s2v(L->top.p)); else - setnilvalue(s2v(L->top.p)); + setnilvalue2s(L->top.p); api_incr_top(L); lua_pushnil(L); } diff --git a/ltm.c b/ltm.c index f2a373f8..cfe90e30 100644 --- a/ltm.c +++ b/ltm.c @@ -262,7 +262,7 @@ static void buildhiddenargs (lua_State *L, CallInfo *ci, const Proto *p, /* move fixed parameters to after the copied function */ for (i = 1; i <= nfixparams; i++) { setobjs2s(L, L->top.p++, ci->func.p + i); - setnilvalue(s2v(ci->func.p + i)); /* erase original parameter (for GC) */ + setnilvalue2s(ci->func.p + i); /* erase original parameter (for GC) */ } ci->func.p += totalargs + 1; /* 'func' now lives after hidden arguments */ ci->top.p += totalargs + 1; @@ -283,7 +283,7 @@ void luaT_adjustvarargs (lua_State *L, CallInfo *ci, const Proto *p) { lua_assert(p->flag & PF_VAHID); buildhiddenargs(L, ci, p, totalargs, nfixparams, nextra); /* set vararg parameter to nil */ - setnilvalue(s2v(ci->func.p + nfixparams + 1)); + setnilvalue2s(ci->func.p + nfixparams + 1); lua_assert(L->top.p <= ci->top.p && ci->top.p <= L->stack_last.p); } } @@ -307,7 +307,7 @@ void luaT_getvararg (CallInfo *ci, StkId ra, TValue *rc) { return; } } - setnilvalue(s2v(ra)); /* else produce nil */ + setnilvalue2s(ra); /* else produce nil */ } @@ -355,10 +355,10 @@ void luaT_getvarargs (lua_State *L, CallInfo *ci, StkId where, int wanted, for (i = 0; i < touse; i++) { lu_byte tag = luaH_getint(h, i + 1, s2v(where + i)); if (tagisempty(tag)) - setnilvalue(s2v(where + i)); + setnilvalue2s(where + i); } } for (; i < wanted; i++) /* complete required results with nil */ - setnilvalue(s2v(where + i)); + setnilvalue2s(where + i); } diff --git a/lvm.c b/lvm.c index 96ae1639..f9e87b61 100644 --- a/lvm.c +++ b/lvm.c @@ -303,7 +303,7 @@ lu_byte luaV_finishget (lua_State *L, const TValue *t, TValue *key, else { /* 't' is a table */ tm = fasttm(L, hvalue(t)->metatable, TM_INDEX); /* table's metamethod */ if (tm == NULL) { /* no metamethod? */ - setnilvalue(s2v(val)); /* result is nil */ + setnilvalue2s(val); /* result is nil */ return LUA_VNIL; } /* else will try the metamethod */ From 0da6d320f757bc9241a33df06f3597598845cf0a Mon Sep 17 00:00:00 2001 From: Roberto Ierusalimschy Date: Tue, 28 Apr 2026 13:44:29 -0300 Subject: [PATCH 30/47] Details --- lcode.c | 8 ++++---- makefile | 9 +++++++-- manual/2html | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lcode.c b/lcode.c index 33cbd687..e0024432 100644 --- a/lcode.c +++ b/lcode.c @@ -663,11 +663,11 @@ static int boolT (FuncState *fs) { ** Add nil to list of constants and return its index. */ static int nilK (FuncState *fs) { - TValue k, v; - setnilvalue(&v); + lua_State *L = fs->ls->L; + TValue k; /* cannot use nil as key; instead use table itself */ - sethvalue(fs->ls->L, &k, fs->kcache); - return k2proto(fs, &k, &v); + sethvalue(L, &k, fs->kcache); + return k2proto(fs, &k, &G(L)->nilvalue); } diff --git a/makefile b/makefile index fa165bca..8144dcb6 100644 --- a/makefile +++ b/makefile @@ -62,8 +62,11 @@ CWARNS= $(CWARNSCPP) $(CWARNSC) $(CWARNGCC) # ASAN_OPTIONS="detect_invalid_pointer_pairs=2". # -fsanitize=undefined (you may need to add "-lubsan" to libs) # -fsanitize=pointer-subtract -fsanitize=address -fsanitize=pointer-compare -# TESTS= -DLUA_USER_H='"ltests.h"' -Og -g +# Test mode: Add test library, turn on asserts, redefine several +# constants ("to give some bugs a chance"), track memory use, and add +# debug information. +# TESTS= -DLUA_USER_H='"ltests.h"' -Og -g LOCAL = $(TESTS) $(CWARNS) @@ -71,13 +74,15 @@ LOCAL = $(TESTS) $(CWARNS) # To enable Linux goodies, -DLUA_USE_LINUX # For C89, "-std=c89 -DLUA_USE_C89" # Note that Linux/Posix options are not compatible with C89 +# (For 32-bit, add option "-m32" to MYCFLAGS and MYLDFLAGS.) MYCFLAGS= $(LOCAL) -std=c99 -DLUA_USE_LINUX MYLDFLAGS= -Wl,-E MYLIBS= -ldl CC= gcc -CFLAGS= -Wall -O2 $(MYCFLAGS) -fno-stack-protector -fno-common -march=native +# (Optionally we can use -march=native -mno-avx512f.) +CFLAGS= -Wall -O2 $(MYCFLAGS) -fno-stack-protector -fno-common AR= ar rc RANLIB= ranlib RM= rm -f diff --git a/manual/2html b/manual/2html index d3b88b34..243f3f22 100755 --- a/manual/2html +++ b/manual/2html @@ -1,4 +1,4 @@ -#!/usr/bin/env lua5.3 +#!/usr/bin/env lua -- special marks: From 36c1f6d949a4d3dfcbe898d80b1be1efe8e5325c Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 29 Apr 2026 15:17:55 -0300 Subject: [PATCH 31/47] Small correction in luaP_opmodes OP_VARARGPREP neither sets nor uses L->top. --- lopcodes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lopcodes.c b/lopcodes.c index c4828bfc..da64ff18 100644 --- a/lopcodes.c +++ b/lopcodes.c @@ -104,7 +104,7 @@ LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = { ,opmode(0, 1, 0, 0, 1, iABC) /* OP_VARARG */ ,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETVARG */ ,opmode(0, 0, 0, 0, 0, iABx) /* OP_ERRNNIL */ - ,opmode(0, 0, 1, 0, 0, iABC) /* OP_VARARGPREP */ + ,opmode(0, 0, 0, 0, 0, iABC) /* OP_VARARGPREP */ ,opmode(0, 0, 0, 0, 0, iAx) /* OP_EXTRAARG */ }; From 53b41d0cddd80bf33fdc631bdd32e3ba53842b89 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Fri, 8 May 2026 15:01:59 -0300 Subject: [PATCH 32/47] Avoid warning in some compilers In function 'funcargs', some compilers can complain that 'args' can be used unitialized, due to the default case (syntax error). --- lparser.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lparser.c b/lparser.c index 1850d6dc..af2b64d1 100644 --- a/lparser.c +++ b/lparser.c @@ -1166,6 +1166,7 @@ static void funcargs (LexState *ls, expdesc *f) { } default: { luaX_syntaxerror(ls, "function arguments expected"); + return; /* to avoid warnings */ } } lua_assert(f->k == VNONRELOC); From 0465c23b3ee214ea3a117ab9d69a83cf85e7a82f Mon Sep 17 00:00:00 2001 From: Roberto I Date: Thu, 28 May 2026 15:10:17 -0300 Subject: [PATCH 33/47] Cleaning 'luaP_isIT' and 'luaP_isOT' - 'luaP_isOT' is only used for tests, so it is defined as a macro to avoid wasting space with an unused function. - 'luaP_isIT' must include OP_VARARGPREP. --- lcode.c | 2 -- lopcodes.c | 23 +++++++---------------- lopcodes.h | 15 ++++++++++----- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/lcode.c b/lcode.c index e0024432..8b61d5de 100644 --- a/lcode.c +++ b/lcode.c @@ -1934,8 +1934,6 @@ void luaK_finish (FuncState *fs) { p->flag &= cast_byte(~PF_VAHID); /* then it will not use hidden args. */ for (i = 0; i < fs->pc; i++) { Instruction *pc = &p->code[i]; - /* avoid "not used" warnings when assert is off (for 'onelua.c') */ - (void)luaP_isOT; (void)luaP_isIT; lua_assert(i == 0 || luaP_isOT(*(pc - 1)) == luaP_isIT(*pc)); switch (GET_OPCODE(*pc)) { case OP_RETURN0: case OP_RETURN1: { diff --git a/lopcodes.c b/lopcodes.c index da64ff18..bb1a4162 100644 --- a/lopcodes.c +++ b/lopcodes.c @@ -109,30 +109,21 @@ LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = { }; - -/* -** Check whether instruction sets top for next instruction, that is, -** it results in multiple values. -*/ -int luaP_isOT (Instruction i) { - OpCode op = GET_OPCODE(i); - switch (op) { - case OP_TAILCALL: return 1; - default: - return testOTMode(op) && GETARG_C(i) == 0; - } -} +#define testITMode(m) (luaP_opmodes[m] & (1 << 5)) /* -** Check whether instruction uses top from previous instruction, that is, -** it accepts multiple results. +** Check whether instruction uses top. That happens for OP_VARARGPREP +** and for instructions that use multiple values set by the previous +** instruction. */ int luaP_isIT (Instruction i) { OpCode op = GET_OPCODE(i); switch (op) { case OP_SETLIST: - return testITMode(GET_OPCODE(i)) && GETARG_vB(i) == 0; + return GETARG_vB(i) == 0; + case OP_VARARGPREP: + return 1; default: return testITMode(GET_OPCODE(i)) && GETARG_B(i) == 0; } diff --git a/lopcodes.h b/lopcodes.h index b6bd182e..86cff065 100644 --- a/lopcodes.h +++ b/lopcodes.h @@ -417,8 +417,8 @@ OP_EXTRAARG/* Ax extra (larger) argument for previous opcode */ ** bits 0-2: op mode ** bit 3: instruction set register A ** bit 4: operator is a test (next instruction must be a jump) -** bit 5: instruction uses 'L->top' set by previous instruction (when B == 0) -** bit 6: instruction sets 'L->top' for next instruction (when C == 0) +** bit 5: used by 'luaP_isIT' +** bit 6: used by 'luaP_isOT' ** bit 7: instruction is an MM instruction (call a metamethod) */ @@ -427,12 +427,17 @@ LUAI_DDEC(const lu_byte luaP_opmodes[NUM_OPCODES];) #define getOpMode(m) (cast(enum OpMode, luaP_opmodes[m] & 7)) #define testAMode(m) (luaP_opmodes[m] & (1 << 3)) #define testTMode(m) (luaP_opmodes[m] & (1 << 4)) -#define testITMode(m) (luaP_opmodes[m] & (1 << 5)) -#define testOTMode(m) (luaP_opmodes[m] & (1 << 6)) #define testMMMode(m) (luaP_opmodes[m] & (1 << 7)) -LUAI_FUNC int luaP_isOT (Instruction i); +/* Check whether instruction sets top for next instruction, that is, +** it results in multiple values. Used only for tests. +*/ +#define luaP_isOT(i) \ + (GET_OPCODE(i) == OP_TAILCALL || \ + ((luaP_opmodes[GET_OPCODE(i)] & (1 << 6)) && GETARG_C(i) == 0)) + + LUAI_FUNC int luaP_isIT (Instruction i); From 40b76de2d77e66b70a9d4bf989c3f5340919973f Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 3 Jun 2026 11:58:39 -0300 Subject: [PATCH 34/47] Removed unused function 'luaD_inctop' Commit 3228a97 removed all its uses. --- ldo.c | 6 ------ ldo.h | 1 - 2 files changed, 7 deletions(-) diff --git a/ldo.c b/ldo.c index 1f18b186..fe8bf20f 100644 --- a/ldo.c +++ b/ldo.c @@ -430,12 +430,6 @@ void luaD_shrinkstack (lua_State *L) { luaE_shrinkCI(L); /* shrink CI list */ } - -void luaD_inctop (lua_State *L) { - L->top.p++; - luaD_checkstack(L, 1); -} - /* }================================================================== */ diff --git a/ldo.h b/ldo.h index 2b3b0db4..85a00e38 100644 --- a/ldo.h +++ b/ldo.h @@ -88,7 +88,6 @@ LUAI_FUNC void luaD_poscall (lua_State *L, CallInfo *ci, int nres); LUAI_FUNC int luaD_reallocstack (lua_State *L, int newsize, int raiseerror); LUAI_FUNC int luaD_growstack (lua_State *L, int n, int raiseerror); LUAI_FUNC void luaD_shrinkstack (lua_State *L); -LUAI_FUNC void luaD_inctop (lua_State *L); LUAI_FUNC int luaD_checkminstack (lua_State *L); LUAI_FUNC void luaD_anchorobj (lua_State *L, Table *anchor, GCObject *obj); From bc4bbcef651ba2870d6c68db16dc7d6ce6f68636 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 17 Jun 2026 11:20:10 -0300 Subject: [PATCH 35/47] Bug: 'luaL_newmetatable' used in a wrong way The call to 'luaL_newmetatable' in 'newbox' can leave an incomplete metatable in the registry, if 'luaL_setfuncs' raises a memory error. --- lauxlib.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lauxlib.c b/lauxlib.c index af44418a..8620c8b3 100644 --- a/lauxlib.c +++ b/lauxlib.c @@ -513,12 +513,25 @@ static const luaL_Reg boxmt[] = { /* box metamethods */ }; +/* +** Get/create metatable (MT) for boxes +*/ +static void getBoxMT (lua_State *L) { + const char *BOXMT = "_UBOX*"; /* key for the metatable */ + if (luaL_getmetatable(L, BOXMT) == LUA_TNIL) { /* MT not created yet? */ + luaL_newlibtable(L, boxmt); /* create it */ + luaL_setfuncs(L, boxmt, 0); /* initialize it */ + lua_copy(L, -1, -2); /* change stack from nil,MT to MT,MT */ + lua_setfield(L, LUA_REGISTRYINDEX, BOXMT); /* store MT in the registry */ + } +} + + static void newbox (lua_State *L) { UBox *box = (UBox *)lua_newuserdatauv(L, sizeof(UBox), 0); box->box = NULL; box->bsize = 0; - if (luaL_newmetatable(L, "_UBOX*")) /* creating metatable? */ - luaL_setfuncs(L, boxmt, 0); /* set its metamethods */ + getBoxMT(L); lua_setmetatable(L, -2); } From b996f8fd1be7fb711cc6f754a31a1c87d2c2fd9b Mon Sep 17 00:00:00 2001 From: Roberto I Date: Sun, 12 Jul 2026 14:57:55 -0300 Subject: [PATCH 36/47] Bug: Issues with write barrier for __newindex In 'luaV_finishset', there is an update on a table that is a field on another table. If the first table is the same as the one with the field (e.g., after 't.__newindex = t'), the update can change the value on that field (e.g., there may be a collision and the field is moved, or the field being updated is '__newindex' itself). After that, the barrier is called with the table stored in that field, which is not the correct table anymore. --- lvm.c | 18 ++++++++++++------ testes/events.lua | 12 ++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lvm.c b/lvm.c index f9e87b61..f83d47d1 100644 --- a/lvm.c +++ b/lvm.c @@ -360,13 +360,19 @@ void luaV_finishset (lua_State *L, const TValue *t, TValue *key, luaT_callTM(L, tm, t, key, val); return; } - t = tm; /* else repeat assignment over 'tm' */ - luaV_fastset(t, key, val, hres, luaH_pset); - if (hres == HOK) { - luaV_finishfastset(L, t, val); - return; /* done */ + t = tm; /* else must repeat assignment over 'tm' */ + /* do the equivalent to 'luaV_fastset', but saving 'h' */ + if (!ttistable(t)) + hres = HNOTATABLE; + else { + Table *h = hvalue(t); /* next call can change the value at 't' */ + hres = luaH_pset(h, key, val); + if (hres == HOK) { + luaC_barrierback(L, obj2gco(h), val); /* luaV_finishfastset */ + return; /* done */ + } } - /* else 'return luaV_finishset(L, t, key, val, slot)' (loop) */ + /* else 'return luaV_finishset(L, t, key, val, hres)' (loop) */ } luaG_runerror(L, "'__newindex' chain too long; possible loop"); } diff --git a/testes/events.lua b/testes/events.lua index 7e434b1f..fa9966ab 100644 --- a/testes/events.lua +++ b/testes/events.lua @@ -390,6 +390,18 @@ do for i=1, 10 do t[i] = 1 end end + +do -- bug since 5.4 + local parent = {} + parent.__newindex = parent + collectgarbage() + local child = setmetatable({}, parent) + child.__newindex = {x = "hello"} + collectgarbage("step") + assert(parent.__newindex.x == "hello") +end + + -- concat metamethod x numbers (bug in 5.1.1) c = {} local x From 6ca33260d26f1b8fab982b13c89de1f445361328 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Mon, 13 Jul 2026 15:18:37 -0300 Subject: [PATCH 37/47] Removed compiler option LUA_COMPAT_LOOPVAR It makes loop variables read-only, but it does not restore the old semantics, so it is not helpful for compatibility. --- lparser.c | 13 ++----------- luaconf.h | 9 --------- manual/manual.of | 3 --- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/lparser.c b/lparser.c index af2b64d1..fe29194c 100644 --- a/lparser.c +++ b/lparser.c @@ -1685,22 +1685,13 @@ static void forbody (LexState *ls, int base, int line, int nvars, int isgen) { } -/* -** Control whether for-loop control variables are read-only -*/ -#if LUA_COMPAT_LOOPVAR -#define LOOPVARKIND VDKREG -#else /* by default, these variables are read only */ -#define LOOPVARKIND RDKCONST -#endif - static void fornum (LexState *ls, TString *varname, int line) { /* fornum -> NAME = exp,exp[,exp] forbody */ FuncState *fs = ls->fs; int base = fs->freereg; new_localvarliteral(ls, "(for state)"); new_localvarliteral(ls, "(for state)"); - new_varkind(ls, varname, LOOPVARKIND); /* control variable */ + new_varkind(ls, varname, RDKCONST); /* control variable */ checknext(ls, '='); exp1(ls); /* initial value */ checknext(ls, ','); @@ -1727,7 +1718,7 @@ static void forlist (LexState *ls, TString *indexname) { new_localvarliteral(ls, "(for state)"); /* iterator function */ new_localvarliteral(ls, "(for state)"); /* state */ new_localvarliteral(ls, "(for state)"); /* closing var. (after swap) */ - new_varkind(ls, indexname, LOOPVARKIND); /* control variable */ + new_varkind(ls, indexname, RDKCONST); /* control variable */ /* other declared variables */ while (testnext(ls, ',')) { new_localvar(ls, str_checkname(ls)); diff --git a/luaconf.h b/luaconf.h index 0a71370f..042932e1 100644 --- a/luaconf.h +++ b/luaconf.h @@ -348,15 +348,6 @@ #endif -/* -@@ LUA_COMPAT_LOOPVAR makes for-loop control variables not read-only, -** as they were in previous versions. -*/ -#if !defined(LUA_COMPAT_LOOPVAR) -#define LUA_COMPAT_LOOPVAR 0 -#endif - - /* @@ LUA_COMPAT_MATHLIB controls the presence of several deprecated ** functions in the mathematical library. diff --git a/manual/manual.of b/manual/manual.of index 5e113336..65261695 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -9623,9 +9623,6 @@ makes @id{global} a regular word. The control variable in @Rw{for} loops is read only. If you need to change it, declare a local variable with the same name in the loop body. - -The compilation option @id{LUA_COMPAT_LOOPVAR} (see @id{luaconf.h}) -makes these variables regular (writable). } @item{ From 84938a7d2b680d2d28ec99606e84fe712efd9a69 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 15 Jul 2026 15:39:07 -0300 Subject: [PATCH 38/47] MacOS by default uses statically linked readline (plus detail in the manual) --- luaconf.h | 4 +--- manual/manual.of | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/luaconf.h b/luaconf.h index 042932e1..ea2c0b2e 100644 --- a/luaconf.h +++ b/luaconf.h @@ -79,9 +79,7 @@ #if defined(LUA_USE_MACOSX) #define LUA_USE_POSIX #define LUA_USE_DLOPEN /* macOS does not need -ldl */ -#if !defined(LUA_READLINELIB) -#define LUA_READLINELIB "libedit.dylib" -#endif +#define LUA_USE_READLINE #endif diff --git a/manual/manual.of b/manual/manual.of index 65261695..426ad453 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -2692,9 +2692,9 @@ which behaves like a nil value. @sect3{constchar|@title{Pointers to Strings} -Several functions in the API accept pointers (@T{const char*}) -to C strings. -Some of there parameters have an associated length (@T{size_t}). +Several functions in the API have parameters +that are pointers to C strings (@T{const char*}). +Some of these parameters have an associated length (@T{size_t}). Unless stated otherwise, when there is an associated length, the string can contain embedded zeros; From 9130ceb19d324472d135e8e644a01e8b16334c31 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Tue, 21 Jul 2026 16:51:04 -0300 Subject: [PATCH 39/47] Avoid casts to 'union GCUnion*' The union may have alignment requirements stricter than some of its members. Some checking tools (e.g., gcc with options -fsanitize) can then complain that the result of a cast from pointer to member to pointer to the union is misaligned. --- lstate.h | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lstate.h b/lstate.h index 01387283..30a560a7 100644 --- a/lstate.h +++ b/lstate.h @@ -383,7 +383,7 @@ typedef struct global_State { /* -** Union of all collectable objects (only for conversions) +** Union of all collectable objects ** ISO C99, 6.5.2.3 p.5: ** "if a union contains several structures that share a common initial ** sequence [...], and if the union object currently contains one @@ -403,32 +403,32 @@ union GCUnion { }; -/* -** ISO C99, 6.7.2.1 p.14: -** "A pointer to a union object, suitably converted, points to each of -** its members [...], and vice versa." +/* macros to convert a GCObject into a specific value +** ISO C99, 6.3.2.2 p.7: +** "A pointer to an object or incomplete type may be converted to a +** pointer to a different object or incomplete type. If the resulting +** pointer is not correctly aligned for the pointed-to type, the +** behavior is undefined. Otherwise, when converted back again, the +** result shall compare equal to the original pointer." */ -#define cast_u(o) cast(union GCUnion *, (o)) - -/* macros to convert a GCObject into a specific value */ -#define gco2ts(o) \ - check_exp(novariant((o)->tt) == LUA_TSTRING, &((cast_u(o))->ts)) -#define gco2u(o) check_exp((o)->tt == LUA_VUSERDATA, &((cast_u(o))->u)) -#define gco2lcl(o) check_exp((o)->tt == LUA_VLCL, &((cast_u(o))->cl.l)) -#define gco2ccl(o) check_exp((o)->tt == LUA_VCCL, &((cast_u(o))->cl.c)) -#define gco2cl(o) \ - check_exp(novariant((o)->tt) == LUA_TFUNCTION, &((cast_u(o))->cl)) -#define gco2t(o) check_exp((o)->tt == LUA_VTABLE, &((cast_u(o))->h)) -#define gco2p(o) check_exp((o)->tt == LUA_VPROTO, &((cast_u(o))->p)) -#define gco2th(o) check_exp((o)->tt == LUA_VTHREAD, &((cast_u(o))->th)) -#define gco2upv(o) check_exp((o)->tt == LUA_VUPVAL, &((cast_u(o))->upv)) +#define gco2(v,T,o) check_exp((o)->tt == v, cast(T*, o)) +#define gco2nv(t,T,o) check_exp(novariant((o)->tt) == t, cast(T*, o)) +#define gco2ts(o) gco2nv(LUA_TSTRING, TString, o) +#define gco2u(o) gco2(LUA_VUSERDATA, Udata, o) +#define gco2lcl(o) (&gco2(LUA_VLCL, Closure, o)->l) +#define gco2ccl(o) (&gco2(LUA_VCCL, Closure, o)->c) +#define gco2cl(o) gco2nv(LUA_TFUNCTION, Closure, o) +#define gco2t(o) gco2(LUA_VTABLE, Table, o) +#define gco2p(o) gco2(LUA_VPROTO, Proto, o) +#define gco2th(o) gco2(LUA_VTHREAD, lua_State, o) +#define gco2upv(o) gco2(LUA_VUPVAL, UpVal, o) /* ** macro to convert a Lua object into a GCObject */ #define obj2gco(v) \ - check_exp(novariant((v)->tt) >= LUA_TSTRING, &(cast_u(v)->gc)) + check_exp(novariant((v)->tt) >= LUA_TSTRING, cast(GCObject*, v)) /* actual number of total memory allocated */ From d5bbe955840c5c83e37ed19df4d392d7ba0970ed Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 22 Jul 2026 13:43:40 -0300 Subject: [PATCH 40/47] Details - Some api_checknelems changed to the more restrict api_checkpop. - Added a class to the html for APIs in the manual. - Comments and manual. --- lapi.c | 6 +++--- luaconf.h | 2 +- lvm.c | 2 +- manual/2html | 7 ++++--- manual/manual.of | 10 +++++++++- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lapi.c b/lapi.c index fb994594..8bc2ddd1 100644 --- a/lapi.c +++ b/lapi.c @@ -1210,7 +1210,7 @@ LUA_API int lua_gc (lua_State *L, int what, ...) { else if (g->GCdebt >= n - MAX_LMEM) /* no overflow? */ newdebt = g->GCdebt - n; else /* overflow */ - newdebt = -MAX_LMEM; /* set debt to miminum value */ + newdebt = -MAX_LMEM; /* set debt to mininum value */ luaE_setdebt(g, newdebt); luaC_condGC(L, (void)0, work = 1); if (work && g->gcstate == GCSpause) /* end of cycle? */ @@ -1299,8 +1299,8 @@ LUA_API void lua_toclose (lua_State *L, int idx) { LUA_API void lua_concat (lua_State *L, int n) { lua_lock(L); - api_checknelems(L, n); if (n > 0) { + api_checkpop(L, n); luaV_concat(L, n); luaC_checkGC(L); } @@ -1418,7 +1418,7 @@ LUA_API const char *lua_setupvalue (lua_State *L, int funcindex, int n) { TValue *fi; lua_lock(L); fi = index2value(L, funcindex); - api_checknelems(L, 1); + api_checkpop(L, 1); name = aux_upvalue(fi, n, &val, &owner); if (name) { L->top.p--; diff --git a/luaconf.h b/luaconf.h index ea2c0b2e..bdac085c 100644 --- a/luaconf.h +++ b/luaconf.h @@ -79,7 +79,7 @@ #if defined(LUA_USE_MACOSX) #define LUA_USE_POSIX #define LUA_USE_DLOPEN /* macOS does not need -ldl */ -#define LUA_USE_READLINE +#define LUA_USE_READLINE /* needs an extra library: -lreadline */ #endif diff --git a/lvm.c b/lvm.c index f83d47d1..986c7db8 100644 --- a/lvm.c +++ b/lvm.c @@ -925,7 +925,7 @@ void luaV_finishOp (lua_State *L) { ** Macros for arithmetic/bitwise/comparison opcodes in 'luaV_execute' ** ** All these macros are to be used exclusively inside the main -** iterpreter loop (function luaV_execute) and may access directly +** interpreter loop (function luaV_execute) and may access directly ** the local variables of that function (L, i, pc, ci, etc.). ** =================================================================== */ diff --git a/manual/2html b/manual/2html index 243f3f22..23f37ac6 100755 --- a/manual/2html +++ b/manual/2html @@ -58,7 +58,7 @@ end local function compose (f,g) assert(f and g) - return function (s) return g(f(s)) end + return function (...) return g(f(...)) end end local function concat (f, g) @@ -395,9 +395,10 @@ APIEntry = function (e) local apiicmd, ne = string.match(e, "^(.-)(.*)") --io.stderr:write(e) if not apiicmd then - return antipara(Tag.hr() .. Tag.h3(a)) .. Tag.pre(h) .. e + return antipara(Tag.hr() .. Tag.h3(a)) .. Tag.pre(h, {class="api"}) .. e else - return antipara(Tag.hr() .. Tag.h3(a)) .. apiicmd .. Tag.pre(h) .. ne + return antipara(Tag.hr() .. Tag.h3(a)) .. apiicmd .. + Tag.pre(h, {class="api"}) .. ne end end, diff --git a/manual/manual.of b/manual/manual.of index 426ad453..e2d1a651 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -1728,7 +1728,7 @@ global X , _G X = 1 -- ERROR _ENV.X = 1 -- Ok _G.print(X) -- Ok -foo() -- 'foo' can freely change any global +foo() -- 'foo' can freely change any global } A chunk is also a block @see{chunks}, @@ -6071,6 +6071,14 @@ In both cases, the function pushes onto the stack the final value associated with @id{tname} in the registry. +Usage note: Beware the use of the return value of this function to +conditionally initializes the new metatable +(e.g., by adding metamethods to it). +If the initialization raises an error, +the metatable will not be properly initialized, +but a subsequent execution of that code will detect that the +metatable already exists and then skip the initialization. + } @APIEntry{lua_State *luaL_newstate (void);| From 7579fc9d7ed90240487251dfb69168f8e64e9294 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Thu, 23 Jul 2026 13:58:30 -0300 Subject: [PATCH 41/47] Small change in scope of variables in repeat-until A close instruction is still inside the scope of the variables it is closing. The extra close in a repeat-until (to close variables before repeating the loop) was being coded outside that scope. --- lparser.c | 2 +- testes/locals.lua | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lparser.c b/lparser.c index fe29194c..3c26a4fd 100644 --- a/lparser.c +++ b/lparser.c @@ -1614,7 +1614,6 @@ static void repeatstat (LexState *ls, int line) { statlist(ls); check_match(ls, TK_UNTIL, TK_REPEAT, line); condexit = cond(ls); /* read condition (inside scope block) */ - leaveblock(fs); /* finish scope */ if (bl2.upval) { /* upvalues? */ int exit = luaK_jump(fs); /* normal exit must jump over fix */ luaK_patchtohere(fs, condexit); /* repetition must close upvalues */ @@ -1623,6 +1622,7 @@ static void repeatstat (LexState *ls, int line) { luaK_patchtohere(fs, exit); /* normal exit comes to here */ } luaK_patchlist(fs, condexit, repeat_init); /* close the loop */ + leaveblock(fs); /* finish scope */ leaveblock(fs); /* finish loop */ } diff --git a/testes/locals.lua b/testes/locals.lua index 6cd10547..e9718341 100644 --- a/testes/locals.lua +++ b/testes/locals.lua @@ -1179,6 +1179,25 @@ if rawget(_G, "T") then end +do + -- detail in scopes of variables in the loop of 'repeat-until' + local res = {} + local function foo (a) + repeat + local x + local i = setmetatable({}, {__close = function () + res[#res + 1] = debug.getlocal(2, 2) -- get 'x' + res[#res + 1] = debug.getlocal(2, 3) -- get 'i' + a = true + end}) + until a + end + foo(false) + -- loop variables still in scope when closing 'i', both when 'repeat' + -- repeats and when 'repeat' stops. + assert(res[1] == "x" and res[2] == "i" and res[3] == "x" and res[4] == "i") +end + -- to-be-closed variables in generic for loops do From c0adc5f8a5a2ab00139837291c914625df942257 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Fri, 28 Aug 2026 09:27:47 -0300 Subject: [PATCH 42/47] Macro 'luaC_condGC' should call pre/pos only once With test option HARDMEMTESTS defined, luaC_condGC was calling pre and pos twice. That made macro checkGC set the stack top before the GC step and then again before the full collection, but the previous step could invalidate the pointer used to set top. --- lgc.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lgc.h b/lgc.h index ee054179..193ac0fc 100644 --- a/lgc.h +++ b/lgc.h @@ -231,8 +231,9 @@ #endif #define luaC_condGC(L,pre,pos) \ - { if (G(L)->GCdebt <= 0) { pre; luaC_step(L); pos;}; \ - condchangemem(L,pre,pos,0); } + { if (G(L)->GCdebt <= 0) \ + { pre; luaC_step(L); condchangemem(L,{},{},0); pos;} \ + else condchangemem(L,pre,pos,0); } /* more often than not, 'pre'/'pos' are empty */ #define luaC_checkGC(L) luaC_condGC(L,(void)0,(void)0) From 35a87f8c5bdeeea285cef38002a962d2361e4807 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Sun, 13 Sep 2026 14:08:50 -0300 Subject: [PATCH 43/47] Added casts to uses of 'sig_atomic' 'sig_atomic' can be larger than 'int' on some platforms. --- ldebug.c | 2 +- lvm.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ldebug.c b/ldebug.c index 5293ccb9..61c52749 100644 --- a/ldebug.c +++ b/ldebug.c @@ -150,7 +150,7 @@ LUA_API lua_Hook lua_gethook (lua_State *L) { LUA_API int lua_gethookmask (lua_State *L) { - return L->hookmask; + return cast_int(L->hookmask); } diff --git a/lvm.c b/lvm.c index 986c7db8..46170534 100644 --- a/lvm.c +++ b/lvm.c @@ -1117,7 +1117,7 @@ void luaV_finishOp (lua_State *L) { -#define updatetrap(ci) (trap = ci->u.l.trap) +#define updatetrap(ci) (trap = cast_int(ci->u.l.trap)) #define updatebase(ci) (base = ci->func.p + 1) @@ -1211,7 +1211,7 @@ void luaV_execute (lua_State *L, CallInfo *ci) { #include "ljumptab.h" #endif startfunc: - trap = L->hookmask; + trap = cast_int(L->hookmask); returning: /* trap already set */ cl = ci_func(ci); k = cl->p->k; From 4c32b2dfd204add9a3fed8d5eb100236e276e44a Mon Sep 17 00:00:00 2001 From: Roberto I Date: Tue, 15 Sep 2026 09:49:44 -0300 Subject: [PATCH 44/47] 'mktime' can return -1 --- loslib.c | 25 +++++++++++++++++-------- testes/files.lua | 7 +++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/loslib.c b/loslib.c index b7a2b0d1..fe9295c7 100644 --- a/loslib.c +++ b/loslib.c @@ -345,11 +345,22 @@ static int os_date (lua_State *L) { } +static int os_time_aux (lua_State *L, time_t t, int err) { + if (err || t != (time_t)(l_timet)t) + return luaL_error(L, + "time result cannot be represented in this installation"); + l_pushtime(L, t); + return 1; +} + + static int os_time (lua_State *L) { - time_t t; - if (lua_isnoneornil(L, 1)) /* called without args? */ - t = time(NULL); /* get current time */ + if (lua_isnoneornil(L, 1)) { /* called without args? */ + time_t t = time(NULL); /* get current time; error if it is -1 */ + return os_time_aux(L, t, (t == (time_t)(-1))); + } else { + time_t t; struct tm ts; luaL_checktype(L, 1, LUA_TTABLE); lua_settop(L, 1); /* make sure table is at the top */ @@ -360,14 +371,12 @@ static int os_time (lua_State *L) { ts.tm_min = getfield(L, "min", 0, 0); ts.tm_sec = getfield(L, "sec", 0, 0); ts.tm_isdst = getboolfield(L, "isdst"); + ts.tm_wday = -1; /* if call succeeds, it will set this value */ t = mktime(&ts); setallfields(L, &ts); /* update fields with normalized values */ + /* error if tm_wday was not set */ + return os_time_aux(L, t, ts.tm_wday == -1); } - if (t != (time_t)(l_timet)t || t == (time_t)(-1)) - return luaL_error(L, - "time result cannot be represented in this installation"); - l_pushtime(L, t); - return 1; } diff --git a/testes/files.lua b/testes/files.lua index 7146ac7c..04936565 100644 --- a/testes/files.lua +++ b/testes/files.lua @@ -854,6 +854,13 @@ local function checkDateTable (t) _G.D = nil end + +do -- testing mktime returning -1 + local t = os.date("*t", -1) + assert(os.time(t) == -1) +end + + checkDateTable(os.time()) if not _port then -- assume that time_t can represent these values From ceb32fb5bc29e3fe0009d54139081efbdf125cd4 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Tue, 15 Sep 2026 10:03:36 -0300 Subject: [PATCH 45/47] Another try for silencing MSVS warning C4334 See also commit 8fac494. --- lobject.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lobject.h b/lobject.h index bf5b65b4..82bd9703 100644 --- a/lobject.h +++ b/lobject.h @@ -827,7 +827,7 @@ typedef struct Table { #define twoto(x) (1u<<(x)) -#define sizenode(t) (twoto((t)->lsizenode)) +#define sizenode(t) cast_uint(twoto((t)->lsizenode)) /* size of buffer for 'luaO_utf8esc' function */ From 973777c74efe207ddddc6677b507007dea05dfdd Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 16 Sep 2026 11:49:35 -0300 Subject: [PATCH 46/47] Details --- lmathlib.c | 44 ++++++++++++++++++++++---------------------- loslib.c | 20 ++++++++++---------- manual/manual.of | 4 ++-- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lmathlib.c b/lmathlib.c index a6b13f96..87e7462a 100644 --- a/lmathlib.c +++ b/lmathlib.c @@ -704,35 +704,35 @@ static int math_log10 (lua_State *L) { static const luaL_Reg mathlib[] = { - {"abs", math_abs}, - {"acos", math_acos}, - {"asin", math_asin}, - {"atan", math_atan}, - {"ceil", math_ceil}, - {"cos", math_cos}, - {"deg", math_deg}, - {"exp", math_exp}, + {"abs", math_abs}, + {"acos", math_acos}, + {"asin", math_asin}, + {"atan", math_atan}, + {"ceil", math_ceil}, + {"cos", math_cos}, + {"deg", math_deg}, + {"exp", math_exp}, {"tointeger", math_toint}, {"floor", math_floor}, - {"fmod", math_fmod}, + {"fmod", math_fmod}, {"frexp", math_frexp}, - {"ult", math_ult}, + {"ult", math_ult}, {"ldexp", math_ldexp}, - {"log", math_log}, - {"max", math_max}, - {"min", math_min}, - {"modf", math_modf}, - {"rad", math_rad}, - {"sin", math_sin}, - {"sqrt", math_sqrt}, - {"tan", math_tan}, + {"log", math_log}, + {"max", math_max}, + {"min", math_min}, + {"modf", math_modf}, + {"rad", math_rad}, + {"sin", math_sin}, + {"sqrt", math_sqrt}, + {"tan", math_tan}, {"type", math_type}, #if defined(LUA_COMPAT_MATHLIB) {"atan2", math_atan}, - {"cosh", math_cosh}, - {"sinh", math_sinh}, - {"tanh", math_tanh}, - {"pow", math_pow}, + {"cosh", math_cosh}, + {"sinh", math_sinh}, + {"tanh", math_tanh}, + {"pow", math_pow}, {"log10", math_log10}, #endif /* placeholders */ diff --git a/loslib.c b/loslib.c index fe9295c7..111ac2f8 100644 --- a/loslib.c +++ b/loslib.c @@ -416,17 +416,17 @@ static int os_exit (lua_State *L) { static const luaL_Reg syslib[] = { - {"clock", os_clock}, - {"date", os_date}, - {"difftime", os_difftime}, - {"execute", os_execute}, - {"exit", os_exit}, - {"getenv", os_getenv}, - {"remove", os_remove}, - {"rename", os_rename}, + {"clock", os_clock}, + {"date", os_date}, + {"difftime", os_difftime}, + {"execute", os_execute}, + {"exit", os_exit}, + {"getenv", os_getenv}, + {"remove", os_remove}, + {"rename", os_rename}, {"setlocale", os_setlocale}, - {"time", os_time}, - {"tmpname", os_tmpname}, + {"time", os_time}, + {"tmpname", os_tmpname}, {NULL, NULL} }; diff --git a/manual/manual.of b/manual/manual.of index e2d1a651..e3c7cd25 100644 --- a/manual/manual.of +++ b/manual/manual.of @@ -6071,8 +6071,8 @@ In both cases, the function pushes onto the stack the final value associated with @id{tname} in the registry. -Usage note: Beware the use of the return value of this function to -conditionally initializes the new metatable +Usage note: Beware the use of the return value of this function +to conditionally initialize the new metatable (e.g., by adding metamethods to it). If the initialization raises an error, the metatable will not be properly initialized, From 0b29f408433e92953cc72b1d3e06c7ac8139e439 Mon Sep 17 00:00:00 2001 From: Roberto I Date: Wed, 16 Sep 2026 14:18:11 -0300 Subject: [PATCH 47/47] Bug: UB when applying GC parameter When computing whether Lua should return from gen-major to gen-minor, the difference between the total memory and the previous total memory can be negative, which can result in that negative value being left-shifted (UB). --- lgc.c | 3 ++- lobject.c | 1 + testes/gengc.lua | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/lgc.c b/lgc.c index a463e41e..cf4160c1 100644 --- a/lgc.c +++ b/lgc.c @@ -1472,7 +1472,8 @@ static int checkmajorminor (lua_State *L, global_State *g) { if (g->gckind == KGC_GENMAJOR) { /* generational mode? */ l_mem numbytes = gettotalbytes(g); l_mem addedbytes = numbytes - g->GCmajorminor; - l_mem limit = applygcparam(g, MAJORMINOR, addedbytes); + l_mem limit = (addedbytes < 0) ? 0 + : applygcparam(g, MAJORMINOR, addedbytes); l_mem tobecollected = numbytes - g->GCmarked; if (tobecollected > limit) { atomic2gen(L, g); /* return to generational mode */ diff --git a/lobject.c b/lobject.c index 763b4846..6997cfd0 100644 --- a/lobject.c +++ b/lobject.c @@ -89,6 +89,7 @@ lu_byte luaO_codeparam (unsigned int p) { l_mem luaO_applyparam (lu_byte p, l_mem x) { int m = p & 0xF; /* mantissa */ int e = (p >> 4); /* exponent */ + lua_assert(x >= 0); if (e > 0) { /* normalized? */ e--; /* correct exponent */ m += 0x10; /* correct mantissa; maximum value is 0x1F */ diff --git a/testes/gengc.lua b/testes/gengc.lua index 6509e39d..84c1a8ee 100644 --- a/testes/gengc.lua +++ b/testes/gengc.lua @@ -162,6 +162,51 @@ end assert(collectgarbage'isrunning') +do + -- bug in 5.0: when computing whether it should return from gen-major + -- to gen-minor, the difference between the total memory and the + -- previous total memory can be negative, which results in that + -- negative value being left-shifted (UB) + + local lim = 1e6 + + -- make major collections non-incremental + local oldsm = collectgarbage("param", "stepmul", 0) + + -- make "majorminor" large enough to force a left-shift + -- when applying the parameter (internal details) + local oldmm = collectgarbage("param", "majorminor", 2000) + + collectgarbage(); collectgarbage() + assert(not T or T.gcquery() == "genminor") + + local M = collectgarbage"count" * 1024 + + -- create a large table + local t = {} + for i = 1, lim do t[i] = true end + assert(collectgarbage"count" * 1024 > M + lim * string.packsize"j") + + -- force collector to "generational major" mode, doing several + -- minor collections that recover no memory + collectgarbage"step"; collectgarbage"step"; collectgarbage"step" + assert(not T or T.gcquery() == "genmajor") + + -- shrink the table + for i = 1, lim do t[i] = nil end + t[2 * lim] = true + assert(collectgarbage"count" < M * 5/4) + + -- bug was here, an assert violation when checking whether to + -- return to 'genminor' + collectgarbage"step" + + -- restore previous parameters + collectgarbage("param", "stepmul", oldsm) + collectgarbage("param", "majorminor", oldmm) +end + + do print"testing stop-the-world collection" local step = collectgarbage("param", "stepsize", 0); collectgarbage("incremental")