From 9fce12730ca9918391a2274651e50dbda5cc6389 Mon Sep 17 00:00:00 2001 From: Anuar Lukpanov <156117923+Mi-Yomi@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:52:06 +0500 Subject: [PATCH 1/5] fix(quest): answer a refused KGSL timestamp read from waits Mesa 27 polls timeline points with CMDSTREAM_READTIMESTAMP_CTXTID and reports any failure as VK_ERROR_UNKNOWN, so gamescope aborted at start on Quest with vk.WaitSemaphores result -13. Track each context's submitted timestamps in the preload and answer a failed read from one millisecond WAITTIMESTAMP calls. Log refused KGSL ioctls to session.log. --- docs/quest.md | 7 ++ tools/linuxfs/preload/kgslpoll.c | 94 +++++++++++++++++++++----- tools/linuxfs/preload/netif.c | 3 + tools/tests/test_kgsl_read.c | 110 +++++++++++++++++++++++++++++++ tools/tests/test_quest_kgsl.py | 9 +++ 5 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 tools/tests/test_kgsl_read.c diff --git a/docs/quest.md b/docs/quest.md index 2f7da2e80..0b67edf99 100644 --- a/docs/quest.md +++ b/docs/quest.md @@ -44,6 +44,13 @@ unchanged. Only `EACCES`/`EPERM` failures retry through `GPUOBJ_ALLOC`, bundled Turnip binary. Unhandled controller ioctls forward to the next preload hook rather than skipping it with a raw syscall. +The Ubuntu runtime's Turnip polls timeline points with +`CMDSTREAM_READTIMESTAMP_CTXTID` and reports any failure as `VK_ERROR_UNKNOWN`, +which aborted gamescope with `vk.WaitSemaphores` result -13 on Quest. The Linux +preload (`tools/linuxfs/preload/kgslpoll.c`) answers a failed read from one +millisecond `WAITTIMESTAMP` calls over the context's submitted timestamps, and +logs refused KGSL ioctls to `session.log` as `[QuestKGSL] ioctl 0x.. refused`. + `tools/build_local.sh` and CI build the wrapper from source using the pinned Android NDK; the normal preload builds include the shared Linux adapter. Runtime input verification checks the resulting driver archive and invalidates diff --git a/tools/linuxfs/preload/kgslpoll.c b/tools/linuxfs/preload/kgslpoll.c index 841c049ee..4c16917be 100644 --- a/tools/linuxfs/preload/kgslpoll.c +++ b/tools/linuxfs/preload/kgslpoll.c @@ -1,28 +1,90 @@ #define _GNU_SOURCE #include #include +#include #include #include -#define KGSL_IOC_TYPE 0x09 -#define KGSL_TIMESTAMP_RETIRED 0x00000002 +#include "../../quest/kgsl_compat.h" -struct kgsl_device_waittimestamp_ctxtid { - unsigned int context_id; - unsigned int timestamp; - unsigned int timeout; -}; +#define KGSL_CONTEXTS 1024 -struct kgsl_cmdstream_readtimestamp_ctxtid { - unsigned int context_id; - unsigned int type; - unsigned int timestamp; -}; +static struct { + uint32_t submitted; + uint32_t retired; +} contexts[KGSL_CONTEXTS]; -#define IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID \ - _IOW(KGSL_IOC_TYPE, 0x7, struct kgsl_device_waittimestamp_ctxtid) -#define IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID \ - _IOWR(KGSL_IOC_TYPE, 0x16, struct kgsl_cmdstream_readtimestamp_ctxtid) +static void report_denied(unsigned long request, int error) { + static unsigned reports; + int saved = errno; + + if (__atomic_fetch_add(&reports, 1, __ATOMIC_RELAXED) < 8) + fprintf(stderr, "[QuestKGSL] ioctl 0x%lx refused, errno=%d\n", request & 0xff, error); + errno = saved; +} + +static void note_submit(unsigned int context_id, unsigned int timestamp) { + if (context_id < KGSL_CONTEXTS) + __atomic_store_n(&contexts[context_id].submitted, timestamp, __ATOMIC_RELAXED); +} + +/* shortcut: an unfinished timestamp sleeps a jiffy per poll, read the memstore instead if Quest's mmap of it is proven */ +static int read_retired(int (*real)(int, unsigned long, void *), int fd, + struct kgsl_cmdstream_readtimestamp_ctxtid *read) { + uint32_t id = read->context_id; + uint32_t timestamp, submitted; + + if (read->type != KGSL_TIMESTAMP_RETIRED || id >= KGSL_CONTEXTS) return -1; + timestamp = __atomic_load_n(&contexts[id].retired, __ATOMIC_RELAXED); + submitted = __atomic_load_n(&contexts[id].submitted, __ATOMIC_RELAXED); + while (timestamp != submitted) { + /* KGSL reads a timeout of 0 as wait forever. */ + struct kgsl_device_waittimestamp_ctxtid wait = { + .context_id = id, .timestamp = timestamp + 1, .timeout = 1}; + + if (real(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait) != 0) { + if (errno != ETIMEDOUT && errno != ETIME && errno != EINTR && errno != EAGAIN) return -1; + break; + } + timestamp++; + __atomic_store_n(&contexts[id].retired, timestamp, __ATOMIC_RELAXED); + } + read->timestamp = timestamp; + return 0; +} + +/* Quest refuses some KGSL ioctls, and Mesa 27 turns a failed timestamp read into VK_ERROR_UNKNOWN. */ +__attribute__((visibility("hidden"))) void bl_kgsl_track(int (*real)(int, unsigned long, void *), + int fd, unsigned long request, void *arg, + int *rc) { + int saved = errno; + + if (arg == NULL) return; + if (*rc == 0) { + if (request == IOCTL_KGSL_GPU_COMMAND) { + const struct kgsl_gpu_command *submit = arg; + note_submit(submit->context_id, submit->timestamp); + } else if (request == IOCTL_KGSL_GPU_AUX_COMMAND) { + const struct kgsl_gpu_aux_command *submit = arg; + note_submit(submit->context_id, submit->timestamp); + } else if (request == IOCTL_KGSL_DRAWCTXT_CREATE) { + const struct kgsl_drawctxt_create *create = arg; + if (create->drawctxt_id < KGSL_CONTEXTS) { + note_submit(create->drawctxt_id, 0); + __atomic_store_n(&contexts[create->drawctxt_id].retired, 0, __ATOMIC_RELAXED); + } + } + return; + } + if (_IOC_TYPE(request) == KGSL_IOC_TYPE && (saved == EACCES || saved == EPERM || + request == IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID)) + report_denied(request, saved); + if (request != IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID) return; + if (read_retired(real, fd, arg) == 0) + *rc = 0; + else if (errno != saved) + report_denied(IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, errno); +} static int enabled(void) { static int cached = -1; diff --git a/tools/linuxfs/preload/netif.c b/tools/linuxfs/preload/netif.c index 872a877e1..308af3443 100644 --- a/tools/linuxfs/preload/netif.c +++ b/tools/linuxfs/preload/netif.c @@ -404,6 +404,8 @@ void bl_kgsl_chip_id_fixup(unsigned long request, void *arg, int rc); #include "../../quest/kgsl_compat.h" int bl_kgsl_poll(int (*real)(int, unsigned long, void *), int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); +void bl_kgsl_track(int (*real)(int, unsigned long, void *), int fd, unsigned long request, + void *arg, int *rc) __attribute__((visibility("hidden"))); int bl_ntsync_ioctl(int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); int bl_drm_gem_close(int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); int bl_venc_ioctl(int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); @@ -430,6 +432,7 @@ int ioctl(int fd, unsigned long request, ...) { if (bl_winebus_rumble_ioctl(real, fd, request, arg, __builtin_return_address(0), &rc)) return rc; if (bl_writewatch_ioctl(real, fd, request, arg, &rc)) return rc; if (!bl_kgsl_poll(real, fd, request, arg, &rc)) rc = quest_kgsl_ioctl(real, fd, request, arg); + bl_kgsl_track(real, fd, request, arg, &rc); bl_kgsl_chip_id_fixup(request, arg, rc); if (arg == NULL || !is_link_query(request)) return rc; int saved = errno; diff --git a/tools/tests/test_kgsl_read.c b/tools/tests/test_kgsl_read.c new file mode 100644 index 000000000..df54bfb94 --- /dev/null +++ b/tools/tests/test_kgsl_read.c @@ -0,0 +1,110 @@ +#include +#include +#include + +#include "../quest/kgsl_compat.h" + +void bl_kgsl_track(int (*real)(int, unsigned long, void *), int fd, unsigned long request, + void *arg, int *rc); + +static int read_error, wait_error, waits; +static unsigned int retired; + +static int mock(int fd, unsigned long request, void *arg) { + assert(fd == 42); + if (request == IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID) { + if (read_error) { + errno = read_error; + return -1; + } + ((struct kgsl_cmdstream_readtimestamp_ctxtid *)arg)->timestamp = retired; + return 0; + } + assert(request == IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID); + struct kgsl_device_waittimestamp_ctxtid *wait = arg; + assert(wait->context_id == 3 && wait->timeout != 0); + waits++; + if (wait_error) { + errno = wait_error; + return -1; + } + if (wait->timestamp > retired) { + errno = ETIMEDOUT; + return -1; + } + return 0; +} + +static int issue(unsigned long request, void *arg) { + int rc = mock(42, request, arg); + bl_kgsl_track(mock, 42, request, arg, &rc); + return rc; +} + +static void submit(unsigned int timestamp) { + struct kgsl_gpu_command command = {.context_id = 3, .timestamp = timestamp}; + int rc = 0; + bl_kgsl_track(mock, 42, IOCTL_KGSL_GPU_COMMAND, &command, &rc); + assert(rc == 0); +} + +static int read_timestamp(unsigned int context, unsigned int *timestamp) { + struct kgsl_cmdstream_readtimestamp_ctxtid read = { + .context_id = context, .type = KGSL_TIMESTAMP_RETIRED}; + int rc = issue(IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID, &read); + *timestamp = read.timestamp; + return rc; +} + +int main(void) { + unsigned int timestamp; + + retired = 5; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 5 && waits == 0); + + read_error = EACCES; + retired = 0; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 0 && waits == 0); + + submit(1); + submit(2); + submit(3); + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 0 && waits == 1); + + retired = 2; + waits = 0; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 2 && waits == 3); + + waits = 0; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 2 && waits == 1); + + retired = 3; + waits = 0; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 3 && waits == 1); + + waits = 0; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 3 && waits == 0); + + submit(4); + retired = 4; + wait_error = EDEADLK; + assert(read_timestamp(3, ×tamp) == -1 && errno == EDEADLK); + wait_error = 0; + + struct kgsl_gpu_aux_command aux = {.context_id = 3, .timestamp = 6}; + int rc = 0; + bl_kgsl_track(mock, 42, IOCTL_KGSL_GPU_AUX_COMMAND, &aux, &rc); + retired = 6; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 6); + + struct kgsl_drawctxt_create create = {.drawctxt_id = 3}; + bl_kgsl_track(mock, 42, IOCTL_KGSL_DRAWCTXT_CREATE, &create, &rc); + retired = 0; + waits = 0; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 0 && waits == 0); + + errno = EACCES; + assert(read_timestamp(5000, ×tamp) == -1 && errno == EACCES); + + puts("PASS: refused timestamp reads answered from retired waits"); +} diff --git a/tools/tests/test_quest_kgsl.py b/tools/tests/test_quest_kgsl.py index 4ce7ae0f6..01cb03d86 100644 --- a/tools/tests/test_quest_kgsl.py +++ b/tools/tests/test_quest_kgsl.py @@ -18,6 +18,15 @@ class QuestKgslTest(unittest.TestCase): result = subprocess.run([binary], capture_output=True, text=True, check=True, timeout=10) self.assertIn('PASS: 12 GPUOBJ', result.stdout) + def test_refused_timestamp_read_is_answered_from_waits(self): + with tempfile.TemporaryDirectory() as folder: + binary = str(Path(folder) / 'test-kgsl-read') + subprocess.run(['gcc', '-Wall', '-Wextra', '-Werror', '-O2', '-o', binary, + str(QUEST.parent / 'tests/test_kgsl_read.c'), + str(QUEST.parent / 'linuxfs/preload/kgslpoll.c')], check=True) + result = subprocess.run([binary], capture_output=True, text=True, check=True, timeout=10) + self.assertIn('PASS: refused timestamp reads', result.stdout) + if __name__ == '__main__': unittest.main() From bf0ee99b6d15542830abf054d653a0889bffd62b Mon Sep 17 00:00:00 2001 From: Anuar Lukpanov <156117923+Mi-Yomi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:01:58 +0500 Subject: [PATCH 2/5] fix(quest): keep timestamp read failures for unknown contexts Answer a failed read only for a context created on the same file descriptor, so an invalid context or a closed descriptor stays an error instead of reading as timestamp zero. --- tools/linuxfs/preload/kgslpoll.c | 12 ++++++++++-- tools/tests/test_kgsl_read.c | 28 +++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/tools/linuxfs/preload/kgslpoll.c b/tools/linuxfs/preload/kgslpoll.c index 4c16917be..0e63d482c 100644 --- a/tools/linuxfs/preload/kgslpoll.c +++ b/tools/linuxfs/preload/kgslpoll.c @@ -12,6 +12,7 @@ static struct { uint32_t submitted; uint32_t retired; + int owner; } contexts[KGSL_CONTEXTS]; static void report_denied(unsigned long request, int error) { @@ -34,7 +35,9 @@ static int read_retired(int (*real)(int, unsigned long, void *), int fd, uint32_t id = read->context_id; uint32_t timestamp, submitted; - if (read->type != KGSL_TIMESTAMP_RETIRED || id >= KGSL_CONTEXTS) return -1; + if (read->type != KGSL_TIMESTAMP_RETIRED || id >= KGSL_CONTEXTS || + __atomic_load_n(&contexts[id].owner, __ATOMIC_RELAXED) != fd + 1) + return -1; timestamp = __atomic_load_n(&contexts[id].retired, __ATOMIC_RELAXED); submitted = __atomic_load_n(&contexts[id].submitted, __ATOMIC_RELAXED); while (timestamp != submitted) { @@ -72,14 +75,19 @@ __attribute__((visibility("hidden"))) void bl_kgsl_track(int (*real)(int, unsign if (create->drawctxt_id < KGSL_CONTEXTS) { note_submit(create->drawctxt_id, 0); __atomic_store_n(&contexts[create->drawctxt_id].retired, 0, __ATOMIC_RELAXED); + __atomic_store_n(&contexts[create->drawctxt_id].owner, fd + 1, __ATOMIC_RELAXED); } + } else if (request == IOCTL_KGSL_DRAWCTXT_DESTROY) { + const struct kgsl_drawctxt_destroy *destroy = arg; + if (destroy->drawctxt_id < KGSL_CONTEXTS) + __atomic_store_n(&contexts[destroy->drawctxt_id].owner, 0, __ATOMIC_RELAXED); } return; } if (_IOC_TYPE(request) == KGSL_IOC_TYPE && (saved == EACCES || saved == EPERM || request == IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID)) report_denied(request, saved); - if (request != IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID) return; + if (request != IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID || saved == EBADF) return; if (read_retired(real, fd, arg) == 0) *rc = 0; else if (errno != saved) diff --git a/tools/tests/test_kgsl_read.c b/tools/tests/test_kgsl_read.c index df54bfb94..9bcb507c1 100644 --- a/tools/tests/test_kgsl_read.c +++ b/tools/tests/test_kgsl_read.c @@ -11,7 +11,7 @@ static int read_error, wait_error, waits; static unsigned int retired; static int mock(int fd, unsigned long request, void *arg) { - assert(fd == 42); + assert(fd == 42 || fd == 43); if (request == IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID) { if (read_error) { errno = read_error; @@ -56,6 +56,12 @@ static int read_timestamp(unsigned int context, unsigned int *timestamp) { return rc; } +static void create_context(unsigned int context) { + struct kgsl_drawctxt_create create = {.drawctxt_id = context}; + int rc = 0; + bl_kgsl_track(mock, 42, IOCTL_KGSL_DRAWCTXT_CREATE, &create, &rc); +} + int main(void) { unsigned int timestamp; @@ -64,8 +70,21 @@ int main(void) { read_error = EACCES; retired = 0; + errno = EACCES; + assert(read_timestamp(3, ×tamp) == -1 && errno == EACCES && waits == 0); + + create_context(3); assert(read_timestamp(3, ×tamp) == 0 && timestamp == 0 && waits == 0); + read_error = EBADF; + assert(read_timestamp(3, ×tamp) == -1 && errno == EBADF && waits == 0); + read_error = EACCES; + + struct kgsl_cmdstream_readtimestamp_ctxtid other = {.context_id = 3, .type = KGSL_TIMESTAMP_RETIRED}; + int other_rc = mock(43, IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID, &other); + bl_kgsl_track(mock, 43, IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID, &other, &other_rc); + assert(other_rc == -1 && errno == EACCES && waits == 0); + submit(1); submit(2); submit(3); @@ -97,12 +116,15 @@ int main(void) { retired = 6; assert(read_timestamp(3, ×tamp) == 0 && timestamp == 6); - struct kgsl_drawctxt_create create = {.drawctxt_id = 3}; - bl_kgsl_track(mock, 42, IOCTL_KGSL_DRAWCTXT_CREATE, &create, &rc); + create_context(3); retired = 0; waits = 0; assert(read_timestamp(3, ×tamp) == 0 && timestamp == 0 && waits == 0); + struct kgsl_drawctxt_destroy destroy = {.drawctxt_id = 3}; + bl_kgsl_track(mock, 42, IOCTL_KGSL_DRAWCTXT_DESTROY, &destroy, &rc); + assert(read_timestamp(3, ×tamp) == -1 && errno == EACCES); + errno = EACCES; assert(read_timestamp(5000, ×tamp) == -1 && errno == EACCES); From 6d7732710c93b3e71c5bc28735b5c40c4ea84f88 Mon Sep 17 00:00:00 2001 From: Anuar Lukpanov <156117923+Mi-Yomi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:27:57 +0500 Subject: [PATCH 3/5] fix(quest): check a tracked KGSL context before answering a read A descriptor can close without destroying its contexts and its number can be reused. KGSL checks the owner on every wait, so one wait on the retired timestamp keeps a read of a dead context an error. --- tools/linuxfs/preload/kgslpoll.c | 22 ++++++++++++++-------- tools/tests/test_kgsl_read.c | 10 +++++++--- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/tools/linuxfs/preload/kgslpoll.c b/tools/linuxfs/preload/kgslpoll.c index 0e63d482c..7b877b9fa 100644 --- a/tools/linuxfs/preload/kgslpoll.c +++ b/tools/linuxfs/preload/kgslpoll.c @@ -29,26 +29,32 @@ static void note_submit(unsigned int context_id, unsigned int timestamp) { __atomic_store_n(&contexts[context_id].submitted, timestamp, __ATOMIC_RELAXED); } +static int wait_retired(int (*real)(int, unsigned long, void *), int fd, uint32_t id, uint32_t timestamp) { + /* KGSL reads a timeout of 0 as wait forever. */ + struct kgsl_device_waittimestamp_ctxtid wait = {.context_id = id, .timestamp = timestamp, .timeout = 1}; + + if (real(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait) == 0) return 0; + return errno == ETIMEDOUT || errno == ETIME || errno == EINTR || errno == EAGAIN ? 1 : -1; +} + /* shortcut: an unfinished timestamp sleeps a jiffy per poll, read the memstore instead if Quest's mmap of it is proven */ static int read_retired(int (*real)(int, unsigned long, void *), int fd, struct kgsl_cmdstream_readtimestamp_ctxtid *read) { uint32_t id = read->context_id; uint32_t timestamp, submitted; + int result; if (read->type != KGSL_TIMESTAMP_RETIRED || id >= KGSL_CONTEXTS || __atomic_load_n(&contexts[id].owner, __ATOMIC_RELAXED) != fd + 1) return -1; timestamp = __atomic_load_n(&contexts[id].retired, __ATOMIC_RELAXED); submitted = __atomic_load_n(&contexts[id].submitted, __ATOMIC_RELAXED); + /* KGSL checks a context's owner on every wait, which catches one that died with a reused descriptor. */ + if (timestamp != 0 && wait_retired(real, fd, id, timestamp) < 0) return -1; while (timestamp != submitted) { - /* KGSL reads a timeout of 0 as wait forever. */ - struct kgsl_device_waittimestamp_ctxtid wait = { - .context_id = id, .timestamp = timestamp + 1, .timeout = 1}; - - if (real(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait) != 0) { - if (errno != ETIMEDOUT && errno != ETIME && errno != EINTR && errno != EAGAIN) return -1; - break; - } + result = wait_retired(real, fd, id, timestamp + 1); + if (result < 0) return -1; + if (result > 0) break; timestamp++; __atomic_store_n(&contexts[id].retired, timestamp, __ATOMIC_RELAXED); } diff --git a/tools/tests/test_kgsl_read.c b/tools/tests/test_kgsl_read.c index 9bcb507c1..23fcd37df 100644 --- a/tools/tests/test_kgsl_read.c +++ b/tools/tests/test_kgsl_read.c @@ -95,14 +95,18 @@ int main(void) { assert(read_timestamp(3, ×tamp) == 0 && timestamp == 2 && waits == 3); waits = 0; - assert(read_timestamp(3, ×tamp) == 0 && timestamp == 2 && waits == 1); + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 2 && waits == 2); retired = 3; waits = 0; - assert(read_timestamp(3, ×tamp) == 0 && timestamp == 3 && waits == 1); + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 3 && waits == 2); waits = 0; - assert(read_timestamp(3, ×tamp) == 0 && timestamp == 3 && waits == 0); + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 3 && waits == 1); + + wait_error = EINVAL; + assert(read_timestamp(3, ×tamp) == -1 && errno == EINVAL); + wait_error = 0; submit(4); retired = 4; From 1259abed5fae505e4173bdff693eb8633071b114 Mon Sep 17 00:00:00 2001 From: Anuar Lukpanov <156117923+Mi-Yomi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:33:03 +0500 Subject: [PATCH 4/5] fix(quest): check a caught-up KGSL context at timestamp zero too A context with no submissions skipped the ownership check. Check whenever nothing is pending, since a pending timestamp's wait already checks it. --- tools/linuxfs/preload/kgslpoll.c | 2 +- tools/tests/test_kgsl_read.c | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/linuxfs/preload/kgslpoll.c b/tools/linuxfs/preload/kgslpoll.c index 7b877b9fa..83e16bc82 100644 --- a/tools/linuxfs/preload/kgslpoll.c +++ b/tools/linuxfs/preload/kgslpoll.c @@ -50,7 +50,7 @@ static int read_retired(int (*real)(int, unsigned long, void *), int fd, timestamp = __atomic_load_n(&contexts[id].retired, __ATOMIC_RELAXED); submitted = __atomic_load_n(&contexts[id].submitted, __ATOMIC_RELAXED); /* KGSL checks a context's owner on every wait, which catches one that died with a reused descriptor. */ - if (timestamp != 0 && wait_retired(real, fd, id, timestamp) < 0) return -1; + if (timestamp == submitted && wait_retired(real, fd, id, timestamp) < 0) return -1; while (timestamp != submitted) { result = wait_retired(real, fd, id, timestamp + 1); if (result < 0) return -1; diff --git a/tools/tests/test_kgsl_read.c b/tools/tests/test_kgsl_read.c index 23fcd37df..d7d137aea 100644 --- a/tools/tests/test_kgsl_read.c +++ b/tools/tests/test_kgsl_read.c @@ -74,8 +74,13 @@ int main(void) { assert(read_timestamp(3, ×tamp) == -1 && errno == EACCES && waits == 0); create_context(3); - assert(read_timestamp(3, ×tamp) == 0 && timestamp == 0 && waits == 0); + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 0 && waits == 1); + wait_error = EINVAL; + assert(read_timestamp(3, ×tamp) == -1 && errno == EINVAL); + wait_error = 0; + + waits = 0; read_error = EBADF; assert(read_timestamp(3, ×tamp) == -1 && errno == EBADF && waits == 0); read_error = EACCES; @@ -95,11 +100,11 @@ int main(void) { assert(read_timestamp(3, ×tamp) == 0 && timestamp == 2 && waits == 3); waits = 0; - assert(read_timestamp(3, ×tamp) == 0 && timestamp == 2 && waits == 2); + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 2 && waits == 1); retired = 3; waits = 0; - assert(read_timestamp(3, ×tamp) == 0 && timestamp == 3 && waits == 2); + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 3 && waits == 1); waits = 0; assert(read_timestamp(3, ×tamp) == 0 && timestamp == 3 && waits == 1); @@ -123,7 +128,7 @@ int main(void) { create_context(3); retired = 0; waits = 0; - assert(read_timestamp(3, ×tamp) == 0 && timestamp == 0 && waits == 0); + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 0 && waits == 1); struct kgsl_drawctxt_destroy destroy = {.drawctxt_id = 3}; bl_kgsl_track(mock, 42, IOCTL_KGSL_DRAWCTXT_DESTROY, &destroy, &rc); From 3193a7a7067dddd057c08bd1b8fc74ebad3d83aa Mon Sep 17 00:00:00 2001 From: Anuar Lukpanov <156117923+Mi-Yomi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:46:01 +0500 Subject: [PATCH 5/5] fix(quest): keep KGSL submitted timestamps monotonic An older submission that returns late must not hide a newer one. Contexts with user-generated timestamps can have gaps, so they keep the failed read instead of waiting once per integer. --- tools/linuxfs/preload/kgslpoll.c | 16 ++++++++++++---- tools/tests/test_kgsl_read.c | 10 ++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/tools/linuxfs/preload/kgslpoll.c b/tools/linuxfs/preload/kgslpoll.c index 83e16bc82..adc6fb28c 100644 --- a/tools/linuxfs/preload/kgslpoll.c +++ b/tools/linuxfs/preload/kgslpoll.c @@ -25,8 +25,14 @@ static void report_denied(unsigned long request, int error) { } static void note_submit(unsigned int context_id, unsigned int timestamp) { - if (context_id < KGSL_CONTEXTS) - __atomic_store_n(&contexts[context_id].submitted, timestamp, __ATOMIC_RELAXED); + uint32_t known; + + if (context_id >= KGSL_CONTEXTS) return; + known = __atomic_load_n(&contexts[context_id].submitted, __ATOMIC_RELAXED); + while ((int32_t)(timestamp - known) > 0 && + !__atomic_compare_exchange_n(&contexts[context_id].submitted, &known, timestamp, 1, + __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + } } static int wait_retired(int (*real)(int, unsigned long, void *), int fd, uint32_t id, uint32_t timestamp) { @@ -79,9 +85,11 @@ __attribute__((visibility("hidden"))) void bl_kgsl_track(int (*real)(int, unsign } else if (request == IOCTL_KGSL_DRAWCTXT_CREATE) { const struct kgsl_drawctxt_create *create = arg; if (create->drawctxt_id < KGSL_CONTEXTS) { - note_submit(create->drawctxt_id, 0); + __atomic_store_n(&contexts[create->drawctxt_id].submitted, 0, __ATOMIC_RELAXED); __atomic_store_n(&contexts[create->drawctxt_id].retired, 0, __ATOMIC_RELAXED); - __atomic_store_n(&contexts[create->drawctxt_id].owner, fd + 1, __ATOMIC_RELAXED); + /* User timestamps can leave gaps that would take one wait per integer, so their reads stay failed. */ + __atomic_store_n(&contexts[create->drawctxt_id].owner, + create->flags & KGSL_CONTEXT_USER_GENERATED_TS ? 0 : fd + 1, __ATOMIC_RELAXED); } } else if (request == IOCTL_KGSL_DRAWCTXT_DESTROY) { const struct kgsl_drawctxt_destroy *destroy = arg; diff --git a/tools/tests/test_kgsl_read.c b/tools/tests/test_kgsl_read.c index d7d137aea..29353d7a7 100644 --- a/tools/tests/test_kgsl_read.c +++ b/tools/tests/test_kgsl_read.c @@ -125,6 +125,11 @@ int main(void) { retired = 6; assert(read_timestamp(3, ×tamp) == 0 && timestamp == 6); + submit(8); + submit(7); + retired = 8; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 8); + create_context(3); retired = 0; waits = 0; @@ -134,6 +139,11 @@ int main(void) { bl_kgsl_track(mock, 42, IOCTL_KGSL_DRAWCTXT_DESTROY, &destroy, &rc); assert(read_timestamp(3, ×tamp) == -1 && errno == EACCES); + struct kgsl_drawctxt_create user = {.flags = KGSL_CONTEXT_USER_GENERATED_TS, .drawctxt_id = 3}; + bl_kgsl_track(mock, 42, IOCTL_KGSL_DRAWCTXT_CREATE, &user, &rc); + waits = 0; + assert(read_timestamp(3, ×tamp) == -1 && errno == EACCES && waits == 0); + errno = EACCES; assert(read_timestamp(5000, ×tamp) == -1 && errno == EACCES);