mirror of
https://gitlab.freedesktop.org/libinput/libinput.git
synced 2026-02-04 00:00:28 +01:00
lua: install a timeout hook before any pcalls to prevent infinite loops
If a lua pcall takes longer than one second, kill the plugin. How often to call the timeout handler is a trade-off but we err on the side of "possibly too high" since the overwhelmingly vast majority of plugins will never trigger it anyway. Gemini suggests that Lua 5.4 can do ~500k ops per second for string concat (the slowest listed), Claude suggests 1 to 10 million ops per second. The test in this patch on my 4y old cheap desktop runs the timeout hook roughly every 37ms. Any normal plugin will be well and truly done with its work by then. Closes: #1245 Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1416>
This commit is contained in:
parent
cc1499fbb3
commit
86f19a0978
2 changed files with 97 additions and 0 deletions
|
|
@ -116,6 +116,8 @@ struct libinput_lua_plugin {
|
|||
|
||||
struct libinput_plugin_timer *timer;
|
||||
bool in_timer_func;
|
||||
|
||||
usec_t lua_pcall_timeout_end;
|
||||
};
|
||||
|
||||
static struct libinput_lua_plugin *
|
||||
|
|
@ -291,12 +293,34 @@ out:
|
|||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
static void
|
||||
lua_timeout_hook(lua_State *L, lua_Debug *debug)
|
||||
{
|
||||
struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L);
|
||||
struct libinput *libinput = lua_get_libinput(L);
|
||||
usec_t now = libinput_now(libinput);
|
||||
|
||||
if (usec_cmp(now, plugin->lua_pcall_timeout_end) > 0) {
|
||||
luaL_error(L, "Plugin execution timeout (exceeded 1 second)");
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
libinput_lua_pcall(struct libinput_lua_plugin *plugin, int narg, int nres)
|
||||
{
|
||||
lua_State *L = plugin->L;
|
||||
struct libinput *libinput = lua_get_libinput(L);
|
||||
|
||||
plugin->lua_pcall_timeout_end = usec_add_millis(libinput_now(libinput), 1000);
|
||||
|
||||
/* Hook is called every 10M instructions (10-1000ms, depending operations) */
|
||||
lua_sethook(L, lua_timeout_hook, LUA_MASKCOUNT, 10000000);
|
||||
|
||||
int rc = lua_pcall(L, narg, nres, 0);
|
||||
|
||||
/* Clear the hook */
|
||||
lua_sethook(L, NULL, 0, 0);
|
||||
plugin->lua_pcall_timeout_end = usec_from_millis(0);
|
||||
if (rc != LUA_OK) {
|
||||
auto libinput_plugin = plugin->parent;
|
||||
const char *errormsg = lua_tostring(L, -1);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
#include <fcntl.h>
|
||||
#include <inttypes.h>
|
||||
#include <valgrind/valgrind.h>
|
||||
|
||||
#include "util-files.h"
|
||||
#include "util-strings.h"
|
||||
|
|
@ -1101,6 +1102,76 @@ START_TEST(lua_disable_wheel_debouncing)
|
|||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(lua_remove_plugin_on_timeout)
|
||||
{
|
||||
enum when when = litest_test_param_get_i32(test_env->params, "when");
|
||||
_destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
|
||||
_autofree_ char *lua = strdup_printf(
|
||||
"libinput:register({1})\n"
|
||||
"function infinite_loop(device)\n"
|
||||
" local i = 0\n"
|
||||
" while true do\n"
|
||||
" i = i + 1\n"
|
||||
" end\n"
|
||||
"end\n"
|
||||
"function new_device(device)\n"
|
||||
" device:connect(\"evdev-frame\", infinite_loop)\n"
|
||||
"end\n"
|
||||
"%s libinput:connect(\"new-evdev-device\", infinite_loop)\n"
|
||||
"%s libinput:connect(\"new-evdev-device\", new_device)\n",
|
||||
when == DEVICE_NEW ? "" : "--",
|
||||
when == FIRST_FRAME ? "" : "--");
|
||||
|
||||
_autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
|
||||
_litest_context_destroy_ struct libinput *li =
|
||||
litest_create_context_with_plugindir(tmpdir->path);
|
||||
_destroy_(litest_device) *dev = NULL;
|
||||
|
||||
litest_with_logcapture(li, capture) {
|
||||
libinput_plugin_system_load_plugins(li,
|
||||
LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
|
||||
litest_drain_events(li);
|
||||
|
||||
usec_t before = usec_from_now();
|
||||
|
||||
dev = litest_add_device(li, LITEST_MOUSE);
|
||||
litest_drain_events(li);
|
||||
|
||||
if (when == FIRST_FRAME) {
|
||||
litest_event(dev, EV_KEY, BTN_LEFT, 1);
|
||||
litest_event(dev, EV_SYN, SYN_REPORT, 0);
|
||||
litest_dispatch(li);
|
||||
litest_event(dev, EV_KEY, BTN_LEFT, 0);
|
||||
litest_event(dev, EV_SYN, SYN_REPORT, 0);
|
||||
litest_dispatch(li);
|
||||
}
|
||||
|
||||
usec_t after = usec_from_now();
|
||||
usec_t elapsed = usec_delta(after, before);
|
||||
|
||||
/* verify the timeout kicked in around ~1 second (but less than 2s) */
|
||||
litest_assert_int_ge(usec_to_millis(elapsed), 1000U);
|
||||
if (!RUNNING_ON_VALGRIND)
|
||||
litest_assert_int_lt(usec_to_millis(elapsed), 2000U);
|
||||
|
||||
size_t index = 0;
|
||||
litest_assert(strv_find_substring(capture->errors,
|
||||
"Plugin execution timeout",
|
||||
&index));
|
||||
litest_assert_str_in("exceeded 1 second", capture->errors[index]);
|
||||
|
||||
litest_assert(strv_find_substring(capture->errors,
|
||||
"unloading after error",
|
||||
NULL));
|
||||
}
|
||||
/* device events should go through now */
|
||||
litest_event(dev, EV_KEY, BTN_LEFT, 1);
|
||||
litest_event(dev, EV_SYN, SYN_REPORT, 0);
|
||||
litest_dispatch(li);
|
||||
litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
TEST_COLLECTION(lua)
|
||||
{
|
||||
/* clang-format off */
|
||||
|
|
@ -1173,6 +1244,8 @@ TEST_COLLECTION(lua)
|
|||
litest_add_parametrized_no_device(lua_disable_button_debounce, params);
|
||||
litest_add_parametrized_no_device(lua_disable_touchpad_jump_detection, params);
|
||||
litest_add_parametrized_no_device(lua_disable_wheel_debouncing, params);
|
||||
|
||||
litest_add_parametrized_no_device(lua_remove_plugin_on_timeout, params);
|
||||
}
|
||||
/* clang-format on */
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue