From 5f2eae2a1e1b2e11025de9f158205cd6743a842d Mon Sep 17 00:00:00 2001 From: Thomas GRIM Date: Sun, 3 May 2026 17:53:26 +0200 Subject: [PATCH 1/3] When handling a cursor change event with an empty cursor_name (Chromium), try matching on the cursor's bitmap image before falling back to the default cursor --- gui-agent/Makefile | 2 +- gui-agent/vmside.c | 80 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/gui-agent/Makefile b/gui-agent/Makefile index 7d270b66..4b7b731a 100644 --- a/gui-agent/Makefile +++ b/gui-agent/Makefile @@ -25,7 +25,7 @@ CFLAGS += -I../include/ `pkg-config --cflags vchan` \ -Wmissing-prototypes -Wstrict-prototypes -Wold-style-declaration \ -Wold-style-definition OBJS = vmside.o txrx-vchan.o error.o list.o encoding.o -LIBS = -lX11 -lXdamage -lXcomposite -lXfixes `pkg-config --libs vchan` -lqubesdb \ +LIBS = -lX11 -lXdamage -lXcomposite -lXcursor -lXfixes `pkg-config --libs vchan` -lqubesdb \ -lunistring diff --git a/gui-agent/vmside.c b/gui-agent/vmside.c index fbf7b091..e6a77ec1 100644 --- a/gui-agent/vmside.c +++ b/gui-agent/vmside.c @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include "xdriver-shm-cmd.h" @@ -420,6 +421,76 @@ static uint32_t find_cursor(Ghandles *g, Atom atom) return CURSOR_DEFAULT; } +/* + * Some applications don't set cursor names properly when sending XfixesDisplayCursorNotify events, notably Chromium and derivatives. + * Before falling back to CURSOR_DEFAULT, we'll try to match (quick hashes of) the live cursor's bitmap with each supported cursor's bitmap. + * TODO: Precompute the hashes of supported_cursors once during init to avoid redundant computations everytime a cursor changes (this might not be trivial). +**/ + +// Generic Fowler-Noll-Vo quick hash function (FNV-1a), magic mix number from WikiPedia's entry +static uint64_t fnv1a64(const void *data, size_t len, uint64_t seed) { + const uint8_t *p = data; + uint64_t hash = seed; + for (size_t i = 0; i < len; i++) { + hash ^= p[i]; + hash *= 1099511628211ULL; + } + + return hash; +} + +// Specialized FNV-1a hash of a cursor, magic seed number from WikiPedia's entry +static uint64_t hash_cursor(uint32_t w, uint32_t h, + uint32_t xhot, uint32_t yhot, + const uint32_t *pixels) { + uint64_t hash = 14695981039346656037ULL; + + uint32_t hdr[4] = { w, h, xhot, yhot }; + hash = fnv1a64(hdr, sizeof(hdr), hash); + hash = fnv1a64(pixels, (size_t)(w * h * sizeof(uint32_t)), hash); + + return hash; +} + +// Fallback function to lookup an unnamed cursor by its bitmap +static uint32_t find_cursor_by_image(Ghandles *g) { + XFixesCursorImage *img = XFixesGetCursorImage(g->display); + if (!img) return CURSOR_DEFAULT; + + /* Narrow unsigned long pixels to uint32_t */ + size_t npx = (size_t)img->width * img->height; + uint32_t *live_px = malloc(npx * sizeof(uint32_t)); + if (!live_px) { + XFree(img); + return CURSOR_DEFAULT; + } + for (size_t i = 0; i < npx; i++) live_px[i] = (uint32_t)img->pixels[i]; + + uint64_t live_hash = hash_cursor(img->width, img->height, img->xhot, img->yhot, live_px); + free(live_px); + + /* Use the live cursor's own size to avoid potential discrepancies between root's and the user's themes */ + uint32_t size = (img->width > img->height) ? img->width : img->height; + XFree(img); + + char *theme = XcursorGetTheme(g->display); + for (size_t i = 0; i < NUM_SUPPORTED_CURSORS; i++) { + XcursorImage *img = XcursorLibraryLoadImage(supported_cursors[i].name, theme, size); + if (!img) continue; + + uint64_t hash = hash_cursor(img->width, img->height, img->xhot, img->yhot, img->pixels); + XcursorImageDestroy(img); + + if (hash == live_hash) { + uint32_t found = CURSOR_X11 + supported_cursors[i].cursor_id; + assert(found < CURSOR_X11_MAX); + return found; + } + } + + return CURSOR_DEFAULT; +} + static void process_xevent_cursor(Ghandles *g, XFixesCursorNotifyEvent *ev) { if (ev->subtype == XFixesDisplayCursorNotify) { @@ -430,7 +501,6 @@ static void process_xevent_cursor(Ghandles *g, XFixesCursorNotifyEvent *ev) int root_x, root_y, win_x, win_y; unsigned int mask; Bool ret; - int cursor; ret = XQueryPointer(g->display, ev->window, &root, &window_under_pointer, @@ -441,7 +511,13 @@ static void process_xevent_cursor(Ghandles *g, XFixesCursorNotifyEvent *ev) if (!lookup_window(g, windows_list, window_under_pointer, __func__)) return; - cursor = find_cursor(g, ev->cursor_name); + uint32_t cursor; + if (ev->cursor_name != None) { + cursor = find_cursor(g, ev->cursor_name); + } else { + cursor = find_cursor_by_image(g); + } + send_cursor(g, window_under_pointer, cursor); } } From 1b5a0e47b93e83b74c64f40e4449ac8ac749b4cb Mon Sep 17 00:00:00 2001 From: Thomas GRIM Date: Sun, 3 May 2026 19:28:30 +0200 Subject: [PATCH 2/3] Rename variables, check bounds, add missing deps, start signing commits --- archlinux/PKGBUILD.in | 1 + debian/control | 2 ++ gui-agent/vmside.c | 26 +++++++++++++++++--------- rpm_spec/gui-agent.spec.in | 1 + 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/archlinux/PKGBUILD.in b/archlinux/PKGBUILD.in index 465dc2a3..fc9b105d 100644 --- a/archlinux/PKGBUILD.in +++ b/archlinux/PKGBUILD.in @@ -22,6 +22,7 @@ makedepends=( xorg-util-macros libxcomposite libxt + libxcursor libxdamage libunistring pixman diff --git a/debian/control b/debian/control index fba36d9b..6cf1995c 100644 --- a/debian/control +++ b/debian/control @@ -14,6 +14,7 @@ Build-Depends: libx11-dev, libgbm-dev, libxcomposite-dev, + libxcursor-dev, libxdamage-dev, libxfixes-dev, x11proto-xf86dga-dev, @@ -42,6 +43,7 @@ Depends: qubesdb-vm (>= 4.1.4), libxdamage1, libxcomposite1, + libxcursor1, libxfixes3, libxt6, libx11-6, diff --git a/gui-agent/vmside.c b/gui-agent/vmside.c index e6a77ec1..dc794844 100644 --- a/gui-agent/vmside.c +++ b/gui-agent/vmside.c @@ -454,24 +454,29 @@ static uint64_t hash_cursor(uint32_t w, uint32_t h, // Fallback function to lookup an unnamed cursor by its bitmap static uint32_t find_cursor_by_image(Ghandles *g) { - XFixesCursorImage *img = XFixesGetCursorImage(g->display); - if (!img) return CURSOR_DEFAULT; + XFixesCursorImage *live_img = XFixesGetCursorImage(g->display); + if (!live_img) return CURSOR_DEFAULT; + + // SEC: Abort immediately on suspiciously huge cursors to avoid mallocating too much RAM + if (live_img->width > 512 || live_img->height > 512) { + return CURSOR_DEFAULT; + } /* Narrow unsigned long pixels to uint32_t */ - size_t npx = (size_t)img->width * img->height; + size_t npx = (size_t)live_img->width * live_img->height; uint32_t *live_px = malloc(npx * sizeof(uint32_t)); if (!live_px) { - XFree(img); + XFree(live_img); return CURSOR_DEFAULT; } - for (size_t i = 0; i < npx; i++) live_px[i] = (uint32_t)img->pixels[i]; + for (size_t i = 0; i < npx; i++) live_px[i] = (uint32_t)live_img->pixels[i]; - uint64_t live_hash = hash_cursor(img->width, img->height, img->xhot, img->yhot, live_px); + uint64_t live_hash = hash_cursor(live_img->width, live_img->height, live_img->xhot, live_img->yhot, live_px); free(live_px); /* Use the live cursor's own size to avoid potential discrepancies between root's and the user's themes */ - uint32_t size = (img->width > img->height) ? img->width : img->height; - XFree(img); + uint32_t size = (live_img->width > live_img->height) ? live_img->width : live_img->height; + XFree(live_img); char *theme = XcursorGetTheme(g->display); for (size_t i = 0; i < NUM_SUPPORTED_CURSORS; i++) { @@ -483,7 +488,10 @@ static uint32_t find_cursor_by_image(Ghandles *g) { if (hash == live_hash) { uint32_t found = CURSOR_X11 + supported_cursors[i].cursor_id; - assert(found < CURSOR_X11_MAX); + // SEC: Check bounds at runtime + if (found >= CURSOR_X11_MAX) { + return CURSOR_DEFAULT; + } return found; } } diff --git a/rpm_spec/gui-agent.spec.in b/rpm_spec/gui-agent.spec.in index 9ba1d75d..975f1e16 100644 --- a/rpm_spec/gui-agent.spec.in +++ b/rpm_spec/gui-agent.spec.in @@ -54,6 +54,7 @@ URL: https://www.qubes-os.org BuildRequires: gcc BuildRequires: libX11-devel BuildRequires: libXcomposite-devel +BuildRequires: libXcursor-devel BuildRequires: libXdamage-devel BuildRequires: libXfixes-devel BuildRequires: libXt-devel From 63a5f26e4267cb5e2beb3c4da1a32f3f2fd5ea02 Mon Sep 17 00:00:00 2001 From: Thomas GRIM Date: Mon, 4 May 2026 11:19:59 +0200 Subject: [PATCH 3/3] Precompute the hashed cursors lookup table the first time we see an unnamed cursor --- gui-agent/vmside.c | 59 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/gui-agent/vmside.c b/gui-agent/vmside.c index dc794844..5b8b5c5f 100644 --- a/gui-agent/vmside.c +++ b/gui-agent/vmside.c @@ -320,6 +320,14 @@ static struct supported_cursor supported_cursors[] = { #define NUM_SUPPORTED_CURSORS (QUBES_ARRAY_SIZE(supported_cursors)) +struct hashed_cursor { + uint64_t hash; + uint32_t cursor_id; +}; + +static struct hashed_cursor *hashed_cursors = NULL; +static size_t num_hashed_cursors = 0; + static int compare_supported_cursors(const void *a, const void *b) { return strcmp(((const struct supported_cursor *)a)->name, ((const struct supported_cursor *)b)->name); @@ -424,7 +432,6 @@ static uint32_t find_cursor(Ghandles *g, Atom atom) /* * Some applications don't set cursor names properly when sending XfixesDisplayCursorNotify events, notably Chromium and derivatives. * Before falling back to CURSOR_DEFAULT, we'll try to match (quick hashes of) the live cursor's bitmap with each supported cursor's bitmap. - * TODO: Precompute the hashes of supported_cursors once during init to avoid redundant computations everytime a cursor changes (this might not be trivial). **/ // Generic Fowler-Noll-Vo quick hash function (FNV-1a), magic mix number from WikiPedia's entry @@ -452,6 +459,28 @@ static uint64_t hash_cursor(uint32_t w, uint32_t h, return hash; } +// Precompute a table of cursor hashes (for a given cursor size) to accelerate matching unnamed cursors +static void precompute_hashed_cursors(Ghandles *g, uint32_t cursor_size) { + char *theme = XcursorGetTheme(g->display); + + free(hashed_cursors); + hashed_cursors = NULL; + num_hashed_cursors = 0; + + hashed_cursors = calloc(NUM_SUPPORTED_CURSORS, sizeof(*hashed_cursors)); + if (!hashed_cursors) return; + + for (size_t i = 0; i < NUM_SUPPORTED_CURSORS; i++) { + XcursorImage *img = XcursorLibraryLoadImage(supported_cursors[i].name, theme, (int)cursor_size); + if (!img) continue; + + hashed_cursors[num_hashed_cursors].hash = hash_cursor(img->width, img->height, img->xhot, img->yhot, img->pixels); + hashed_cursors[num_hashed_cursors].cursor_id = supported_cursors[i].cursor_id; + num_hashed_cursors++; + XcursorImageDestroy(img); + } +} + // Fallback function to lookup an unnamed cursor by its bitmap static uint32_t find_cursor_by_image(Ghandles *g) { XFixesCursorImage *live_img = XFixesGetCursorImage(g->display); @@ -473,22 +502,11 @@ static uint32_t find_cursor_by_image(Ghandles *g) { uint64_t live_hash = hash_cursor(live_img->width, live_img->height, live_img->xhot, live_img->yhot, live_px); free(live_px); - - /* Use the live cursor's own size to avoid potential discrepancies between root's and the user's themes */ - uint32_t size = (live_img->width > live_img->height) ? live_img->width : live_img->height; XFree(live_img); - char *theme = XcursorGetTheme(g->display); - for (size_t i = 0; i < NUM_SUPPORTED_CURSORS; i++) { - XcursorImage *img = XcursorLibraryLoadImage(supported_cursors[i].name, theme, size); - if (!img) continue; - - uint64_t hash = hash_cursor(img->width, img->height, img->xhot, img->yhot, img->pixels); - XcursorImageDestroy(img); - - if (hash == live_hash) { - uint32_t found = CURSOR_X11 + supported_cursors[i].cursor_id; - // SEC: Check bounds at runtime + for (size_t i = 0; i < num_hashed_cursors; i++) { + if (hashed_cursors[i].hash == live_hash) { + uint32_t found = CURSOR_X11 + hashed_cursors[i].cursor_id; if (found >= CURSOR_X11_MAX) { return CURSOR_DEFAULT; } @@ -523,6 +541,17 @@ static void process_xevent_cursor(Ghandles *g, XFixesCursorNotifyEvent *ev) if (ev->cursor_name != None) { cursor = find_cursor(g, ev->cursor_name); } else { + // Precompute the table of hashed cursors based on the actual cursor size + if (num_hashed_cursors == 0) { + XFixesCursorImage *live_img = XFixesGetCursorImage(g->display); + if (live_img) { + uint32_t size = (live_img->width > live_img->height) ? live_img->width : live_img->height; + XFree(live_img); + fprintf(stderr, "Precomputing hashed cursors to accelerate subsequent lookups"); + precompute_hashed_cursors(g, size); + } + } + cursor = find_cursor_by_image(g); }