diff --git a/docs/quest.md b/docs/quest.md index 2f7da2e80..ac09937e5 100644 --- a/docs/quest.md +++ b/docs/quest.md @@ -41,7 +41,9 @@ Both the Android driver wrapper and the Linux session preload use `tools/quest/kgsl_compat.c`. Successful legacy allocation/free calls are unchanged. Only `EACCES`/`EPERM` failures retry through `GPUOBJ_ALLOC`, `GPUOBJ_INFO` and `GPUOBJ_FREE`. The Android wrapper depends on the original -bundled Turnip binary. Unhandled controller ioctls forward to the next +bundled Turnip binary. Quest's SELinux policy also denies `CMDSTREAM_READTIMESTAMP_CTXTID`, which Mesa 26's Turnip +needs to wait on timeline semaphores; the adapter answers it from the last submitted timestamp +and a zero-timeout `WAITTIMESTAMP`. Unhandled controller ioctls forward to the next preload hook rather than skipping it with a raw syscall. `tools/build_local.sh` and CI build the wrapper from source using the pinned diff --git a/tools/quest/kgsl_compat.c b/tools/quest/kgsl_compat.c index 33f4cfe18..18fd61b0a 100644 --- a/tools/quest/kgsl_compat.c +++ b/tools/quest/kgsl_compat.c @@ -29,10 +29,92 @@ static void report(const char *operation, int error) { errno = saved; } +#define TRACKED_CONTEXTS 32 + +static struct { + unsigned int context, queued, retired; +} tracked[TRACKED_CONTEXTS]; +static char tracked_lock; + +static void tracked_acquire(void) { + while (__atomic_test_and_set(&tracked_lock, __ATOMIC_ACQUIRE)) {} +} + +static void tracked_release(void) { + __atomic_clear(&tracked_lock, __ATOMIC_RELEASE); +} + +static int tracked_find(unsigned int context, int create) { + int free_slot = -1; + for (int i = 0; i < TRACKED_CONTEXTS; i++) { + if (tracked[i].context == context) return i; + if (free_slot < 0 && tracked[i].context == 0) free_slot = i; + } + if (!create) return -1; + if (free_slot < 0) free_slot = (int)(context % TRACKED_CONTEXTS); + tracked[free_slot].context = context; + tracked[free_slot].queued = tracked[free_slot].retired = 0; + return free_slot; +} + +static void track_submit(unsigned int context, unsigned int timestamp) { + if (context == 0) return; + tracked_acquire(); + int slot = tracked_find(context, 1); + if ((int)(timestamp - tracked[slot].queued) < 0) tracked[slot].retired = 0; + tracked[slot].queued = timestamp; + tracked_release(); +} + +static void track_destroy(unsigned int context) { + tracked_acquire(); + int slot = tracked_find(context, 0); + if (slot >= 0) tracked[slot].context = 0; + tracked_release(); +} + +/* Quest's SELinux policy denies CMDSTREAM_READTIMESTAMP_CTXTID, which Mesa 26's Turnip needs to wait on + * timeline semaphores; a zero-timeout WAITTIMESTAMP on the last submitted timestamp answers it. */ +static int read_timestamp(quest_ioctl_fn real, int fd, struct kgsl_cmdstream_readtimestamp_ctxtid *query) { + tracked_acquire(); + int slot = tracked_find(query->context_id, 0); + unsigned int queued = slot < 0 ? 0 : tracked[slot].queued; + unsigned int retired = slot < 0 ? 0 : tracked[slot].retired; + tracked_release(); + if (slot < 0) { errno = EACCES; return -1; } + + if (query->type == KGSL_TIMESTAMP_RETIRED && (int)(queued - retired) > 0) { + struct kgsl_device_waittimestamp_ctxtid wait = { + .context_id = query->context_id, .timestamp = queued, .timeout = 0}; + if (real(fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait) == 0) { + retired = queued; + tracked_acquire(); + slot = tracked_find(query->context_id, 0); + if (slot >= 0 && (int)(retired - tracked[slot].retired) > 0) tracked[slot].retired = retired; + tracked_release(); + } else if (errno != ETIMEDOUT) { + return -1; + } + } + query->timestamp = query->type == KGSL_TIMESTAMP_RETIRED ? retired : queued; + return 0; +} + int quest_kgsl_ioctl(quest_ioctl_fn real, int fd, unsigned long request, void *arg) { int rc = real(fd, request, arg); + if (rc == 0 && arg != NULL) { + if (request == IOCTL_KGSL_GPU_COMMAND) { + const struct kgsl_gpu_command *submit = arg; + track_submit(submit->context_id, submit->timestamp); + } else if (request == IOCTL_KGSL_DRAWCTXT_DESTROY) { + track_destroy(((const struct kgsl_drawctxt_destroy *)arg)->drawctxt_id); + } + } if (rc != -1 || (errno != EACCES && errno != EPERM) || arg == NULL) return rc; + if (request == IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID) + return read_timestamp(real, fd, arg); + if (request == IOCTL_KGSL_GPUMEM_ALLOC_ID) { struct kgsl_gpumem_alloc_id *old = arg; struct kgsl_gpuobj_alloc alloc = {.size = old->size, .flags = old->flags}; diff --git a/tools/quest/test_kgsl_compat.c b/tools/quest/test_kgsl_compat.c index 30ae397a8..4c9b84e2a 100644 --- a/tools/quest/test_kgsl_compat.c +++ b/tools/quest/test_kgsl_compat.c @@ -43,7 +43,66 @@ static struct kgsl_gpumem_alloc_id reset(void) { address = 0x100000; return (struct kgsl_gpumem_alloc_id){.size = 123, .flags = 0x10000}; } +static int read_error, wait_error, waits; +static unsigned int waited_for; +static int timestamp_mock(int fd, unsigned long request, void *arg) { + assert(fd == 42); + if (request == IOCTL_KGSL_GPU_COMMAND || request == IOCTL_KGSL_DRAWCTXT_DESTROY) return 0; + if (request == IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID) { + errno = read_error; + return -1; + } + if (request == IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID) { + const struct kgsl_device_waittimestamp_ctxtid *w = arg; + assert(w->timeout == 0); + waits++; + waited_for = w->timestamp; + if (wait_error) { errno = wait_error; return -1; } + return 0; + } + errno = EACCES; + return -1; +} +static void submit(unsigned int context, unsigned int timestamp) { + struct kgsl_gpu_command cmd = {.context_id = context, .timestamp = timestamp}; + assert(quest_kgsl_ioctl(timestamp_mock, 42, IOCTL_KGSL_GPU_COMMAND, &cmd) == 0); +} +static int read_ts(unsigned int context, unsigned int type, unsigned int *timestamp) { + struct kgsl_cmdstream_readtimestamp_ctxtid q = {.context_id = context, .type = type, .timestamp = 0xdead}; + int rc = quest_kgsl_ioctl(timestamp_mock, 42, IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID, &q); + *timestamp = q.timestamp; + return rc; +} +static void timestamp_scenarios(void) { + unsigned int ts; + read_error = EACCES; + assert(read_ts(5, KGSL_TIMESTAMP_RETIRED, &ts) == -1 && errno == EACCES && ts == 0xdead); + submit(5, 10); + assert(read_ts(5, KGSL_TIMESTAMP_QUEUED, &ts) == 0 && ts == 10 && waits == 0); + wait_error = ETIMEDOUT; + assert(read_ts(5, KGSL_TIMESTAMP_RETIRED, &ts) == 0 && ts == 0 && waits == 1 && waited_for == 10); + wait_error = 0; + assert(read_ts(5, KGSL_TIMESTAMP_RETIRED, &ts) == 0 && ts == 10 && waits == 2); + assert(read_ts(5, KGSL_TIMESTAMP_RETIRED, &ts) == 0 && ts == 10 && waits == 2); + submit(5, 11); + wait_error = ETIMEDOUT; + assert(read_ts(5, KGSL_TIMESTAMP_RETIRED, &ts) == 0 && ts == 10 && waited_for == 11); + wait_error = EIO; + assert(read_ts(5, KGSL_TIMESTAMP_RETIRED, &ts) == -1 && errno == EIO); + wait_error = 0; + submit(5, 0xfffffffe); + submit(5, 1); + assert(read_ts(5, KGSL_TIMESTAMP_RETIRED, &ts) == 0 && ts == 1); + struct kgsl_drawctxt_destroy gone = {.drawctxt_id = 5}; + assert(quest_kgsl_ioctl(timestamp_mock, 42, IOCTL_KGSL_DRAWCTXT_DESTROY, &gone) == 0); + assert(read_ts(5, KGSL_TIMESTAMP_QUEUED, &ts) == -1 && errno == EACCES); + read_error = EIO; + submit(6, 3); + assert(read_ts(6, KGSL_TIMESTAMP_QUEUED, &ts) == -1 && errno == EIO); +} + int main(void) { + timestamp_scenarios(); struct kgsl_gpumem_alloc_id p = reset(); assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_ALLOC_ID, &p) == 0 && count == 1); p = reset(); old_error = EACCES; @@ -74,4 +133,5 @@ int main(void) { reset(); assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_FREE_ID, &release) == 0 && count == 1); puts("PASS: 12 GPUOBJ translation, passthrough, failure and cleanup scenarios"); + puts("PASS: denied timestamp reads answered from tracked submissions"); } diff --git a/tools/tests/test_quest_kgsl.py b/tools/tests/test_quest_kgsl.py index 4ce7ae0f6..68cd99fb0 100644 --- a/tools/tests/test_quest_kgsl.py +++ b/tools/tests/test_quest_kgsl.py @@ -17,6 +17,7 @@ class QuestKgslTest(unittest.TestCase): str(QUEST / 'test_kgsl_compat.c'), str(QUEST / 'kgsl_compat.c')], check=True) result = subprocess.run([binary], capture_output=True, text=True, check=True, timeout=10) self.assertIn('PASS: 12 GPUOBJ', result.stdout) + self.assertIn('PASS: denied timestamp reads', result.stdout) if __name__ == '__main__':