From 45dfd0f0301af855f068df27b2e40cc9f5713acd Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 30 Mar 2026 09:41:08 +1000 Subject: [PATCH] lua: separate the API from the metatables Previously we had one vtable for the libinputplugin and EvdevDevice objects. This allowed plugins to call __gc(), a decidedly internal method. This fixes a use-after-free: A plugin that called EvdevDevice::__gc() frees the plugin's copy of device->name but leaves the pointer in-place, a subsequent call will thus cause a UAF read. Fix this by separating what is the object's metatable from the public methods that are accessible to a plugin. CVE-2026-35094 Fixes: #1272 Found-by: Koen Tange Part-of: --- src/libinput-plugin-lua.c | 54 +++++++++++++++++++++++++++++---------- test/test-plugins-lua.c | 33 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/libinput-plugin-lua.c b/src/libinput-plugin-lua.c index ac828f7f..c6f99a5f 100644 --- a/src/libinput-plugin-lua.c +++ b/src/libinput-plugin-lua.c @@ -562,6 +562,12 @@ libinputplugin_unregister(lua_State *L) return luaL_error(L, "@@unregistering@@"); } +static int +readonly_newindex(lua_State *L) +{ + return luaL_error(L, "attempt to modify a read-only table"); +} + static int libinputplugin_gc(lua_State *L) { @@ -673,7 +679,28 @@ libinputplugin_log_error(lua_State *L) return libinputplugin_log(L, LIBINPUT_LOG_PRIORITY_ERROR); } -static const struct luaL_Reg libinputplugin_vtable[] = { +static void +setup_vfuncs(lua_State *L, + const char *metatable_name, + const struct luaL_Reg *vfuncs, + const struct luaL_Reg *public_methods) +{ + luaL_newmetatable(L, metatable_name); + luaL_setfuncs(L, vfuncs, 0); + + lua_newtable(L); + luaL_setfuncs(L, public_methods, 0); + lua_setfield(L, -2, "__index"); + + /* set metatable.__metatable = false to prevent a script from getmetatable(), + which is blocked anyway but safe and sorry and whatnot */ + lua_pushboolean(L, 0); + lua_setfield(L, -2, "__metatable"); + + lua_pop(L, 1); +} + +static const struct luaL_Reg libinputplugin_methods[] = { { "now", libinputplugin_now }, { "version", libinputplugin_version }, { "connect", libinputplugin_connect }, @@ -685,18 +712,18 @@ static const struct luaL_Reg libinputplugin_vtable[] = { { "log_debug", libinputplugin_log_debug }, { "log_info", libinputplugin_log_info }, { "log_error", libinputplugin_log_error }, - { "__gc", libinputplugin_gc }, { NULL, NULL } }; +static const struct luaL_Reg libinputplugin_meta[] = { { "__gc", libinputplugin_gc }, + { "__newindex", + readonly_newindex }, + { NULL, NULL } }; + static void libinputplugin_init(lua_State *L) { - luaL_newmetatable(L, PLUGIN_METATABLE); - lua_pushstring(L, "__index"); - lua_pushvalue(L, -2); /* push metatable */ - lua_settable(L, -3); /* metatable.__index = metatable */ - luaL_setfuncs(L, libinputplugin_vtable, 0); + setup_vfuncs(L, PLUGIN_METATABLE, libinputplugin_meta, libinputplugin_methods); } static int @@ -1073,7 +1100,7 @@ evdevdevice_gc(lua_State *L) return 0; } -static const struct luaL_Reg evdevdevice_vtable[] = { +static const struct luaL_Reg evdevdevice_methods[] = { { "info", evdevdevice_info }, { "name", evdevdevice_name }, { "usages", evdevdevice_usages }, @@ -1087,18 +1114,17 @@ static const struct luaL_Reg evdevdevice_vtable[] = { { "prepend_frame", evdevdevice_prepend_frame }, { "append_frame", evdevdevice_append_frame }, { "disable_feature", evdevdevice_disable_feature }, - { "__gc", evdevdevice_gc }, { NULL, NULL } }; +static const struct luaL_Reg evdevdevice_meta[] = { { "__gc", evdevdevice_gc }, + { "__newindex", readonly_newindex }, + { NULL, NULL } }; + static void evdevdevice_init(lua_State *L) { - luaL_newmetatable(L, EVDEV_DEVICE_METATABLE); - lua_pushstring(L, "__index"); - lua_pushvalue(L, -2); /* push metatable */ - lua_settable(L, -3); /* metatable.__index = metatable */ - luaL_setfuncs(L, evdevdevice_vtable, 0); + setup_vfuncs(L, EVDEV_DEVICE_METATABLE, evdevdevice_meta, evdevdevice_methods); } static void diff --git a/test/test-plugins-lua.c b/test/test-plugins-lua.c index 4d687203..aba8f6c6 100644 --- a/test/test-plugins-lua.c +++ b/test/test-plugins-lua.c @@ -526,6 +526,38 @@ START_TEST(lua_disallowed_functions) } END_TEST +START_TEST(lua_gc_not_accessible) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + const char *lua = + "libinput:register({1})\n" + "assert(libinput.__gc == nil)\n" + "function check_device_gc(device)\n" + " assert(device.__gc == nil)\n" + " libinput:log_info(\"gc_not_accessible: ok\")\n" + "end\n" + "libinput:connect(\"new-evdev-device\", check_device_gc)\n"; + + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_INFO) + libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, + LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE); + litest_drain_events(li); + + _destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE); + litest_drain_events(li); + + litest_assert_logcapture_no_errors(capture); + litest_assert_strv_substring(capture->infos, "gc_not_accessible: ok"); + } +} +END_TEST + START_TEST(lua_frame_handler) { _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); @@ -1219,6 +1251,7 @@ TEST_COLLECTION(lua) litest_add_no_device(lua_register_multiversions); litest_add_no_device(lua_allowed_functions); litest_add_no_device(lua_disallowed_functions); + litest_add_no_device(lua_gc_not_accessible); litest_add_no_device(lua_frame_handler); litest_add_no_device(lua_device_info);