[Tarantool-patches] [PATCH luajit 2/3] Restore state when recording __concat metamethod throws OOM.

Sergey Kaplun skaplun at tarantool.org
Mon Mar 10 17:51:36 MSK 2025


From: Mike Pall <mike>

Reported by Sergey Kaplun.

(cherry picked from commit 19878ec05c239ccaf5f3d17af27670a963e25b8b)

Before the patch, the Lua stack isn't restored if the OOM related to
string allocation happens during fold optimization inside `rec_cat()`.

This patch moves most of the `rec_cat()` logic to the protected
`rec_mm_concat_cp()` to be able to restore the stack in case of OOM.

Unfortunately, after this patch, the test
<lj-839-concat-recording.test.lua> fails since the incorrect adjusting
of the topslot. It will be fixed in the next patch.

Sergey Kaplun:
* added the description and the test for the problem

Part of tarantool/tarantool#11055
---
 src/lj_record.c                               | 51 +++++++++++-------
 .../lj-1298-oom-on-concat-recording.test.lua  | 53 +++++++++++++++++++
 test/tarantool-tests/utils/CMakeLists.txt     |  1 +
 test/tarantool-tests/utils/allocinject.c      | 21 ++++++++
 4 files changed, 108 insertions(+), 18 deletions(-)
 create mode 100644 test/tarantool-tests/lj-1298-oom-on-concat-recording.test.lua

diff --git a/src/lj_record.c b/src/lj_record.c
index 5345fa63..7a481a51 100644
--- a/src/lj_record.c
+++ b/src/lj_record.c
@@ -1943,25 +1943,19 @@ static TRef rec_tnew(jit_State *J, uint32_t ah)
 
 typedef struct RecCatDataCP {
   jit_State *J;
-  RecordIndex *ix;
+  BCReg baseslot, topslot;
+  TRef tr;
 } RecCatDataCP;
 
 static TValue *rec_mm_concat_cp(lua_State *L, lua_CFunction dummy, void *ud)
 {
   RecCatDataCP *rcd = (RecCatDataCP *)ud;
-  UNUSED(L); UNUSED(dummy);
-  rec_mm_arith(rcd->J, rcd->ix, MM_concat);  /* Call __concat metamethod. */
-  return NULL;
-}
-
-static TRef rec_cat(jit_State *J, BCReg baseslot, BCReg topslot)
-{
+  jit_State *J = rcd->J;
+  BCReg baseslot = rcd->baseslot, topslot = rcd->topslot;
   TRef *top = &J->base[topslot];
-  TValue savetv[5+LJ_FR2];
   BCReg s;
   RecordIndex ix;
-  RecCatDataCP rcd;
-  int errcode;
+  UNUSED(L); UNUSED(dummy);
   lj_assertJ(baseslot < topslot, "bad CAT arg");
   for (s = baseslot; s <= topslot; s++)
     (void)getslot(J, s);  /* Ensure all arguments have a reference. */
@@ -1983,7 +1977,10 @@ static TRef rec_cat(jit_State *J, BCReg baseslot, BCReg topslot)
     } while (trp <= top);
     tr = emitir(IRT(IR_BUFSTR, IRT_STR), tr, hdr);
     J->maxslot = (BCReg)(xbase - J->base);
-    if (xbase == base) return tr;  /* Return simple concatenation result. */
+    if (xbase == base) {
+      rcd->tr = tr;  /* Return simple concatenation result. */
+      return NULL;
+    }
     /* Pass partial result. */
     topslot = J->maxslot--;
     *xbase = tr;
@@ -1996,13 +1993,31 @@ static TRef rec_cat(jit_State *J, BCReg baseslot, BCReg topslot)
   copyTV(J->L, &ix.tabv, &J->L->base[topslot-1]);
   ix.tab = top[-1];
   ix.key = top[0];
-  memcpy(savetv, &J->L->base[topslot-1], sizeof(savetv));  /* Save slots. */
+  rec_mm_arith(J, &ix, MM_concat);  /* Call __concat metamethod. */
+  rcd->tr = 0;  /* No result yet. */
+  return NULL;
+}
+
+static TRef rec_cat(jit_State *J, BCReg baseslot, BCReg topslot)
+{
+  lua_State *L = J->L;
+  ptrdiff_t delta = L->top - L->base;
+  TValue savetv[5+LJ_FR2], errobj;
+  RecCatDataCP rcd;
+  int errcode;
   rcd.J = J;
-  rcd.ix = &ix;
-  errcode = lj_vm_cpcall(J->L, NULL, &rcd, rec_mm_concat_cp);
-  memcpy(&J->L->base[topslot-1], savetv, sizeof(savetv));  /* Restore slots. */
-  if (errcode) return (TRef)(-errcode);
-  return 0;  /* No result yet. */
+  rcd.baseslot = baseslot;
+  rcd.topslot = topslot;
+  memcpy(savetv, &L->base[topslot-1], sizeof(savetv));  /* Save slots. */
+  errcode = lj_vm_cpcall(L, NULL, &rcd, rec_mm_concat_cp);
+  if (errcode) copyTV(L, &errobj, L->top-1);
+  memcpy(&L->base[topslot-1], savetv, sizeof(savetv));  /* Restore slots. */
+  if (errcode) {
+    L->top = L->base + delta;
+    copyTV(L, L->top++, &errobj);
+    return (TRef)(-errcode);
+  }
+  return rcd.tr;
 }
 
 /* -- Record bytecode ops ------------------------------------------------- */
