From 9b91c02e0734cafbab1439581bbcd42ca23819af Mon Sep 17 00:00:00 2001 From: XxJSONDeruloxX Date: Wed, 14 Oct 2026 21:49:23 -0400 Subject: [PATCH] fix: preserve robust list syscall semantics --- docs/development/quest-openxr.md | 9 +++ tools/linuxfs/preload/robust.c | 23 +++++-- tools/tests/robust_preload_harness.c | 91 ++++++++++++++++++++++++++++ tools/tests/test_robust_preload.py | 18 ++++++ 4 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 tools/tests/robust_preload_harness.c create mode 100644 tools/tests/test_robust_preload.py diff --git a/docs/development/quest-openxr.md b/docs/development/quest-openxr.md index d3bb7b1e4..12fb4c1a4 100644 --- a/docs/development/quest-openxr.md +++ b/docs/development/quest-openxr.md @@ -310,6 +310,15 @@ bundled into the user-facing runtime. A valid already-signaled SYNC_FD value of ## Remaining stages +Steam's ARM64 watchdog also aborts with `futex robust_list not initialized by +pthreads`. A captured preload trace showed successful own-thread queries +leaving `errno=EFAULT`. The preload now prefers the real kernel query, falls +back only for `ENOSYS`, preserves errno on fallback success and safely rejects +invalid output pointers. A host regression compares native and fallback +results with the kernel's actual head, including a held robust mutex and a +second thread, and preserves permission failures. This is not yet a verified +fix for Valve's assertion: the Quest disconnected before a new device trace. + - Android/Linux eye-image bridge and frame ownership/synchronization. - OpenVR driver IPC for predicted 6DoF head, per-eye views and tracked hands. - Real Touch input mapping and haptics returning through the bridge. diff --git a/tools/linuxfs/preload/robust.c b/tools/linuxfs/preload/robust.c index 6bbb6f91e..18c708f46 100644 --- a/tools/linuxfs/preload/robust.c +++ b/tools/linuxfs/preload/robust.c @@ -98,23 +98,36 @@ long syscall(long number, ...) { args[i] = va_arg(ap, long); } va_end(ap); + int saved_errno = errno; + if (!real_syscall) { + real_syscall = (long (*)(long, ...)) dlsym(RTLD_NEXT, "syscall"); + } if (number == SYS_get_robust_list && (args[0] == 0 || args[0] == gettid())) { + errno = saved_errno; + long result = real_syscall(number, args[0], args[1], args[2]); + if (result != -1 || errno != ENOSYS) { + return result; + } struct robust_list_head *head = thread_robust_head(); if (!head) { errno = ENOSYS; return -1; } - *(struct robust_list_head **)args[1] = head; - *(size_t *)args[2] = sizeof(*head); + size_t length = sizeof(*head); + struct iovec local[2] = {{&head, sizeof(head)}, {&length, sizeof(length)}}; + struct iovec remote[2] = {{(void *)args[1], sizeof(head)}, {(void *)args[2], sizeof(length)}}; + ssize_t written = process_vm_writev(getpid(), local, 2, remote, 2, 0); + if (written != (ssize_t)(sizeof(head) + sizeof(length))) { + if (written >= 0) errno = EFAULT; + return -1; + } + errno = saved_errno; return 0; } long handled; if (bl_fsync_syscall(number, args, &handled)) { return handled; } - if (!real_syscall) { - real_syscall = (long (*)(long, ...)) dlsym(RTLD_NEXT, "syscall"); - } if (number == SYS_openat2) { if (!__atomic_load_n(&openat2_missing, __ATOMIC_RELAXED)) { long fd = real_syscall(number, args[0], args[1], args[2], args[3]); diff --git a/tools/tests/robust_preload_harness.c b/tools/tests/robust_preload_harness.c new file mode 100644 index 000000000..71374f4c4 --- /dev/null +++ b/tools/tests/robust_preload_harness.c @@ -0,0 +1,91 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +int bl_fsync_syscall(long number, const long *args, long *result) { + (void)number; + (void)args; + (void)result; + return 0; +} + +#include "../linuxfs/preload/robust.c" + +static long (*kernel_syscall)(long, ...); +static int missing_error; + +static long missing_robust(long number, ...) { + assert(number == SYS_get_robust_list); + errno = missing_error; + return -1; +} + +static struct robust_list_head *check_head(void) { + struct robust_list_head *expected = NULL; + size_t expected_length = 0; + assert(kernel_syscall(SYS_get_robust_list, 0, &expected, &expected_length) == 0); + struct robust_list_head *head = NULL; + size_t length = 0; + errno = E2BIG; + assert(syscall(SYS_get_robust_list, 0L, &head, &length, 0L, 0L, 0L) == 0); + assert(errno == E2BIG); + assert(head == expected && length == expected_length); + return head; +} + +static void *check_thread(void *argument) { + (void)argument; + pthread_mutexattr_t attributes; + pthread_mutex_t mutex; + assert(pthread_mutexattr_init(&attributes) == 0); + assert(pthread_mutexattr_setrobust(&attributes, PTHREAD_MUTEX_ROBUST) == 0); + assert(pthread_mutex_init(&mutex, &attributes) == 0); + assert(pthread_mutexattr_destroy(&attributes) == 0); + struct robust_list_head *head = check_head(); + assert(pthread_mutex_lock(&mutex) == 0); + struct robust_list *first = head->list.next; + struct robust_list *pending = head->list_op_pending; + assert(check_head() == head); + assert(head->list.next == first && head->list_op_pending == pending); + assert(pthread_mutex_unlock(&mutex) == 0); + assert(pthread_mutex_destroy(&mutex) == 0); + return head; +} + +int main(void) { + kernel_syscall = dlsym(RTLD_NEXT, "syscall"); + assert(kernel_syscall); + real_syscall = kernel_syscall; + struct robust_list_head *main_head = check_thread(NULL); + pthread_t thread; + void *thread_head; + assert(pthread_create(&thread, NULL, check_thread, NULL) == 0); + assert(pthread_join(thread, &thread_head) == 0); + assert(thread_head != main_head); + real_syscall = missing_robust; + missing_error = ENOSYS; + check_thread(NULL); + assert(pthread_create(&thread, NULL, check_thread, NULL) == 0); + assert(pthread_join(thread, NULL) == 0); + struct robust_list_head *head = NULL; + size_t length = 0; + assert(syscall(SYS_get_robust_list, 0L, NULL, &length, 0L, 0L, 0L) == -1); + assert(errno == EFAULT); + assert(syscall(SYS_get_robust_list, 0L, &head, NULL, 0L, 0L, 0L) == -1); + assert(errno == EFAULT); + head = NULL; + missing_error = EPERM; + assert(syscall(SYS_get_robust_list, 0L, &head, &length, 0L, 0L, 0L) == -1); + assert(errno == EPERM && !head && !length); + missing_error = EACCES; + assert(syscall(SYS_get_robust_list, 0L, &head, &length, 0L, 0L, 0L) == -1); + assert(errno == EACCES && !head && !length); + puts("robust preload: native and ENOSYS fallback metadata, threads, errno and permission failures passed"); + return 0; +} diff --git a/tools/tests/test_robust_preload.py b/tools/tests/test_robust_preload.py new file mode 100644 index 000000000..9e5c0688f --- /dev/null +++ b/tools/tests/test_robust_preload.py @@ -0,0 +1,18 @@ +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +@unittest.skipUnless(shutil.which("gcc"), "needs gcc on a Linux host") +class RobustPreloadTest(unittest.TestCase): + def test_native_and_missing_syscall_contract(self): + with tempfile.TemporaryDirectory() as directory: + executable = Path(directory) / "robust-preload-test" + harness = Path(__file__).with_name("robust_preload_harness.c") + subprocess.run(["gcc", "-O2", "-Wall", "-Wextra", "-Werror", "-pthread", + str(harness), "-ldl", "-o", str(executable)], check=True) + result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=20) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("permission failures passed", result.stdout)