From 9e9de6c9cda56e20feb299607df2e3755ebf7613 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Mon, 29 Jul 2024 15:27:01 -0400 Subject: [PATCH 01/12] connection: Use bool, not int, for a boolean variable No functional change intended. Signed-off-by: Demi Marie Obenour --- src/connection.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connection.c b/src/connection.c index e1b751ac..330e45c2 100644 --- a/src/connection.c +++ b/src/connection.c @@ -413,7 +413,7 @@ decode_cmsg(struct wl_ring_buffer *buffer, struct msghdr *msg) { struct cmsghdr *cmsg; size_t size, i; - int overflow = 0; + bool overflow = false; for (cmsg = CMSG_FIRSTHDR(msg); cmsg != NULL; cmsg = CMSG_NXTHDR(msg, cmsg)) { @@ -424,7 +424,7 @@ decode_cmsg(struct wl_ring_buffer *buffer, struct msghdr *msg) size = cmsg->cmsg_len - CMSG_LEN(0); if (ring_buffer_ensure_space(buffer, size) < 0 || overflow) { - overflow = 1; + overflow = true; size /= sizeof(int32_t); for (i = 0; i < size; i++) close(((int*)CMSG_DATA(cmsg))[i]); From f00586ee5f47b7c7fa8d92f01f61d1ba593c142d Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Mon, 29 Jul 2024 15:27:10 -0400 Subject: [PATCH 02/12] connection: Add comments explaining safety No functional change intended. Signed-off-by: Demi Marie Obenour --- src/connection.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/connection.c b/src/connection.c index 330e45c2..300c28d8 100644 --- a/src/connection.c +++ b/src/connection.c @@ -1052,7 +1052,10 @@ wl_connection_demarshal(struct wl_connection *connection, goto err; } + /* This ring buffer will always have a multiple of sizeof(int) + * bytes in it. */ ring_buffer_copy(&connection->fds_in, &fd, sizeof fd); + /* This can wrap but that is okay. */ connection->fds_in.tail += sizeof fd; closure->args[i].h = fd; break; From 568a9325f080b9772ef318ae777eaedb3040b248 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Mon, 29 Jul 2024 15:27:21 -0400 Subject: [PATCH 03/12] connection: empty iovecs are never created Neither ring_buffer_put_iov() nor ring_buffer_get_iov() distinguishes between a full wl_ring_buffer and an empty one. Instead, both just assume that the returned iovec should not be empty. However, both have only one caller, and that caller does guarantee that the ring buffer is not full (for ring_buffer_put_iov()) or empty (for ring_buffer_get_iov()). Therefore, the code is safe. Signed-off-by: Demi Marie Obenour --- src/connection.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/connection.c b/src/connection.c index 300c28d8..41410e7b 100644 --- a/src/connection.c +++ b/src/connection.c @@ -113,6 +113,7 @@ ring_buffer_put(struct wl_ring_buffer *b, const void *data, size_t count) return 0; } +/* Precondition: the buffer is not full */ static void ring_buffer_put_iov(struct wl_ring_buffer *b, struct iovec *iov, int *count) { @@ -125,10 +126,13 @@ ring_buffer_put_iov(struct wl_ring_buffer *b, struct iovec *iov, int *count) iov[0].iov_len = tail - head; *count = 1; } else if (tail == 0) { + /* We assume that head == 0 means an empty buffer, not a full one. */ iov[0].iov_base = b->data + head; iov[0].iov_len = ring_buffer_capacity(b) - head; *count = 1; } else { + /* head == 0 is checked earlier, so there is at least one byte to + * read after head. */ iov[0].iov_base = b->data + head; iov[0].iov_len = ring_buffer_capacity(b) - head; iov[1].iov_base = b->data; @@ -137,6 +141,7 @@ ring_buffer_put_iov(struct wl_ring_buffer *b, struct iovec *iov, int *count) } } +/* Precondition: the buffer is not empty */ static void ring_buffer_get_iov(struct wl_ring_buffer *b, struct iovec *iov, int *count) { @@ -161,6 +166,7 @@ ring_buffer_get_iov(struct wl_ring_buffer *b, struct iovec *iov, int *count) } } +/* Precondition: the data will not overflow the buffer */ static void ring_buffer_copy(struct wl_ring_buffer *b, void *data, size_t count) { @@ -456,6 +462,9 @@ wl_connection_flush(struct wl_connection *connection) tail = connection->out.tail; while (ring_buffer_size(&connection->out) > 0) { + /* Ring buffer is not empty, so this is safe. */ + ring_buffer_get_iov(&connection->out, iov, &count); + build_cmsg(&connection->fds_out, cmsg, &clen); if (clen >= CLEN) { @@ -530,6 +539,7 @@ wl_connection_read(struct wl_connection *connection) if (ring_buffer_ensure_space(&connection->in, 1) < 0) return -1; + /* Ring buffer is not full, so this is safe. */ ring_buffer_put_iov(&connection->in, iov, &count); msg.msg_name = NULL; From ac630dd3b465742a10eb2518b1aeb8c19fb96b2b Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Fri, 9 Aug 2024 17:56:35 -0400 Subject: [PATCH 04/12] connection: More explanations for why the code is safe This adds many assertions to check that buffers are valid. Signed-off-by: Demi Marie Obenour --- src/connection.c | 146 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 22 deletions(-) diff --git a/src/connection.c b/src/connection.c index 41410e7b..1be14964 100644 --- a/src/connection.c +++ b/src/connection.c @@ -91,19 +91,45 @@ ring_buffer_mask(const struct wl_ring_buffer *b, size_t i) { return i & m; } +static size_t +ring_buffer_size(struct wl_ring_buffer *b) +{ + return b->head - b->tail; +} + +/* Precondition: the data will not overflow the buffer */ static int ring_buffer_put(struct wl_ring_buffer *b, const void *data, size_t count) { - size_t head, size; + size_t head, size, buffer_size, capacity; + + if (b->head < b->tail) { + wl_abort("ring_buffer_put: ring buffer corrupt, %zu < %zu\n", + b->head, b->tail); + } + + capacity = ring_buffer_capacity(b); + buffer_size = ring_buffer_size(b); + if (buffer_size > capacity) { + wl_abort("ring_buffer_put: ring buffer corrupt: " + "%zu - %zu > %zu\n", b->head, b->tail, capacity); + } if (count == 0) return 0; + if (capacity - buffer_size < count) { + wl_abort("ring_buffer_put: attempt to overfill buffer: " + "%zu - %zu < %zu\n", capacity, buffer_size, count); + } + head = ring_buffer_mask(b, b->head); - if (head + count <= ring_buffer_capacity(b)) { + size = capacity - head; + if (count <= size) { + /* Enough space after head to fulfill request */ memcpy(b->data + head, data, count); } else { - size = ring_buffer_capacity(b) - head; + /* Need to wrap around */ memcpy(b->data + head, data, size); memcpy(b->data, (const char *) data + size, count - size); } @@ -117,24 +143,54 @@ ring_buffer_put(struct wl_ring_buffer *b, const void *data, size_t count) static void ring_buffer_put_iov(struct wl_ring_buffer *b, struct iovec *iov, int *count) { - size_t head, tail; + size_t head, tail, size, capacity; + + if (b->head < b->tail) { + wl_abort("ring_buffer_put_iov: ring buffer corrupt, %zu < %zu\n", + b->head, b->tail); + } + + size = ring_buffer_size(b); + capacity = ring_buffer_capacity(b); + if (size >= capacity) { + wl_abort("ring_buffer_put_iov: ring buffer full or corrupt: " + "%zu - %zu >= %zu\n", b->head, b->tail, capacity); + } head = ring_buffer_mask(b, b->head); tail = ring_buffer_mask(b, b->tail); if (head < tail) { + /* Buffer is like this: + * head tail + * | | + * +---------+-----------------+---------+ + * | VALID | INVALID | VALID | + * +---------+-----------------+---------+ + */ iov[0].iov_base = b->data + head; iov[0].iov_len = tail - head; *count = 1; } else if (tail == 0) { - /* We assume that head == 0 means an empty buffer, not a full one. */ + /* Buffer is like this: + * tail head + * | | + * +---------------------------+---------+ + * | VALID | INVALID | + * +---------------------------+---------+ + */ iov[0].iov_base = b->data + head; - iov[0].iov_len = ring_buffer_capacity(b) - head; + iov[0].iov_len = capacity - head; *count = 1; } else { - /* head == 0 is checked earlier, so there is at least one byte to - * read after head. */ + /* Buffer is like this: + * tail head + * | | + * +---------------------------+---------+ + * | INVALID | VALID | INVALID | + * +---------------------------+---------+ + */ iov[0].iov_base = b->data + head; - iov[0].iov_len = ring_buffer_capacity(b) - head; + iov[0].iov_len = capacity - head; iov[1].iov_base = b->data; iov[1].iov_len = tail; *count = 2; @@ -145,52 +201,98 @@ ring_buffer_put_iov(struct wl_ring_buffer *b, struct iovec *iov, int *count) static void ring_buffer_get_iov(struct wl_ring_buffer *b, struct iovec *iov, int *count) { - size_t head, tail; + size_t head, tail, capacity; + + if (b->head <= b->tail) { + wl_abort("ring_buffer_get_iov(): empty or corrupt buffer: %zu <= %zu\n", + b->head, b->tail); + } + + capacity = ring_buffer_capacity(b); + if (ring_buffer_size(b) > capacity) { + wl_abort("ring_buffer_put_iov: ring buffer corrupt: " + "%zu - %zu > %zu\n", b->head, b->tail, capacity); + } head = ring_buffer_mask(b, b->head); tail = ring_buffer_mask(b, b->tail); if (tail < head) { + /* Buffer is like this: + * tail head + * | | + * +---------+-----------------+---------+ + * | INVALID | VALID | INVALID | + * +---------+-----------------+---------+ + */ iov[0].iov_base = b->data + tail; iov[0].iov_len = head - tail; *count = 1; } else if (head == 0) { + /* Buffer is like this: + * head tail + * | | + * +---------------------------+---------+ + * | INVALID | VALID | + * +---------------------------+---------+ + */ iov[0].iov_base = b->data + tail; - iov[0].iov_len = ring_buffer_capacity(b) - tail; + iov[0].iov_len = capacity - tail; *count = 1; } else { + /* Buffer is like this: + * head tail + * | | + * +-------+-------------------+---------+ + * | VALID | INVALID | VALID | + * +---------------------------+---------+ + */ iov[0].iov_base = b->data + tail; - iov[0].iov_len = ring_buffer_capacity(b) - tail; + iov[0].iov_len = capacity - tail; iov[1].iov_base = b->data; iov[1].iov_len = head; *count = 2; } } -/* Precondition: the data will not overflow the buffer */ +/* Precondition: the data will not underflow the buffer */ static void ring_buffer_copy(struct wl_ring_buffer *b, void *data, size_t count) { - size_t tail, size; + size_t tail, size, buffer_size, capacity; + + if (b->head < b->tail) { + wl_abort("ring_buffer_copy(): ring buffer corrupt, %zu < %zu\n", + b->head, b->tail); + } + + buffer_size = ring_buffer_size(b); + capacity = ring_buffer_capacity(b); + if (buffer_size > capacity) { + wl_abort("ring_buffer_copy(): ring buffer corrupt: " + "%zu - %zu > %zu\n", b->head, b->tail, capacity); + } if (count == 0) return; + if (buffer_size < count) { + wl_abort("ring_buffer_copy(): attempt to copy %zu bytes " + "but buffer has %zu bytes\n", + count, buffer_size); + } + tail = ring_buffer_mask(b, b->tail); - if (tail + count <= ring_buffer_capacity(b)) { + size = capacity - tail; + if (count <= size) { + /* Enough data after the tail to fulfill the request */ memcpy(data, b->data + tail, count); } else { - size = ring_buffer_capacity(b) - tail; + /* Must wrap buffer around */ memcpy(data, b->data + tail, size); memcpy((char *) data + size, b->data, count - size); } } -static size_t -ring_buffer_size(struct wl_ring_buffer *b) -{ - return b->head - b->tail; -} - static char * ring_buffer_tail(const struct wl_ring_buffer *b) { From 2bd88ca4bfa0c37e35a98bae1d1cc95d6ad42109 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Mon, 29 Jul 2024 15:27:26 -0400 Subject: [PATCH 05/12] connection: Document correct use of atoi() atoi() has undefined behavior on invalid input, but here the input comes from wayland-scanner, which is trusted. Signed-off-by: Demi Marie Obenour --- src/connection.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/connection.c b/src/connection.c index 1be14964..c322084c 100644 --- a/src/connection.c +++ b/src/connection.c @@ -795,6 +795,7 @@ wl_message_get_since(const struct wl_message *message) { int since; + /* This is trusted input */ since = atoi(message->signature); if (since == 0) From bd0aa37eb9e0e81619cb71bd97e66354ec64de8a Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Mon, 29 Jul 2024 15:27:30 -0400 Subject: [PATCH 06/12] server: Make wl_resource_post_no_memory() a wrapper function Save some code size. Signed-off-by: Demi Marie Obenour --- src/wayland-server.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wayland-server.c b/src/wayland-server.c index 1d6be3ec..fb68f2a3 100644 --- a/src/wayland-server.c +++ b/src/wayland-server.c @@ -716,8 +716,7 @@ wl_client_post_implementation_error(struct wl_client *client, WL_EXPORT void wl_resource_post_no_memory(struct wl_resource *resource) { - wl_resource_post_error(resource->client->display_resource, - WL_DISPLAY_ERROR_NO_MEMORY, "no memory"); + wl_client_post_no_memory(resource->client); } /** Detect if a wl_resource uses the deprecated public definition. From 3ea4b30700d691ab8d4d944e5a5606b0af7f41f5 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sun, 11 Aug 2024 19:23:21 -0400 Subject: [PATCH 07/12] connection: check for NULL string only once This removes a redundant check and combines two others. Signed-off-by: Demi Marie Obenour --- src/connection.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/connection.c b/src/connection.c index c322084c..4ba93092 100644 --- a/src/connection.c +++ b/src/connection.c @@ -1063,14 +1063,14 @@ wl_connection_demarshal(struct wl_connection *connection, case WL_ARG_STRING: length = *p++; - if (length == 0 && !arg.nullable) { - wl_log("NULL string received on non-nullable " - "type, message %s(%s)\n", message->name, - message->signature); - errno = EINVAL; - goto err; - } if (length == 0) { + if (!arg.nullable) { + wl_log("NULL string received on non-nullable " + "type, message %s(%s)\n", message->name, + message->signature); + errno = EINVAL; + goto err; + } closure->args[i].s = NULL; break; } @@ -1088,7 +1088,7 @@ wl_connection_demarshal(struct wl_connection *connection, s = (char *) p; - if (length > 0 && s[length - 1] != '\0') { + if (s[length - 1] != '\0') { wl_log("string not nul-terminated, " "message %s(%s)\n", message->name, message->signature); From cd0d1543c093d079ecf3444a16a4dcb02007a62a Mon Sep 17 00:00:00 2001 From: ykla Date: Sun, 20 Jul 2025 02:09:35 +0000 Subject: [PATCH 08/12] ci: upgrade FreeBSD to 14.3 Signed-off-by: ykla yklaxds@gmail.com --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 26ceae1f..eeed7899 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,7 +43,7 @@ include: # API changes. If you need new features from ci-templates you must bump # this to the current SHA you require from the ci-templates repo, however # be aware that you may need to account for API changes when doing so. - ref: 32afe5644697e503af18a736587c8619fa036a72 + ref: 48c2c583a865bd59be21e8938df247faf460099c file: - '/templates/debian.yml' - '/templates/freebsd.yml' @@ -306,11 +306,11 @@ armv7-release-debian-build: .os-freebsd: variables: BUILD_OS: freebsd - FDO_DISTRIBUTION_VERSION: "14.2" + FDO_DISTRIBUTION_VERSION: "14.3" FDO_DISTRIBUTION_PACKAGES: 'libxslt meson ninja pkgconf expat libffi libepoll-shim libxml2' # bump this tag every time you change something which requires rebuilding the # base image - FDO_DISTRIBUTION_TAG: "2025-06-23.1" + FDO_DISTRIBUTION_TAG: "2025-07-20.0" # Don't build documentation since installing the required tools massively # increases the VM image (and therefore container) size. MESON_ARGS: "--fatal-meson-warnings -Dwerror=true -Ddocumentation=false" From 264da6a92b48ef41661021236c5d51ca52722309 Mon Sep 17 00:00:00 2001 From: YaoBing Xiao Date: Wed, 16 Jul 2025 22:29:40 +0800 Subject: [PATCH 09/12] cursor: Free theme when size check fails to avoid memory leak Signed-off-by: YaoBing Xiao --- cursor/wayland-cursor.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cursor/wayland-cursor.c b/cursor/wayland-cursor.c index 89ecc9a1..2e21db73 100644 --- a/cursor/wayland-cursor.c +++ b/cursor/wayland-cursor.c @@ -398,7 +398,7 @@ wl_cursor_theme_load(const char *name, int size, struct wl_shm *shm) return NULL; if (size < 0 || (size > 0 && INT_MAX / size / 4 < size)) - return NULL; + goto err; if (!name) name = "default"; @@ -409,7 +409,7 @@ wl_cursor_theme_load(const char *name, int size, struct wl_shm *shm) theme->pool = shm_pool_create(shm, size * size * 4); if (!theme->pool) - goto out_error_pool; + goto err; xcursor_load_theme(name, size, load_callback, theme); @@ -421,7 +421,7 @@ wl_cursor_theme_load(const char *name, int size, struct wl_shm *shm) return theme; -out_error_pool: +err: free(theme); return NULL; } From 77730f10a0eaac1c654d1bdc689783292bdb5f2d Mon Sep 17 00:00:00 2001 From: Kyle Brenneman Date: Tue, 17 Sep 2024 17:27:37 -0600 Subject: [PATCH 10/12] connection: Add a function to parse WAYLAND_DEBUG tokens Add a new function, wl_check_env_token, to scan for a token in a comma-separated string. Change wl_display_create in wayland-server.c and wl_display_connect_to_fd in wayland-client.c to use that instead of a simple substring search. This means that WAYLAND_DEBUG will accept a value like "client,server" but not "clientserver". But, this will make it easier to add other tokens without worrying about overlap between them. Signed-off-by: Kyle Brenneman --- src/connection.c | 42 ++++++++++++++++++++++++++++++++++++++++++ src/wayland-client.c | 2 +- src/wayland-private.h | 3 +++ src/wayland-server.c | 2 +- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/connection.c b/src/connection.c index 593f52f3..9c6a6b01 100644 --- a/src/connection.c +++ b/src/connection.c @@ -1491,6 +1491,48 @@ wl_closure_queue(struct wl_closure *closure, struct wl_connection *connection) return result; } +bool +wl_check_env_token(const char *env, const char *token) +{ + const char *ptr = env; + size_t token_len; + + if (env == NULL) + return false; + + token_len = strlen(token); + + // Scan the string for comma-separated tokens and look for a match. + while (true) { + const char *end; + size_t len; + + // Skip over any leading separators. + while (*ptr == ',') + ptr++; + + if (*ptr == '\x00') + return false; + + end = strchr(ptr + 1, ','); + + // If there isn't another separarator, then the rest of the string + // is one token. + if (end == NULL) + return (strcmp(ptr, token) == 0); + + len = end - ptr; + if (len == token_len && memcmp(ptr, token, len) == 0) { + return true; + } + + // Skip to the next token. + ptr += len; + } + + return false; +} + void wl_closure_print(struct wl_closure *closure, struct wl_object *target, int send, int discarded, uint32_t (*n_parse)(union wl_argument *arg), diff --git a/src/wayland-client.c b/src/wayland-client.c index c8633046..c0b361f0 100644 --- a/src/wayland-client.c +++ b/src/wayland-client.c @@ -1236,7 +1236,7 @@ wl_display_connect_to_fd(int fd) no_color = getenv("NO_COLOR"); force_color = getenv("FORCE_COLOR"); debug = getenv("WAYLAND_DEBUG"); - if (debug && (strstr(debug, "client") || strstr(debug, "1"))) { + if (debug && (wl_check_env_token(debug, "client") || wl_check_env_token(debug, "1"))) { debug_client = 1; if (isatty(fileno(stderr))) debug_color = 1; diff --git a/src/wayland-private.h b/src/wayland-private.h index d7ba9dae..d0e4cfc6 100644 --- a/src/wayland-private.h +++ b/src/wayland-private.h @@ -237,6 +237,9 @@ wl_closure_send(struct wl_closure *closure, struct wl_connection *connection); int wl_closure_queue(struct wl_closure *closure, struct wl_connection *connection); +bool +wl_check_env_token(const char *env, const char *token); + void wl_closure_print(struct wl_closure *closure, struct wl_object *target, int send, int discarded, diff --git a/src/wayland-server.c b/src/wayland-server.c index 482743b3..c81d98f1 100644 --- a/src/wayland-server.c +++ b/src/wayland-server.c @@ -1198,7 +1198,7 @@ wl_display_create(void) no_color = getenv("NO_COLOR"); force_color = getenv("FORCE_COLOR"); debug = getenv("WAYLAND_DEBUG"); - if (debug && (strstr(debug, "server") || strstr(debug, "1"))) { + if (debug && (wl_check_env_token(debug, "server") || wl_check_env_token(debug, "1"))) { debug_server = 1; if (isatty(fileno(stderr))) debug_color = 1; From 4673ef7e9ce5de21051b64c39816a98187611966 Mon Sep 17 00:00:00 2001 From: Kyle Brenneman Date: Tue, 10 Sep 2024 14:36:06 -0600 Subject: [PATCH 11/12] connection: Add a thread ID to WAYLAND_DEBUG output. If WAYLAND_DEBUG contains the token "thread_id", and gettid() is available, then include the current thread ID in the output from wl_closure_print. If multiple threads are sending requests, then those requests can get interleaved. That's usually fine, but for wl_surface requests and commits, that can cause problems ranging from incorrect behavior to protocol errors. Being able to see which requests are sent by different threads would make such problems much easier to diagnose. Signed-off-by: Kyle Brenneman --- meson.build | 1 + src/connection.c | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/meson.build b/meson.build index 37c14687..ce386a4c 100644 --- a/meson.build +++ b/meson.build @@ -46,6 +46,7 @@ have_funcs = [ 'memfd_create', 'mremap', 'strndup', + 'gettid', ] foreach f: have_funcs config_h.set('HAVE_' + f.underscorify().to_upper(), cc.has_function(f)) diff --git a/src/connection.c b/src/connection.c index 9c6a6b01..2d1e8d1d 100644 --- a/src/connection.c +++ b/src/connection.c @@ -26,6 +26,8 @@ #define _GNU_SOURCE +#include "../config.h" + #include #include #include @@ -1538,6 +1540,9 @@ wl_closure_print(struct wl_closure *closure, struct wl_object *target, int send, int discarded, uint32_t (*n_parse)(union wl_argument *arg), const char *queue_name, int color) { +#if defined(HAVE_GETTID) + static int include_tid = -1; +#endif // defined(HAVE_GETTID) int i; struct argument_details arg; const char *signature = closure->message->signature; @@ -1558,6 +1563,18 @@ wl_closure_print(struct wl_closure *closure, struct wl_object *target, color ? WL_DEBUG_COLOR_GREEN : "", time / 1000, time % 1000); +#if defined(HAVE_GETTID) + if (include_tid < 0) { + include_tid = wl_check_env_token(getenv("WAYLAND_DEBUG"), "thread_id"); + } + + if (include_tid) { + fprintf(f, "%sTID#%d ", + color ? WL_DEBUG_COLOR_CYAN : "", + (int) gettid()); + } +#endif + if (queue_name) { fprintf(f, "%s{%s} ", color ? WL_DEBUG_COLOR_YELLOW : "", From d81525a235e48cc5de3e4005a16ddb1fbdfd9d7c Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Wed, 2 Jul 2025 12:15:33 +0200 Subject: [PATCH 12/12] client: add wl_display_dispatch_pending_single As well as wl_display_dispatch_queue_pending_single. The motivation is writing libwayland bindings for a dynamic language with exceptions/non-local returns. Since it is invalid for a wl_dispatcher_func_t callback provided to libwayland to not return, there is no way to prevent dispatching of further events in the case of an exception in the dynamic language event handler. Furthermore, since creating/destroying Wayland objects in an event handler affects the dispatching of subsequent events by libwayland, it is not possible to collect Wayland events in a queue outside libwayland and dispatch them one-by-one after wl_display_dispatch_pending() returns. Adding libwayland API to dispatch at most one pending event solves this problem cleanly. The bindings can have libwayland dispatch a single event, wait for wl_display_dispatch_pending_single() to return, run the dynamic language event handler (which may longjmp away), and continue the loop for as long as there are more events to dispatch. References: https://codeberg.org/ifreund/janet-wayland Signed-off-by: Isaac Freund --- src/wayland-client-core.h | 7 ++++ src/wayland-client.c | 75 +++++++++++++++++++++++++++++++++++++++ tests/display-test.c | 69 +++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+) diff --git a/src/wayland-client-core.h b/src/wayland-client-core.h index 970e6254..e0523e49 100644 --- a/src/wayland-client-core.h +++ b/src/wayland-client-core.h @@ -268,9 +268,16 @@ int wl_display_dispatch_queue_pending(struct wl_display *display, struct wl_event_queue *queue); +int +wl_display_dispatch_queue_pending_single(struct wl_display *display, + struct wl_event_queue *queue); + int wl_display_dispatch_pending(struct wl_display *display); +int +wl_display_dispatch_pending_single(struct wl_display *display); + int wl_display_get_error(struct wl_display *display); diff --git a/src/wayland-client.c b/src/wayland-client.c index c0b361f0..ed686b5c 100644 --- a/src/wayland-client.c +++ b/src/wayland-client.c @@ -1882,6 +1882,34 @@ err: return -1; } + +static int +dispatch_queue_single(struct wl_display *display, struct wl_event_queue *queue) +{ + if (display->last_error) + goto err; + + while (!wl_list_empty(&display->display_queue.event_list)) { + dispatch_event(display, &display->display_queue); + if (display->last_error) + goto err; + } + + if (!wl_list_empty(&queue->event_list)) { + dispatch_event(display, queue); + if (display->last_error) + goto err; + return 1; + } else { + return 0; + } + +err: + errno = display->last_error; + + return -1; +} + /** Prepare to read events from the display's file descriptor to a queue * * \param display The display context object @@ -2212,6 +2240,34 @@ wl_display_dispatch_queue_pending(struct wl_display *display, return ret; } +/** Dispatch at most one pending event in an event queue + * + * \param display The display context object + * \param queue The event queue to dispatch + * \return The number of dispatched events (0 or 1) on success or -1 on failure + * + * Dispatch at most one pending event for objects assigned to the given + * event queue. On failure -1 is returned and errno set appropriately. + * If there are no events queued, this function returns immediately. + * + * \memberof wl_display + * \since 1.25.0 + */ +WL_EXPORT int +wl_display_dispatch_queue_pending_single(struct wl_display *display, + struct wl_event_queue *queue) +{ + int ret; + + pthread_mutex_lock(&display->mutex); + + ret = dispatch_queue_single(display, queue); + + pthread_mutex_unlock(&display->mutex); + + return ret; +} + /** Process incoming events * * \param display The display context object @@ -2272,6 +2328,25 @@ wl_display_dispatch_pending(struct wl_display *display) &display->default_queue); } +/** Dispatch at most one pending event in the default event queue. + * + * \param display The display context object + * \return The number of dispatched events (0 or 1) on success or -1 on failure + * + * Dispatch at most one pending event for objects assigned to the default + * event queue. On failure -1 is returned and errno set appropriately. + * If there are no events queued, this function returns immediately. + * + * \memberof wl_display + * \since 1.25.0 + */ +WL_EXPORT int +wl_display_dispatch_pending_single(struct wl_display *display) +{ + return wl_display_dispatch_queue_pending_single(display, + &display->default_queue); +} + /** Retrieve the last error that occurred on a display * * \param display The display context object diff --git a/tests/display-test.c b/tests/display-test.c index 89606c73..fe78b521 100644 --- a/tests/display-test.c +++ b/tests/display-test.c @@ -1695,6 +1695,75 @@ TEST(global_remove) display_destroy(d); } +static void +dispatch_single_read_events(struct wl_display *d) +{ + if (wl_display_prepare_read(d) < 0) { + return; + } + + int ret = 0; + do { + ret = wl_display_flush(d); + } while (ret < 0 && (errno == EINTR || errno == EAGAIN)); + assert(ret >= 0); + + struct pollfd pfd[1]; + pfd[0].fd = wl_display_get_fd(d); + pfd[0].events = POLLIN; + + do { + ret = poll(pfd, 1, -1); + } while (ret < 0 && errno == EINTR); + assert(ret > 0); + + wl_display_read_events(d); +} + +static void +dispatch_single_client(void) +{ + struct client *c = client_connect(); + + assert(wl_display_dispatch_pending_single(c->wl_display) == 0); + + struct wl_registry *registry = wl_display_get_registry(c->wl_display); + + dispatch_single_read_events(c->wl_display); + + // [1815110.061] {Default Queue} wl_registry#3.global(1, "test", 1) + assert(wl_display_dispatch_pending_single(c->wl_display) == 1); + + dispatch_single_read_events(c->wl_display); + + // [1815110.067] {Default Queue} wl_registry#3.global(2, "wl_seat", 1) + assert(wl_display_dispatch_pending_single(c->wl_display) == 1); + + // No more events + assert(wl_display_dispatch_pending_single(c->wl_display) == 0); + + wl_registry_destroy(registry); + + client_disconnect(c); +} + +TEST(dispatch_single) +{ + struct display *d = display_create(); + + struct wl_global *global = wl_global_create(d->wl_display, + &wl_seat_interface, + 1, d, bind_seat); + + client_create_noarg(d, dispatch_single_client); + + display_run(d); + + wl_global_destroy(global); + + display_destroy(d); +} + static void terminate_display(void *arg) {