[Git][xorg-team/lib/libinput][upstream-unstable] 3 commits: lua: separate the API from the metatables
"Timo Aaltonen (@tjaalton)" <[email protected]>
| Newsgroups | gmane.linux.debian.devel.x |
|---|---|
| Message-ID | <[email protected]> |
Timo Aaltonen pushed to branch upstream-unstable at X Strike Force / lib / libinput Commits: 45506c7b by Peter Hutterer at 2026-04-02T10:17:52+10:00 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 <[email protected]> (cherry picked from commit 45dfd0f0301af855f068df27b2e40cc9f5713acd) Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1461> - - - - - 49f9a815 by Peter Hutterer at 2026-04-02T10:18:00+10:00 lua: force text mode for loading plugins luaL_loadfile() by default allows for both text files and precompiled lua files. Precompiled files are not verified on load allowing for a sandbox escape. CVE-2026-35093 Fixes: #1271 Found-by: Koen Tange <[email protected]> (cherry picked from commit 356c498fd4ba25ec99f6866fc96847ec3d1f16bf) Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1461> - - - - - 19206869 by Peter Hutterer at 2026-04-02T11:04:12+10:00 libinput 1.31.1 Signed-off-by: Peter Hutterer <[email protected]> - - - - - 3 changed files: - meson.build - src/libinput-plugin-lua.c - test/test-plugins-lua.c Changes: ===================================== meson.build ===================================== @@ -1,5 +1,5 @@ project('libinput', 'c', - version : '1.31.0', + version : '1.31.1', license : 'MIT/Expat', default_options : [ 'c_std=gnu99', 'warning_level=2' ], meson_version : '>= 0.64.0') ===================================== 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 @@ -1336,7 +1362,7 @@ libinput_lua_plugin_new_from_path(struct libinput *libinput, const char *path) return NULL; } - int ret = luaL_loadfile(L, path); + int ret = luaL_loadfilex(L, path, "t"); if (ret == LUA_OK) { plugin->L = steal(&L); ===================================== 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); @@ -1172,10 +1204,79 @@ START_TEST(lua_remove_plugin_on_timeout) } END_TEST +/* Pre-compiled Lua 5.4 bytecode for the following source: + * + * libinput:register({1}) + * libinput:connect("new-evdev-device", function(device) + * libinput:log_info("loaded from binary lua file") + * end) + * + * To regenerate: + * luac5.4 -o /dev/stdout /tmp/plugin.lua | xxd -i + */ +static const unsigned char binary_lua_plugin[] = { + 0x1b, 0x4c, 0x75, 0x61, 0x54, 0x00, 0x19, 0x93, 0x0d, 0x0a, 0x1a, 0x0a, 0x04, + 0x08, 0x08, 0x78, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x28, 0x77, 0x40, 0x01, 0x9b, 0x40, 0x74, 0x65, 0x73, 0x74, 0x2f, + 0x31, 0x30, 0x2d, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x2d, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x2e, 0x6c, 0x75, 0x61, 0x80, 0x80, 0x00, 0x01, 0x04, 0x8e, + 0x51, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x14, 0x80, 0x00, 0x01, 0x13, + 0x01, 0x00, 0x01, 0x52, 0x00, 0x00, 0x00, 0x81, 0x01, 0x00, 0x80, 0x4e, 0x01, + 0x01, 0x00, 0x44, 0x00, 0x03, 0x01, 0x0b, 0x00, 0x00, 0x00, 0x14, 0x80, 0x00, + 0x02, 0x03, 0x81, 0x01, 0x00, 0xcf, 0x01, 0x00, 0x00, 0x44, 0x00, 0x04, 0x01, + 0x46, 0x00, 0x01, 0x01, 0x84, 0x04, 0x89, 0x6c, 0x69, 0x62, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x04, 0x89, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x04, + 0x88, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x04, 0x91, 0x6e, 0x65, 0x77, + 0x2d, 0x65, 0x76, 0x64, 0x65, 0x76, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x81, 0x01, 0x00, 0x00, 0x81, 0x80, 0x8d, 0x8f, 0x01, 0x00, 0x04, 0x85, 0x8b, + 0x00, 0x00, 0x00, 0x94, 0x80, 0x01, 0x01, 0x83, 0x01, 0x01, 0x00, 0xc4, 0x00, + 0x03, 0x01, 0xc7, 0x00, 0x01, 0x00, 0x83, 0x04, 0x89, 0x6c, 0x69, 0x62, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x04, 0x89, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x6e, 0x66, + 0x6f, 0x04, 0x9c, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x20, 0x66, 0x72, 0x6f, + 0x6d, 0x20, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x20, 0x6c, 0x75, 0x61, 0x20, + 0x66, 0x69, 0x6c, 0x65, 0x81, 0x00, 0x00, 0x00, 0x80, 0x85, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x80, 0x81, 0x87, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x80, 0x85, + 0x81, 0x85, 0x5f, 0x45, 0x4e, 0x56, 0x8e, 0x01, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xfe, 0x02, 0x80, 0x80, 0x81, 0x85, 0x5f, + 0x45, 0x4e, 0x56, +}; + +START_TEST(lua_reject_precompiled_files) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + + /* Write the binary bytecode to a .lua file in the tmpdir. + * Binary (pre-compiled) Lua files must be rejected by the + * plugin loader for security reasons. */ + _autofree_ char *path = strdup_printf("%s/10-binary-plugin.lua", tmpdir->path); + _autoclose_ int fd = open(path, O_WRONLY | O_CREAT, 0644); + litest_assert_errno_success(fd); + + ssize_t written = write(fd, binary_lua_plugin, sizeof(binary_lua_plugin)); + litest_assert_int_eq((int)written, (int)sizeof(binary_lua_plugin)); + fsync(fd); + + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, + LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE); + litest_drain_events(li); + + size_t index = 0; + litest_assert( + strv_find_substring(capture->errors, "Failed to load", &index)); + litest_assert_str_in(path, capture->errors[index]); + } +} +END_TEST + TEST_COLLECTION(lua) { /* clang-format off */ litest_add_no_device(lua_load_failure); + litest_add_no_device(lua_reject_precompiled_files); litest_with_parameters(params, "content", 'I', 6, litest_named_i32(EMPTY), @@ -1219,6 +1320,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); View it on GitLab: https://salsa.debian.org/xorg-team/lib/libinput/-/compare/659967488e1e66d7fb7210c6b86860c8e1e5bed4...1920686963fe224f7d2f4fe195cbed651e11d455 -- View it on GitLab: https://salsa.debian.org/xorg-team/lib/libinput/-/compare/659967488e1e66d7fb7210c6b86860c8e1e5bed4...1920686963fe224f7d2f4fe195cbed651e11d455 You're receiving this email because of your account on salsa.debian.org.