Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package libinput for openSUSE:Factory checked in at 2026-06-05 14:56:01 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libinput (Old) and /work/SRC/openSUSE:Factory/.libinput.new.2375 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libinput" Fri Jun 5 14:56:01 2026 rev:132 rq:1357044 version:1.31.3 Changes: -------- --- /work/SRC/openSUSE:Factory/libinput/libinput.changes 2026-05-16 19:23:46.692203032 +0200 +++ /work/SRC/openSUSE:Factory/.libinput.new.2375/libinput.changes 2026-06-05 14:56:02.209727500 +0200 @@ -1,0 +2,14 @@ +Thu Jun 4 01:55:06 UTC 2026 - Jan Engelhardt <[email protected]> + +- Update to release 1.31.3 + * libinput-device-group now sanitizes the PHYS value which + prevents local privilege escalation through udev property + injection. + * `libinput record`: the --autorestart interval handling was + broken for intervals 5s and higher + * `libinput recor`: added a convenience fix for running with + --autorestart. + * Eraser buttons can now be mapped to any button (previously only + BTN_STYLUS, BTN_STYULUS2 and BTN_STYLUS3 were permitted). + +------------------------------------------------------------------- Old: ---- libinput-1.31.2.tar.gz New: ---- libinput-1.31.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libinput.spec ++++++ --- /var/tmp/diff_new_pack.BDX1g2/_old 2026-06-05 14:56:03.305772828 +0200 +++ /var/tmp/diff_new_pack.BDX1g2/_new 2026-06-05 14:56:03.309772993 +0200 @@ -37,7 +37,7 @@ %define lname libinput10 %define pname libinput Name: libinput%{?xsuffix} -Version: 1.31.2 +Version: 1.31.3 Release: 0 Summary: Input device and event processing library License: MIT ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.BDX1g2/_old 2026-06-05 14:56:03.361775144 +0200 +++ /var/tmp/diff_new_pack.BDX1g2/_new 2026-06-05 14:56:03.365775309 +0200 @@ -1,5 +1,5 @@ -mtime: 1778803125 -commit: 35d2d9dc284600b2191c7ce44233899d3564df78e84a2dee5caf6ab7c6b50bbb +mtime: 1780538190 +commit: d7355c8f4dc61a1e7d37c0b5ac864194a4eff5025e4d9cd5464991bc9805ee7b url: https://src.opensuse.org/jengelh/libinput revision: master ++++++ build.specials.obscpio ++++++ ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2026-06-04 03:56:30.000000000 +0200 @@ -0,0 +1 @@ +.osc ++++++ libinput-1.31.2.tar.gz -> libinput-1.31.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/meson.build new/libinput-1.31.3/meson.build --- old/libinput-1.31.2/meson.build 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/meson.build 2026-06-04 03:05:01.000000000 +0200 @@ -1,5 +1,5 @@ project('libinput', 'c', - version : '1.31.2', + version : '1.31.3', license : 'MIT/Expat', default_options : [ 'c_std=gnu99', 'warning_level=2' ], meson_version : '>= 0.64.0') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/src/evdev-tablet-pad.c new/libinput-1.31.3/src/evdev-tablet-pad.c --- old/libinput-1.31.2/src/evdev-tablet-pad.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/src/evdev-tablet-pad.c 2026-06-04 03:05:01.000000000 +0200 @@ -237,6 +237,9 @@ static inline double normalize_wacom_strip(const struct input_absinfo *absinfo) { + if (absinfo->maximum <= 1 || absinfo->value <= 0) + return 0.0; + /* strip axes don't use a proper value, they just shift the bit left * for each position. 0 isn't a real value either, it's only sent on * finger release */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/src/evdev-tablet.c new/libinput-1.31.3/src/evdev-tablet.c --- old/libinput-1.31.2/src/evdev-tablet.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/src/evdev-tablet.c 2026-06-04 03:05:01.000000000 +0200 @@ -1311,18 +1311,6 @@ static enum libinput_config_status eraser_button_set_button(struct libinput_tablet_tool *tool, uint32_t button) { - switch (button) { - case BTN_STYLUS: - case BTN_STYLUS2: - case BTN_STYLUS3: - break; - default: - log_bug_libinput(libinput_device_get_context(tool->last_device), - "Unsupported eraser button 0x%x", - button); - return LIBINPUT_CONFIG_STATUS_INVALID; - } - tool->eraser_button.want_button = button; eraser_button_toggle(tool); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/src/evdev-totem.c new/libinput-1.31.3/src/evdev-totem.c --- old/libinput-1.31.2/src/evdev-totem.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/src/evdev-totem.c 2026-06-04 03:05:01.000000000 +0200 @@ -753,7 +753,7 @@ libevdev_get_abs_maximum(evdev, ABS_MT_TOOL_TYPE) >= MT_TOOL_DIAL; has_size = evdev_device_get_size(device, &w, &h) == 0; has_touch_size = - libevdev_get_abs_resolution(device->evdev, ABS_MT_TOUCH_MAJOR) > 0 || + libevdev_get_abs_resolution(device->evdev, ABS_MT_TOUCH_MAJOR) > 0 && libevdev_get_abs_resolution(device->evdev, ABS_MT_TOUCH_MINOR) > 0; if (has_xy && has_slot && has_tool_dial && has_size && has_touch_size) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/src/evdev.c new/libinput-1.31.3/src/evdev.c --- old/libinput-1.31.2/src/evdev.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/src/evdev.c 2026-06-04 03:05:01.000000000 +0200 @@ -1635,6 +1635,16 @@ return true; absinfo = libevdev_get_abs_info(evdev, code); + + if (((uint64_t)absinfo->maximum - (uint64_t)absinfo->minimum) > INT32_MAX / 2) { + evdev_log_bug_kernel(device, + "kernel axis range [%d, %d] on %s too extreme\n", + absinfo->minimum, + absinfo->maximum, + libevdev_event_code_get_name(EV_ABS, code)); + return false; + } + if (absinfo->minimum == absinfo->maximum) { /* Some devices have a sort-of legitimate min/max of 0 for * ABS_MISC and above (e.g. Roccat Kone XTD). Don't ignore @@ -1654,6 +1664,18 @@ libevdev_event_code_get_name(EV_ABS, code)); return false; } + } else if (absinfo->minimum > absinfo->maximum) { + evdev_log_bug_kernel(device, + "device has min > max on %s\n", + libevdev_event_code_get_name(EV_ABS, code)); + return false; + } + + if (absinfo->resolution < 0) { + evdev_log_bug_kernel(device, + "kernel resolution is negative on %s\n", + libevdev_event_code_get_name(EV_ABS, code)); + return false; } return true; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/src/libinput.c new/libinput-1.31.3/src/libinput.c --- old/libinput-1.31.2/src/libinput.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/src/libinput.c 2026-06-04 03:05:01.000000000 +0200 @@ -5216,16 +5216,9 @@ if (!libinput_tablet_tool_config_eraser_button_get_modes(tool)) return LIBINPUT_CONFIG_STATUS_UNSUPPORTED; - switch (button) { - case BTN_STYLUS: - case BTN_STYLUS2: - case BTN_STYLUS3: - break; - default: - if (!libinput_tablet_tool_has_button(tool, button)) - return LIBINPUT_CONFIG_STATUS_INVALID; - break; - } + evdev_usage_t usage = evdev_usage_from_code(EV_KEY, button); + if (!evdev_usage_is_button(usage)) + return LIBINPUT_CONFIG_STATUS_INVALID; return tool->config.eraser_button.set_button(tool, button); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/src/libinput.h new/libinput-1.31.3/src/libinput.h --- old/libinput-1.31.2/src/libinput.h 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/src/libinput.h 2026-06-04 03:05:01.000000000 +0200 @@ -7371,14 +7371,9 @@ * the eraser mode to @ref LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON via * libinput_tablet_tool_config_eraser_button_set_mode(). * - * The buttons BTN_STYLUS, BTN_STYLUS2 and BTN_STYLUS2 are always - * allowed, even if libinput_tablet_tool_has_button() returns zero - * for the button. Otherwise, the button must be one that - * libinput_tablet_tool_has_button() returns a nonzero value for. - * * @param tool The libinput tool - * @param button The button, usually one of BTN_STYLUS, BTN_STYLUS2 or - * BTN_STYLUS3 + * @param button The button code. Must be a valid button (e.g. BTN_STYLUS) + * excluding fake buttons (e.g. BTN_TOOL_*) and keys (KEY_*) * * @return A config status code * diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/src/util-strings.h new/libinput-1.31.3/src/util-strings.h --- old/libinput-1.31.2/src/util-strings.h 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/src/util-strings.h 2026-06-04 03:05:01.000000000 +0200 @@ -543,7 +543,10 @@ /** * Return a copy of str with all % converted to %% to make the string - * acceptable as printf format. + * acceptable as printf format, and all non-NUL control characters + * (bytes 0x01-0x1f, 0x7f) replaced with '?' to prevent terminal + * escape sequence injection. NUL bytes are excluded implicitly + * because the string is null-terminated. */ static inline char * str_sanitize(const char *str) @@ -551,19 +554,34 @@ if (!str) return NULL; - if (!strchr(str, '%')) - return strdup(str); - size_t slen = strlen(str); slen = min(slen, 512); + + bool needs_sanitization = false; + for (size_t i = 0; i < slen; i++) { + unsigned char c = str[i]; + if (c == '%' || c < 0x20 || c == 0x7f) { + needs_sanitization = true; + break; + } + } + if (!needs_sanitization) + return strdup(str); + char *sanitized = zalloc(2 * slen + 1); const char *src = str; char *dst = sanitized; for (size_t i = 0; i < slen; i++) { - if (*src == '%') + unsigned char c = *src++; + if (c == '%') { + *dst++ = '%'; *dst++ = '%'; - *dst++ = *src++; + } else if (c < 0x20 || c == 0x7f) { + *dst++ = '?'; + } else { + *dst++ = c; + } } *dst = '\0'; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/test/litest.h new/libinput-1.31.3/test/litest.h --- old/libinput-1.31.2/test/litest.h 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/test/litest.h 2026-06-04 03:05:01.000000000 +0200 @@ -40,6 +40,8 @@ #include "litest-runner.h" #include "quirks.h" +DEFINE_DESTROY_CLEANUP_FUNC(libevdev_uinput); + #define START_TEST(func_) \ static enum litest_runner_result func_(const struct litest_runner_test_env *test_env) { \ int _i _unused_ = test_env->rangeval; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/test/test-device.c new/libinput-1.31.3/test/test-device.c --- old/libinput-1.31.2/test/test-device.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/test/test-device.c 2026-06-04 03:05:01.000000000 +0200 @@ -1656,6 +1656,77 @@ } END_TEST +enum extreme_axis_range_idx { + EXTREME_RANGE_0_TO_INT32MAX, + EXTREME_RANGE_INT32MIN_TO_0, + EXTREME_RANGE_NEGHALF_TO_POSHALF, +}; + +START_TEST(abs_device_extreme_axis_range) +{ + struct libevdev_uinput *uinput; + struct libinput_device *device; + int idx = litest_test_param_get_i32(test_env->params, "range"); + /* All ranges exceed INT32_MAX/2, device should be rejected */ + const struct { + int32_t min, max; + } ranges[] = { + [EXTREME_RANGE_0_TO_INT32MAX] = { 0, INT32_MAX }, + [EXTREME_RANGE_INT32MIN_TO_0] = { INT32_MIN, 0 }, + [EXTREME_RANGE_NEGHALF_TO_POSHALF] = { -(INT32_MAX / 2), + INT32_MAX / 2 }, + }; + struct input_absinfo absinfo[] = { + { ABS_X, ranges[idx].min, ranges[idx].max, 0, 0, 0 }, + { ABS_Y, 0, 1000, 0, 0, 0 }, + { -1, -1, -1, -1, -1, -1 }, + }; + + _litest_context_destroy_ struct libinput *li = litest_create_context(); + litest_disable_log_handler(li); + /* clang-format off */ + uinput = litest_create_uinput_abs_device("test device", NULL, + absinfo, + EV_KEY, BTN_LEFT, + EV_KEY, BTN_RIGHT, + -1); + /* clang-format on */ + device = libinput_path_add_device(li, libevdev_uinput_get_devnode(uinput)); + litest_restore_log_handler(li); + litest_assert_ptr_null(device); + + libevdev_uinput_destroy(uinput); +} +END_TEST + +START_TEST(abs_device_negative_resolution) +{ + struct libevdev_uinput *uinput; + struct libinput_device *device; + struct input_absinfo absinfo[] = { + { ABS_X, 0, 1000, 0, 0, -1 }, /* negative resolution */ + { ABS_Y, 0, 1000, 0, 0, -1 }, /* negative resolution */ + { -1, -1, -1, -1, -1, -1 }, + }; + + _litest_context_destroy_ struct libinput *li = litest_create_context(); + litest_disable_log_handler(li); + /* clang-format off */ + uinput = litest_create_uinput_abs_device("test device", NULL, + absinfo, + EV_KEY, BTN_LEFT, + EV_KEY, BTN_RIGHT, + -1); + /* clang-format on */ + device = libinput_path_add_device(li, libevdev_uinput_get_devnode(uinput)); + litest_restore_log_handler(li); + /* Device should be rejected */ + litest_assert_ptr_null(device); + + libevdev_uinput_destroy(uinput); +} +END_TEST + TEST_COLLECTION(device) { /* clang-format off */ @@ -1713,6 +1784,13 @@ litest_add(device_wheel_only, LITEST_WHEEL, LITEST_RELATIVE|LITEST_ABSOLUTE|LITEST_TABLET); litest_add_no_device(device_accelerometer); + litest_with_parameters(params, "range", 'I', 3, + litest_named_i32(EXTREME_RANGE_0_TO_INT32MAX, "0-to-INT32MAX"), + litest_named_i32(EXTREME_RANGE_INT32MIN_TO_0, "INT32MIN-to-0"), + litest_named_i32(EXTREME_RANGE_NEGHALF_TO_POSHALF, "-INT32MAX/2-to-INT32MAX/2")) { + litest_add_parametrized_no_device(abs_device_extreme_axis_range, params); + } + litest_add_no_device(abs_device_negative_resolution); litest_add(device_udev_tag_wacom_tablet, LITEST_TABLET, LITEST_TOTEM); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/test/test-pad.c new/libinput-1.31.3/test/test-pad.c --- old/libinput-1.31.2/test/test-pad.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/test/test-pad.c 2026-06-04 03:05:01.000000000 +0200 @@ -1113,6 +1113,54 @@ } END_TEST +START_TEST(pad_strip_wacom_degenerate_max) +{ + struct libinput_event *ev; + struct libinput_event_tablet_pad *pev; + double pos; + + /* Override ABS_RX to have maximum=1, which would cause + * log2(1) = 0 and division by zero in normalize_wacom_strip() + * without the guard. */ + /* clang-format off */ + struct input_absinfo abs_override[] = { + { ABS_RX, 0, 1, 0, 0, 0 }, + { -1, -1, -1, -1, -1, -1 }, + }; + /* clang-format on */ + + _litest_context_destroy_ struct libinput *li = litest_create_context(); + struct litest_device *dev = + litest_add_device_with_overrides(li, + LITEST_WACOM_INTUOS3_PAD, + NULL, + NULL, + abs_override, + NULL); + + litest_drain_events(li); + + /* Send strip events - value 1 with max 1 triggers + * log2(1)/log2(1) = 0/0 = NaN without the guard */ + litest_pad_strip_start(dev, 100); + litest_dispatch(li); + + ev = libinput_get_event(li); + pev = litest_is_pad_strip_event(ev, 0, LIBINPUT_TABLET_PAD_STRIP_SOURCE_FINGER); + pos = libinput_event_tablet_pad_get_strip_position(pev); + /* Without the fix, pos would be NaN from 0/0 division. + * With the fix, pos must be a valid finite number. */ + litest_assert_double_ge(pos, 0.0); + litest_assert_double_le(pos, 1.0); + libinput_event_destroy(ev); + + litest_pad_strip_end(dev); + litest_drain_events(li); + + litest_device_destroy(dev); +} +END_TEST + START_TEST(pad_send_events_disabled) { struct litest_device *dev = litest_current_device(); @@ -1171,6 +1219,7 @@ litest_add(pad_has_strip, LITEST_STRIP, LITEST_ANY); litest_add(pad_strip, LITEST_STRIP, LITEST_ANY); litest_add(pad_strip_finger_up, LITEST_STRIP, LITEST_ANY); + litest_add_no_device(pad_strip_wacom_degenerate_max); litest_add(pad_has_dial, LITEST_DIAL, LITEST_ANY); litest_add(pad_dial_low_res, LITEST_DIAL, LITEST_ANY); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/test/test-tablet.c new/libinput-1.31.3/test/test-tablet.c --- old/libinput-1.31.2/test/test-tablet.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/test/test-tablet.c 2026-06-04 03:05:01.000000000 +0200 @@ -7883,6 +7883,189 @@ } END_TEST +START_TEST(tablet_eraser_button_different_buttons) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_DISTANCE, 10 }, + { ABS_PRESSURE, 0 }, + { -1, -1 }, + }; + _unref_(libinput_tablet_tool) *pen = NULL; + + uint32_t eraser_button_mapping = + litest_test_param_get_i32(test_env->params, "eraser-button-mapping"); + + if (!libevdev_has_event_code(dev->evdev, EV_KEY, BTN_TOOL_RUBBER)) + return LITEST_NOT_APPLICABLE; + + litest_log_group("Prox in/out to disable proximity timer") { + litest_tablet_proximity_in(dev, 25, 25, axes); + litest_tablet_proximity_out(dev); + litest_timeout_tablet_proxout(li); + + litest_checkpoint( + "Eraser prox in/out to force-disable config on broken tablets"); + litest_tablet_set_tool_type(dev, BTN_TOOL_RUBBER); + litest_tablet_proximity_in(dev, 25, 25, axes); + litest_tablet_proximity_out(dev); + litest_timeout_tablet_proxout(li); + } + + litest_drain_events(li); + + litest_log_group("Proximity in for pen") { + litest_tablet_set_tool_type(dev, BTN_TOOL_PEN); + litest_tablet_proximity_in(dev, 20, 20, axes); + litest_dispatch(li); + _destroy_(libinput_event) *ev = libinput_get_event(li); + auto tev = litest_is_proximity_event( + ev, + LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); + pen = libinput_event_tablet_tool_get_tool(tev); + litest_assert_enum_eq(libinput_tablet_tool_get_type(pen), + LIBINPUT_TABLET_TOOL_TYPE_PEN); + pen = libinput_tablet_tool_ref(pen); + } + + if (!libinput_tablet_tool_config_eraser_button_get_modes(pen)) + return LITEST_NOT_APPLICABLE; + + auto status = libinput_tablet_tool_config_eraser_button_set_mode( + pen, + LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON); + litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS); + status = libinput_tablet_tool_config_eraser_button_set_button( + pen, + eraser_button_mapping); + litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS); + + litest_log_group("Prox out to apply changed settings") { + litest_tablet_proximity_out(dev); + litest_timeout_tablet_proxout(li); + litest_drain_events(li); + } + + litest_mark_test_start(); + + litest_tablet_proximity_in(dev, 10, 10, axes); + litest_drain_events(li); + + /* Make sure the button still works as-is */ + if (libinput_tablet_tool_has_button(pen, eraser_button_mapping)) { + litest_log_group("Testing button on pen") { + litest_event(dev, EV_KEY, eraser_button_mapping, 1); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_dispatch(li); + litest_event(dev, EV_KEY, eraser_button_mapping, 0); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_dispatch(li); + litest_assert_tablet_button_event( + li, + eraser_button_mapping, + LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_tablet_button_event( + li, + eraser_button_mapping, + LIBINPUT_BUTTON_STATE_RELEASED); + } + } + + litest_dispatch(li); + + litest_log_group("Prox out for the pen ...") { + litest_with_event_frame(dev) { + litest_tablet_set_tool_type(dev, BTN_TOOL_PEN); + litest_tablet_proximity_out(dev); + } + litest_dispatch(li); + } + + litest_log_group("...and prox in for the eraser") { + litest_with_event_frame(dev) { + litest_tablet_set_tool_type(dev, BTN_TOOL_RUBBER); + litest_tablet_proximity_in(dev, 12, 12, axes); + } + litest_dispatch(li); + } + + litest_drain_events_of_type(li, LIBINPUT_EVENT_TABLET_TOOL_AXIS); + + litest_log_group("Expect button event") { + _destroy_(libinput_event) *ev = libinput_get_event(li); + auto tev = + litest_is_tablet_event(ev, LIBINPUT_EVENT_TABLET_TOOL_BUTTON); + litest_assert_enum_eq(libinput_event_tablet_tool_get_button_state(tev), + LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_int_eq(libinput_event_tablet_tool_get_button(tev), + eraser_button_mapping); + litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen); + } + + litest_log_group("Prox out for the eraser...") { + litest_with_event_frame(dev) { + litest_tablet_proximity_out(dev); + } + litest_dispatch(li); + } + + litest_log_group("...and prox in for the pen") { + litest_with_event_frame(dev) { + litest_tablet_set_tool_type(dev, BTN_TOOL_PEN); + litest_tablet_proximity_in(dev, 12, 12, axes); + } + litest_dispatch(li); + } + + litest_drain_events_of_type(li, LIBINPUT_EVENT_TABLET_TOOL_AXIS); + + litest_log_group("Expect button event") { + _destroy_(libinput_event) *ev = libinput_get_event(li); + auto tev = + litest_is_tablet_event(ev, LIBINPUT_EVENT_TABLET_TOOL_BUTTON); + litest_assert_int_eq(libinput_event_tablet_tool_get_button(tev), + eraser_button_mapping); + litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen); + } +} +END_TEST + +START_TEST(tablet_eraser_button_invalid_buttons) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_DISTANCE, 10 }, + { ABS_PRESSURE, 0 }, + { -1, -1 }, + }; + + uint32_t eraser_button_mapping = + litest_test_param_get_i32(test_env->params, "eraser-button-mapping"); + + if (!libevdev_has_event_code(dev->evdev, EV_KEY, BTN_TOOL_RUBBER)) + return LITEST_NOT_APPLICABLE; + + litest_drain_events(li); + litest_tablet_proximity_in(dev, 20, 20, axes); + litest_dispatch(li); + + _destroy_(libinput_event) *ev = libinput_get_event(li); + auto tev = + litest_is_proximity_event(ev, LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); + auto tool = libinput_event_tablet_tool_get_tool(tev); + + if (!libinput_tablet_tool_config_eraser_button_get_modes(tool)) + return LITEST_NOT_APPLICABLE; + + auto status = libinput_tablet_tool_config_eraser_button_set_button( + tool, + eraser_button_mapping); + litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_INVALID); +} +END_TEST + START_TEST(tablet_eraser_button_config_after_device_removal) { _litest_context_destroy_ struct libinput *li = litest_create_context(); @@ -8099,7 +8282,21 @@ "with-motion-events", 'b') { litest_add_parametrized(tablet_eraser_button_disabled, LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params); } - + litest_with_parameters(params, + "eraser-button-mapping", 'I', 4, + litest_named_i32(BTN_STYLUS), + litest_named_i32(BTN_STYLUS3), + litest_named_i32(BTN_LEFT), + litest_named_i32(BTN_BACK)){ + litest_add_parametrized(tablet_eraser_button_different_buttons, LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params); + } + litest_with_parameters(params, + "eraser-button-mapping", 'I', 3, + litest_named_i32(BTN_TOUCH), + litest_named_i32(BTN_TOOL_FINGER), + litest_named_i32(KEY_A)) { + litest_add_parametrized(tablet_eraser_button_invalid_buttons, LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params); + } litest_add_no_device(tablet_eraser_button_config_after_device_removal); /* clang-format on */ } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/test/test-totem.c new/libinput-1.31.3/test/test-totem.c --- old/libinput-1.31.2/test/test-totem.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/test/test-totem.c 2026-06-04 03:05:01.000000000 +0200 @@ -556,6 +556,57 @@ } END_TEST +START_TEST(totem_touch_size_missing_resolution) +{ + struct libinput_device *device; + /* Match the Dell Canvas Totem quirk: vendor=0x2575, product=0x0204, + * name matching "*System Multi Axis", bus=USB */ + struct input_id id = { + .bustype = 0x3, + .vendor = 0x2575, + .product = 0x0204, + }; + struct input_absinfo absinfo[] = { + { ABS_MT_SLOT, 0, 4, 0, 0, 0 }, + { ABS_MT_TOUCH_MAJOR, 0, 32767, 0, 0, 10 }, + { ABS_MT_TOUCH_MINOR, 0, 32767, 0, 0, 0 }, /* resolution missing */ + { ABS_MT_ORIENTATION, -89, 89, 0, 0, 0 }, + { ABS_MT_POSITION_X, 0, 32767, 0, 0, 55 }, + { ABS_MT_POSITION_Y, 0, 32767, 0, 0, 98 }, + { ABS_MT_TOOL_TYPE, 9, 10, 0, 0, 0 }, + { ABS_MT_TRACKING_ID, 0, 65535, 0, 0, 0 }, + { -1, -1, -1, -1, -1, -1 }, + }; + /* clang-format off */ + int events[] = { + EV_KEY, BTN_0, + INPUT_PROP_MAX, INPUT_PROP_DIRECT, + -1, -1, + }; + /* clang-format on */ + + _destroy_(libevdev_uinput) *uinput = + litest_create_uinput_device_from_description( + "CoolTouch System Multi Axis", + &id, + absinfo, + events); + _litest_context_destroy_ struct libinput *li = litest_create_context(); + litest_disable_log_handler(li); + device = libinput_path_add_device(li, libevdev_uinput_get_devnode(uinput)); + litest_restore_log_handler(li); + + /* The device matches the Dell Canvas Totem quirk but has + * ABS_MT_TOUCH_MINOR resolution missing. The totem dispatch + * should reject it, so it must not be recognized as a tablet + * tool device. */ + if (device) + litest_assert(!libinput_device_has_capability( + device, + LIBINPUT_DEVICE_CAP_TABLET_TOOL)); +} +END_TEST + TEST_COLLECTION(totem) { /* clang-format off */ @@ -571,6 +622,7 @@ litest_add(totem_button, LITEST_TOTEM, LITEST_ANY); litest_add(totem_button_down_on_init, LITEST_TOTEM, LITEST_ANY); litest_add_no_device(totem_button_up_on_delete); + litest_add_no_device(totem_touch_size_missing_resolution); litest_add(totem_arbitration_below, LITEST_TOTEM, LITEST_ANY); litest_add(totem_arbitration_during, LITEST_TOTEM, LITEST_ANY); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/test/test-utils.c new/libinput-1.31.3/test/test-utils.c --- old/libinput-1.31.2/test/test-utils.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/test/test-utils.c 2026-06-04 03:05:01.000000000 +0200 @@ -2199,6 +2199,16 @@ { "x %", "x %%" }, { "%sx", "%%sx" }, { "%s%s", "%%s%%s" }, + { "\t", "?" }, + { "\n", "?" }, + { "\r", "?" }, + { "\x1b[31m", "?[31m" }, + { "foo\tbar", "foo?bar" }, + { "foo\nbar", "foo?bar" }, + { "\x01\x1f\x7f", "???" }, + { "clean", "clean" }, + { "a\x1b[0mb", "a?[0mb" }, + { "%\n", "%%?" }, { NULL, NULL }, }; /* clang-format on */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/tools/libinput-record.c new/libinput-1.31.3/tools/libinput-record.c --- old/libinput-1.31.2/tools/libinput-record.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/tools/libinput-record.c 2026-06-04 03:05:01.000000000 +0200 @@ -38,7 +38,6 @@ #include <sys/epoll.h> #include <sys/signalfd.h> #include <sys/stat.h> -#include <sys/timerfd.h> #include <sys/utsname.h> #include <time.h> #include <unistd.h> @@ -108,7 +107,7 @@ }; struct record_context { - int timeout; + usec_t timeout; bool show_keycodes; usec_t offset; @@ -130,8 +129,7 @@ struct list sources; struct { - bool had_events_since_last_time; - bool skipped_timer_print; + usec_t last_wall_time; } timestamps; bool had_events; @@ -1353,13 +1351,13 @@ } static void -print_libinput_header(FILE *fp, int timeout) +print_libinput_header(FILE *fp, usec_t timeout) { iprintf(fp, I_TOPLEVEL, "libinput:\n"); iprintf(fp, I_LIBINPUT, "version: \"%s\"\n", LIBINPUT_VERSION); iprintf(fp, I_LIBINPUT, "git: \"%s\"\n", LIBINPUT_GIT_VERSION); - if (timeout > 0) - iprintf(fp, I_LIBINPUT, "autorestart: %d\n", timeout); + if (!usec_is_zero(timeout)) + iprintf(fp, I_LIBINPUT, "autorestart: %u\n", usec_to_seconds(timeout)); } static void @@ -1519,7 +1517,8 @@ break; } - iprintf(fp, I_EVDEV, "# Name: %s\n", libevdev_get_name(dev)); + _autofree_ char *name = str_sanitize(libevdev_get_name(dev)); + iprintf(fp, I_EVDEV, "# Name: %s\n", name ? name : ""); iprintf(fp, I_EVDEV, "# ID: bus 0x%04x%svendor 0x%04x product 0x%04x version 0x%04x\n", @@ -1570,7 +1569,8 @@ static void print_bits_info(FILE *fp, struct libevdev *dev) { - iprintf(fp, I_EVDEV, "name: \"%s\"\n", libevdev_get_name(dev)); + _autofree_ char *name = str_sanitize(libevdev_get_name(dev)); + iprintf(fp, I_EVDEV, "name: \"%s\"\n", name ? name : ""); iprintf(fp, I_EVDEV, "id: [%d, %d, %d, %d]\n", @@ -1936,7 +1936,8 @@ if (rc != 0) continue; - fprintf(stderr, "%s%s: %s\n", prefix, path, libevdev_get_name(device)); + _autofree_ char *name = str_sanitize(libevdev_get_name(device)); + fprintf(stderr, "%s%s: %s\n", prefix, path, name ? name : ""); libevdev_free(device); available_devices++; } @@ -2071,21 +2072,6 @@ } } -static void -arm_timer(int timerfd) -{ - time_t t = time(NULL); - struct tm tm; - struct itimerspec interval = { - .it_value = { 0, 0 }, - .it_interval = { 5, 0 }, - }; - - localtime_r(&t, &tm); - interval.it_value.tv_sec = 5 - (tm.tm_sec % 5); - timerfd_settime(timerfd, 0, &interval, NULL); -} - static struct source * add_source(struct record_context *ctx, int fd, @@ -2132,33 +2118,11 @@ } static void -timefd_dispatch(struct record_context *ctx, int fd, void *data) -{ - char discard[64]; - - (void)read(fd, discard, sizeof(discard)); - - if (ctx->timestamps.had_events_since_last_time) { - print_wall_time(ctx); - ctx->timestamps.had_events_since_last_time = false; - ctx->timestamps.skipped_timer_print = false; - } else { - ctx->timestamps.skipped_timer_print = true; - } -} - -static void evdev_dispatch(struct record_context *ctx, int fd, void *data) { struct record_device *this_device = data; - if (ctx->timestamps.skipped_timer_print) { - print_wall_time(ctx); - ctx->timestamps.skipped_timer_print = false; - } - ctx->had_events = true; - ctx->timestamps.had_events_since_last_time = true; handle_events(ctx, this_device); } @@ -2179,7 +2143,6 @@ struct hidraw *hidraw = data; ctx->had_events = true; - ctx->timestamps.had_events_since_last_time = true; handle_hidraw(hidraw); } @@ -2189,15 +2152,29 @@ struct source *source; struct epoll_event ep[64]; int i, count; + int timeout = usec_to_millis(ctx->timeout); - count = epoll_wait(ctx->epoll_fd, ep, ARRAY_LENGTH(ep), ctx->timeout); + count = epoll_wait(ctx->epoll_fd, + ep, + ARRAY_LENGTH(ep), + timeout > 0 ? timeout : -1); if (count < 0) return -errno; + if (count > 0) { + usec_t now = usec_from_now(); + usec_t dt = usec_delta(now, ctx->timestamps.last_wall_time); + if (usec_cmp(dt, usec_from_seconds(5)) > 0) { + ctx->timestamps.last_wall_time = now; + print_wall_time(ctx); + } + } + for (i = 0; i < count; ++i) { source = ep[i].data.ptr; if (source->fd == -1) continue; + source->dispatch(ctx, source->fd, source->user_data); } @@ -2207,13 +2184,12 @@ static int mainloop(struct record_context *ctx) { - bool autorestart = (ctx->timeout > 0); + bool autorestart = !usec_is_zero(ctx->timeout); struct source *source; struct record_device *d = NULL; sigset_t mask; - int sigfd, timerfd; + int sigfd; - assert(ctx->timeout != 0); assert(!list_empty(&ctx->devices)); ctx->epoll_fd = epoll_create1(0); @@ -2227,10 +2203,6 @@ sigfd = signalfd(-1, &mask, SFD_NONBLOCK); add_source(ctx, sigfd, signalfd_dispatch, NULL); - timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - add_source(ctx, timerfd, timefd_dispatch, NULL); - arm_timer(timerfd); - list_for_each(d, &ctx->devices, link) { struct hidraw *hidraw; @@ -2279,8 +2251,8 @@ if (autorestart) iprintf(ctx->first_device->fp, I_NONE, - "# Autorestart timeout: %d\n", - ctx->timeout); + "# Autorestart timeout: %u\n", + usec_to_seconds(ctx->timeout)); iprintf(ctx->first_device->fp, I_TOPLEVEL, "devices:\n"); @@ -2290,6 +2262,8 @@ print_device_description(d); iprintf(d->fp, I_DEVICE, "events:\n"); } + + ctx->timestamps.last_wall_time = usec_from_now(); print_wall_time(ctx); if (ctx->libinput) { @@ -2324,8 +2298,8 @@ list_for_each(d, &ctx->devices, link) { iprintf(d->fp, I_NONE, - "# Closing after %ds inactivity", - ctx->timeout / 1000); + "# Closing after %us inactivity", + usec_to_seconds(ctx->timeout)); } } @@ -2651,8 +2625,9 @@ main(int argc, char **argv) { struct record_context ctx = { - .timeout = -1, + .timeout = usec_from_uint64_t(0), .show_keycodes = false, + .timestamps.last_wall_time = usec_from_uint64_t(0), }; struct option opts[] = { { "autorestart", required_argument, 0, OPT_AUTORESTART }, @@ -2690,14 +2665,16 @@ usage(); rc = EXIT_SUCCESS; goto out; - case OPT_AUTORESTART: - if (!safe_atoi(optarg, &ctx.timeout) || ctx.timeout <= 0) { + case OPT_AUTORESTART: { + int timeout; + if (!safe_atoi(optarg, &timeout) || timeout <= 0) { usage(); rc = EXIT_INVALID_USAGE; goto out; } - ctx.timeout = ctx.timeout * 1000; + ctx.timeout = usec_from_seconds(timeout); break; + } case 'o': case OPT_OUTFILE: output_arg = optarg; @@ -2752,10 +2729,10 @@ optind++; } - if (ctx.timeout > 0 && output_arg == NULL) { - fprintf(stderr, "Option --autorestart requires --output-file\n"); - rc = EXIT_INVALID_USAGE; - goto out; + if (!usec_is_zero(ctx.timeout) && output_arg == NULL) { + output_arg = "libinput-recording.yml"; + fprintf(stderr, + "Option --autorestart requires --output-file, defaulting to libinput-recording.yml\n"); } ctx.output_file.name = safe_strdup(output_arg); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/tools/test_tool_option_parsing.py new/libinput-1.31.3/tools/test_tool_option_parsing.py --- old/libinput-1.31.2/tools/test_tool_option_parsing.py 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/tools/test_tool_option_parsing.py 2026-06-04 03:05:01.000000000 +0200 @@ -360,7 +360,7 @@ def test_libinput_record_autorestart(libinput_record, recording): libinput_record.run_command_invalid(["--autorestart"]) - libinput_record.run_command_invalid(["--autorestart=2"]) + libinput_record.run_command_success(["--autorestart=2"]) libinput_record.run_command_success(["-o", recording, "--autorestart=2"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libinput-1.31.2/udev/libinput-device-group.c new/libinput-1.31.3/udev/libinput-device-group.c --- old/libinput-1.31.2/udev/libinput-device-group.c 2026-05-14 12:56:17.000000000 +0200 +++ new/libinput-1.31.3/udev/libinput-device-group.c 2026-06-04 03:05:01.000000000 +0200 @@ -107,7 +107,8 @@ udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(e)) { struct udev_device *d; - const char *path, *phys; + _autofree_ char *phys = NULL; + const char *path; const char *pidstr, *vidstr; int pid, vid, dist; @@ -122,7 +123,7 @@ vidstr = udev_device_get_property_value(d, "ID_VENDOR_ID"); pidstr = udev_device_get_property_value(d, "ID_MODEL_ID"); - phys = udev_device_get_sysattr_value(d, "phys"); + phys = str_sanitize(udev_device_get_sysattr_value(d, "phys")); if (vidstr && pidstr && phys && safe_atoi_base(vidstr, &vid, 16) && safe_atoi_base(pidstr, &pid, 16) && vid == VENDOR_ID_WACOM && @@ -134,7 +135,7 @@ best_dist = dist; free(*phys_attr); - *phys_attr = safe_strdup(phys); + *phys_attr = steal(&phys); } } @@ -151,7 +152,8 @@ int rc = 1; struct udev *udev = NULL; struct udev_device *device = NULL; - const char *syspath, *phys = NULL; + _autofree_ char *phys = NULL; + const char *syspath = NULL; const char *product; int bustype, vendor_id, product_id, version; char group[1024]; @@ -175,8 +177,7 @@ * bit and use the remainder as device group identifier */ while (device != NULL) { struct udev_device *parent; - - phys = udev_device_get_sysattr_value(device, "phys"); + phys = str_sanitize(udev_device_get_sysattr_value(device, "phys")); if (phys) break;
