eis: keep track of touch IDs and don't allow duplicate ones

A client sending duplicate touch IDs will be disconnected but
motion or end events for touches that no longer exist will be silently
ignored.

The tracking state uses a uint64_t to store currently valid touch ids -
since the whole range of touch ids are uint32_t (including zero) this is
the simple way of using an out-of-range marker value (UINT64_MAX)

Part-of: <https://gitlab.freedesktop.org/libinput/libei/-/merge_requests/368>
This commit is contained in:
Peter Hutterer 2025-12-10 16:25:09 +10:00 committed by Marge Bot
parent 84c23989e9
commit 330b54d389
3 changed files with 160 additions and 3 deletions

View file

@ -25,6 +25,7 @@
#include "config.h"
#include <errno.h>
#include <stdint.h>
#include "util-macros.h"
#include "util-bits.h"
@ -641,6 +642,35 @@ eis_device_get_keyboard_interface(struct eis_device *device)
return &keyboard_interface;
}
/* Returns true and the position of the touch with the given ID, or
* false and the first position that is available
*/
static bool
find_touch(struct eis_device *device, uint32_t touchid, size_t *index)
{
ssize_t first_available = -1;
for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) {
if (device->touch_state.down[i] != UINT64_MAX) {
if (device->touch_state.down[i] == touchid) {
if (index)
*index = i;
return true;
}
} else if (first_available < 0) {
first_available = i;
}
}
if (index) {
if (first_available < 0)
*index = EIS_MAX_TOUCHES;
else
*index = (size_t)first_available;
}
return false;
}
static struct brei_result *
client_msg_touch_down(struct eis_touchscreen *touchscreen,
uint32_t touchid, float x, float y)
@ -655,6 +685,17 @@ client_msg_touch_down(struct eis_touchscreen *touchscreen,
}
if (device->state == EIS_DEVICE_STATE_EMULATING) {
size_t first_available;
if (find_touch(device, touchid, &first_available)) {
return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
"Touch down event for duplicated touch ID");
}
if (first_available >= EIS_MAX_TOUCHES)
return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_ERROR,
"Too many simultaneous touch events");
device->touch_state.down[first_available] = touchid;
eis_queue_touch_down_event(device, touchid, x, y);
return NULL;
}
@ -676,13 +717,26 @@ client_msg_touch_motion(struct eis_touchscreen *touchscreen,
}
if (device->state == EIS_DEVICE_STATE_EMULATING) {
eis_queue_touch_motion_event(device, touchid, x, y);
/* Silently ignore motion for non-existing touches */
if (find_touch(device, touchid, NULL))
eis_queue_touch_motion_event(device, touchid, x, y);
return NULL;
}
return maybe_error_on_device_state(device, "touch motion");
}
static bool
release_touch(struct eis_device *device, uint32_t touchid)
{
size_t index;
bool rc = find_touch(device, touchid, &index);
if (rc)
device->touch_state.down[index] = UINT64_MAX;
return rc;
}
static struct brei_result *
client_msg_touch_up(struct eis_touchscreen *touchscreen, uint32_t touchid)
{
@ -696,7 +750,8 @@ client_msg_touch_up(struct eis_touchscreen *touchscreen, uint32_t touchid)
}
if (device->state == EIS_DEVICE_STATE_EMULATING) {
eis_queue_touch_up_event(device, touchid);
if (release_touch(device, touchid))
eis_queue_touch_up_event(device, touchid);
return NULL;
}
@ -722,7 +777,8 @@ client_msg_touch_cancel(struct eis_touchscreen *touchscreen, uint32_t touchid)
}
if (device->state == EIS_DEVICE_STATE_EMULATING) {
eis_queue_touch_cancel_event(device, touchid);
if (release_touch(device, touchid))
eis_queue_touch_cancel_event(device, touchid);
return NULL;
}
@ -776,6 +832,10 @@ eis_seat_new_device(struct eis_seat *seat)
list_append(&seat->devices, &device->link);
for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) {
device->touch_state.down[i] = UINT64_MAX;
}
return eis_device_ref(device);
}
@ -1463,6 +1523,10 @@ eis_device_pause(struct eis_device *device)
eis_device_event_paused(device, eis_client_get_next_serial(client));
memset(device->key_button_state.down, 0, sizeof(device->key_button_state.down));
for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) {
device->touch_state.down[i] = UINT64_MAX;
}
}
_public_ void

View file

@ -34,6 +34,8 @@
#define KEY_MAX 0x2ffU
#define KEY_CNT (KEY_MAX + 1)
#define EIS_MAX_TOUCHES 16
enum eis_device_state {
EIS_DEVICE_STATE_NEW,
EIS_DEVICE_STATE_AWAITING_READY,
@ -82,6 +84,10 @@ struct eis_device {
struct {
unsigned char down[NCHARS(KEY_CNT)];
} key_button_state;
struct {
uint64_t down[EIS_MAX_TOUCHES]; /* touch id */
} touch_state;
};
struct eis_touch {

View file

@ -1261,3 +1261,90 @@ class TestEiProtocol:
else:
ei.callback_roundtrip()
assert status.disconnected is False
def test_touch_disconnect_on_duplicate_id(self, eis):
"""
Ensure EIS disconnects us when we use a duplicate touch id
"""
ei = eis.ei
@dataclass
class Status:
device: EiDevice = None
touchscreen: Optional[EiTouchscreen] = None
disconnected: bool = False
resumed: bool = False
serial: int = 0
status = Status()
def on_interface(device, object, name, version, new_objects):
logger.debug(
"new capability",
device=device,
object=object,
name=name,
version=version,
)
if name == InterfaceName.EI_TOUCHSCREEN:
assert status.touchscreen is None
status.touchscreen = new_objects["object"]
def on_device_resumed(device, serial):
status.resumed = True
status.serial = serial
def on_new_device(seat, device, version, new_objects):
logger.debug("new device", object=new_objects["device"])
status.device = new_objects["device"]
status.device.connect("Interface", on_interface)
status.device.connect("Resumed", on_device_resumed)
def on_new_object(o: Interface):
logger.debug("new object", object=o)
if o.name == InterfaceName.EI_SEAT:
ei.seat_fill_capability_masks(o)
o.connect("Device", on_new_device)
ei.context.connect("register", on_new_object)
ei.dispatch()
def on_disconnected(connection, last_serial, reason, explanation):
status.disconnected = True
def on_connection(setup, serial, id, version, new_objects={}):
connection = new_objects["connection"]
connection.connect("Disconnected", on_disconnected)
setup = ei.handshake
setup.connect("Connection", on_connection)
ei.init_default_sender_connection(interface_versions={"ei_touchscreen": 1})
ei.wait_for_seat()
seat = ei.seats[0]
ei.send(seat.Bind(seat.bind_mask([InterfaceName.EI_TOUCHSCREEN])))
ei.wait_for(lambda: status.touchscreen and status.resumed)
assert status.touchscreen is not None
ei.send(status.device.StartEmulating(status.serial, 123))
logger.debug("Sending touch events")
touchid = 1
touchscreen = status.touchscreen
device = status.device
ei.send(touchscreen.Down(touchid, 10, 20))
ei.send(device.Frame(status.serial, int(time.time())))
ei.send(touchscreen.Motion(touchid, 10, 25))
ei.send(device.Frame(status.serial, int(time.time())))
ei.send(touchscreen.Down(touchid, 10, 20))
try:
ei.send(device.Frame(status.serial, int(time.time())))
except BrokenPipeError:
pass
ei.dispatch()
ei.wait_for(lambda: status.disconnected)
assert status.disconnected is True