diff --git a/test/tarantool-tests/lj-1298-oom-on-concat-recording.test.lua b/test/tarantool-tests/lj-1298-oom-on-concat-recording.test.lua
new file mode 100644
index 00000000..961df798
--- /dev/null
+++ b/test/tarantool-tests/lj-1298-oom-on-concat-recording.test.lua
@@ -0,0 +1,53 @@
+local tap = require('tap')
+
+-- Test file to demonstrate unbalanced Lua stack after instruction
+-- recording due to throwing an OOM error at the moment of
+-- recording without restoring the Lua stack back.
+-- See also: https://github.com/LuaJIT/LuaJIT/issues/1298.
+
+local test = tap.test('lj-1298-oom-on-concat-recording'):skipcond({
+  ['Test requires JIT enabled'] = not jit.status(),
+})
+
+local allocinject = require('allocinject')
+
+test:plan(2)
+
+jit.opt.start('hotloop=1')
+
+-- Allocation limit to return `NULL`.
+local ALLOC_LIMIT = 2048
+
+local bigstr = string.rep('x', ALLOC_LIMIT)
+local __concat = function()
+  return 'concated'
+end
+
+-- Need to use metamethod call in the concat recording.
+-- We may use any object with a metamethod, but let's use a table
+-- as the most common one.
+local concatable_t = setmetatable({}, {
+  __concat = __concat,
+})
+
+local function concat_with_err()
+  local counter = 0
+  local _
+  while counter < 1 do
+    counter = counter + 1
+    _ = bigstr .. concatable_t .. ''
+  end
+  assert(false, 'unreachable, should raise an error before')
+end
+
+-- Returns `NULL` on any allocation beyond the given limit.
+allocinject.enable_null_limited_alloc(ALLOC_LIMIT)
+
+local status, errmsg = pcall(concat_with_err)
+
+allocinject.disable()
+
+test:ok(not status, 'no assertion failure during recording, correct status')
+test:like(errmsg, 'not enough memory', 'correct error message')
+
+test:done(true)
diff --git a/test/tarantool-tests/utils/CMakeLists.txt b/test/tarantool-tests/utils/CMakeLists.txt
index 624cc933..15871934 100644
--- a/test/tarantool-tests/utils/CMakeLists.txt
+++ b/test/tarantool-tests/utils/CMakeLists.txt
@@ -2,5 +2,6 @@ list(APPEND tests
   lj-1166-error-stitch-oom-ir-buff.test.lua
   lj-1166-error-stitch-oom-snap-buff.test.lua
   lj-1247-fin-tab-rehashing-on-trace.test.lua
+  lj-1298-oom-on-concat-recording.test.lua
 )
 BuildTestCLib(allocinject allocinject.c "${tests}")
diff --git a/test/tarantool-tests/utils/allocinject.c b/test/tarantool-tests/utils/allocinject.c
index 21e1d611..7ac60625 100644
--- a/test/tarantool-tests/utils/allocinject.c
+++ b/test/tarantool-tests/utils/allocinject.c
@@ -34,6 +34,19 @@ static void *allocf_inj_null_doubling_realloc(void *ud, void *ptr, size_t osize,
 	return old_allocf(ud, ptr, osize, nsize);
 }
 
+static size_t limit = 0;
+/* Returns `NULL` on allocations beyond the given limit. */
+static void *allocf_inj_null_limited_alloc(void *ud, void *ptr, size_t osize,
+					   size_t nsize)
+{
+	assert(old_allocf != NULL);
+	assert(limit != 0);
+	/* Check the specific allocation. */
+	if (osize == 0 && nsize > limit)
+		return NULL;
+	return old_allocf(ud, ptr, osize, nsize);
+}
+
 static int enable(lua_State *L, lua_Alloc allocf_with_injection)
 {
 	assert(old_allocf == NULL);
@@ -52,6 +65,13 @@ static int enable_null_doubling_realloc(lua_State *L)
 	return enable(L, allocf_inj_null_doubling_realloc);
 }
 
+static int enable_null_limited_alloc(lua_State *L)
+{
+	limit = lua_tointeger(L, 1);
+	assert(limit != 0);
+	return enable(L, allocf_inj_null_limited_alloc);
+}
+
 /* Restore the default allocator function. */
 static int disable(lua_State *L)
 {
@@ -65,6 +85,7 @@ static int disable(lua_State *L)
 static const struct luaL_Reg allocinject[] = {
 	{"enable_null_alloc", enable_null_alloc},
 	{"enable_null_doubling_realloc", enable_null_doubling_realloc},
+	{"enable_null_limited_alloc", enable_null_limited_alloc},
 	{"disable", disable},
 	{NULL, NULL}
 };
-- 
2.48.1



More information about the Tarantool-patches mailing list