]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
Fix UCL object memory leak in Lua integration
authorVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 28 Oct 2025 10:54:39 +0000 (10:54 +0000)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 28 Oct 2025 10:54:39 +0000 (10:54 +0000)
When UCL objects are passed to Lua via ucl_object_push_lua_unwrapped(),
the reference count is incremented but the garbage collector finalizer
was not being called, causing memory leaks.

The issue is that Lua 5.1/LuaJIT does not support __gc metamethod for
tables, only for userdata. This fix adds proper garbage collection
support for both Lua versions:

- For Lua 5.1/LuaJIT: Add __gc metamethod to the userdata stored in
  the table at index [0], which properly triggers reference cleanup
- For Lua 5.2+: Use the existing table __gc metamethod which works
  correctly in newer Lua versions

This ensures that ucl_object_unref() is called exactly once when the
Lua wrapper is garbage collected, preventing memory leaks.

contrib/libucl/lua_ucl.c

index 13306b9425f24dfa18a6bebb87d9a23b2c8e3b31..8180f825e1f8f24802ae06b6eee39d02d9a7277a 100644 (file)
@@ -744,6 +744,29 @@ lua_ucl_object_get(lua_State *L, int index)
        return NULL;
 }
 
+#if LUA_VERSION_NUM < 502
+/**
+ * __gc metamethod for userdata that holds UCL object pointer.
+ * This is necessary because Lua 5.1/LuaJIT does not support __gc for tables,
+ * only for userdata. The userdata is stored at index [0] in the table created
+ * by ucl_object_push_lua_unwrapped.
+ */
+static int
+lua_ucl_userdata_gc(lua_State *L)
+{
+       ucl_object_t **pobj;
+
+       if (lua_isuserdata(L, 1)) {
+               pobj = (ucl_object_t **) lua_touserdata(L, 1);
+               if (pobj && *pobj) {
+                       ucl_object_unref(*pobj);
+               }
+       }
+
+       return 0;
+}
+#endif
+
 void ucl_object_push_lua_unwrapped(lua_State *L, const ucl_object_t *obj)
 {
        ucl_object_t **pobj;
@@ -755,6 +778,17 @@ void ucl_object_push_lua_unwrapped(lua_State *L, const ucl_object_t *obj)
        lua_createtable(L, 1, 9);
        pobj = lua_newuserdata(L, sizeof(*pobj));
        *pobj = ucl_object_ref(obj);
+
+#if LUA_VERSION_NUM < 502
+       /* For Lua 5.1/LuaJIT: Set a metatable on the userdata itself to ensure __gc is called.
+        * Lua 5.1 does not support __gc for tables, only for userdata.
+        * For Lua 5.2+, we use table __gc instead (see lua_ucl_object_mt). */
+       lua_newtable(L);
+       lua_pushcfunction(L, lua_ucl_userdata_gc);
+       lua_setfield(L, -2, "__gc");
+       lua_setmetatable(L, -2);
+#endif
+
        lua_rawseti(L, -2, 0);
 
        lua_pushcfunction(L, lua_ucl_index);
@@ -1340,6 +1374,7 @@ lua_ucl_object_validate(lua_State *L)
        return 2;
 }
 
+#if LUA_VERSION_NUM >= 502
 static int
 lua_ucl_object_gc(lua_State *L)
 {
@@ -1347,10 +1382,13 @@ lua_ucl_object_gc(lua_State *L)
 
        obj = lua_ucl_object_get(L, 1);
 
-       ucl_object_unref(obj);
+       if (obj) {
+               ucl_object_unref(obj);
+       }
 
        return 0;
 }
+#endif
 
 static int
 lua_ucl_iter_gc(lua_State *L)
@@ -1746,8 +1784,12 @@ lua_ucl_object_mt(lua_State *L)
        lua_pushcfunction(L, lua_ucl_len);
        lua_setfield(L, -2, "__len");
 
+#if LUA_VERSION_NUM >= 502
+       /* Table __gc is only supported in Lua 5.2+.
+        * For Lua 5.1/LuaJIT, we use userdata __gc in ucl_object_push_lua_unwrapped */
        lua_pushcfunction(L, lua_ucl_object_gc);
        lua_setfield(L, -2, "__gc");
+#endif
        lua_pushcfunction(L, lua_ucl_object_tostring);
        lua_setfield(L, -2, "__tostring");