From 356c498fd4ba25ec99f6866fc96847ec3d1f16bf Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 30 Mar 2026 11:35:35 +1000 Subject: [PATCH] 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 Part-of: --- src/libinput-plugin-lua.c | 2 +- test/test-plugins-lua.c | 69 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/libinput-plugin-lua.c b/src/libinput-plugin-lua.c index c6f99a5f..2a09a64a 100644 --- a/src/libinput-plugin-lua.c +++ b/src/libinput-plugin-lua.c @@ -1362,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); diff --git a/test/test-plugins-lua.c b/test/test-plugins-lua.c index aba8f6c6..e61806d3 100644 --- a/test/test-plugins-lua.c +++ b/test/test-plugins-lua.c @@ -1204,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),