fix(quest): gamescope no longer aborts at start with vk.WaitSemaphores -13 (#1938)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-11-03 21:58:49 -05:00
commit ad0d39f457
5 files changed
+270 -16

No files matched your search

+7
View File
@@ -46,6 +46,13 @@ needs to wait on timeline semaphores; the adapter answers it from the last submi
and a zero-timeout `WAITTIMESTAMP`. 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
+100 -16
View File
@@ -1,28 +1,112 @@
#define _GNU_SOURCE
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#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;
int owner;
} 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) {
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) {
/* 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 == 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;
if (result > 0) 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) {
__atomic_store_n(&contexts[create->drawctxt_id].submitted, 0, __ATOMIC_RELAXED);
__atomic_store_n(&contexts[create->drawctxt_id].retired, 0, __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;
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 || saved == EBADF) 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;
+3
View File
@@ -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;
+151
View File
@@ -0,0 +1,151 @@
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#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 || fd == 43);
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;
}
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;
retired = 5;
assert(read_timestamp(3, &timestamp) == 0 && timestamp == 5 && waits == 0);
read_error = EACCES;
retired = 0;
errno = EACCES;
assert(read_timestamp(3, &timestamp) == -1 && errno == EACCES && waits == 0);
create_context(3);
assert(read_timestamp(3, &timestamp) == 0 && timestamp == 0 && waits == 1);
wait_error = EINVAL;
assert(read_timestamp(3, &timestamp) == -1 && errno == EINVAL);
wait_error = 0;
waits = 0;
read_error = EBADF;
assert(read_timestamp(3, &timestamp) == -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);
assert(read_timestamp(3, &timestamp) == 0 && timestamp == 0 && waits == 1);
retired = 2;
waits = 0;
assert(read_timestamp(3, &timestamp) == 0 && timestamp == 2 && waits == 3);
waits = 0;
assert(read_timestamp(3, &timestamp) == 0 && timestamp == 2 && waits == 1);
retired = 3;
waits = 0;
assert(read_timestamp(3, &timestamp) == 0 && timestamp == 3 && waits == 1);
waits = 0;
assert(read_timestamp(3, &timestamp) == 0 && timestamp == 3 && waits == 1);
wait_error = EINVAL;
assert(read_timestamp(3, &timestamp) == -1 && errno == EINVAL);
wait_error = 0;
submit(4);
retired = 4;
wait_error = EDEADLK;
assert(read_timestamp(3, &timestamp) == -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, &timestamp) == 0 && timestamp == 6);
submit(8);
submit(7);
retired = 8;
assert(read_timestamp(3, &timestamp) == 0 && timestamp == 8);
create_context(3);
retired = 0;
waits = 0;
assert(read_timestamp(3, &timestamp) == 0 && timestamp == 0 && waits == 1);
struct kgsl_drawctxt_destroy destroy = {.drawctxt_id = 3};
bl_kgsl_track(mock, 42, IOCTL_KGSL_DRAWCTXT_DESTROY, &destroy, &rc);
assert(read_timestamp(3, &timestamp) == -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, &timestamp) == -1 && errno == EACCES && waits == 0);
errno = EACCES;
assert(read_timestamp(5000, &timestamp) == -1 && errno == EACCES);
puts("PASS: refused timestamp reads answered from retired waits");
}
+9
View File
@@ -19,6 +19,15 @@ class QuestKgslTest(unittest.TestCase):
self.assertIn('PASS: 12 GPUOBJ', result.stdout)
self.assertIn('PASS: denied timestamp reads', 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()