diff --git a/docs/quest.md b/docs/quest.md index ac09937e5..6f69d06cd 100644 --- a/docs/quest.md +++ b/docs/quest.md @@ -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 diff --git a/tools/linuxfs/preload/kgslpoll.c b/tools/linuxfs/preload/kgslpoll.c index 841c049ee..adc6fb28c 100644 --- a/tools/linuxfs/preload/kgslpoll.c +++ b/tools/linuxfs/preload/kgslpoll.c @@ -1,28 +1,112 @@ #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; + 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; 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..29353d7a7 --- /dev/null +++ b/tools/tests/test_kgsl_read.c @@ -0,0 +1,151 @@ +#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 || 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, ×tamp) == 0 && timestamp == 5 && waits == 0); + + 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 == 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; + + 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, ×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 == 1); + + wait_error = EINVAL; + assert(read_timestamp(3, ×tamp) == -1 && errno == EINVAL); + wait_error = 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); + + submit(8); + submit(7); + retired = 8; + assert(read_timestamp(3, ×tamp) == 0 && timestamp == 8); + + create_context(3); + retired = 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); + 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); + + 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 68cd99fb0..7c3365544 100644 --- a/tools/tests/test_quest_kgsl.py +++ b/tools/tests/test_quest_kgsl.py @@ -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()