[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.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.