| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Copyright (C) 2025 Igalia S.L. |
| * |
| * Robust list test by André Almeida <andrealmeid@igalia.com> |
| * |
| * The robust list uAPI allows userspace to create "robust" locks, in the sense |
| * that if the lock holder thread dies, the remaining threads that are waiting |
| * for the lock won't block forever, waiting for a lock that will never be |
| * released. |
| * |
| * This is achieve by userspace setting a list where a thread can enter all the |
| * locks (futexes) that it is holding. The robust list is a linked list, and |
| * userspace register the start of the list with the syscall set_robust_list(). |
| * If such thread eventually dies, the kernel will walk this list, waking up one |
| * thread waiting for each futex and marking the futex word with the flag |
| * FUTEX_OWNER_DIED. |
| * |
| * See also |
| * man set_robust_list |
| * Documententation/locking/robust-futex-ABI.rst |
| * Documententation/locking/robust-futexes.rst |
| */ |
| |
| #define _GNU_SOURCE |
| |
| #include "futextest.h" |
| #include "kselftest_harness.h" |
| |
| #include <dlfcn.h> |
| #include <errno.h> |
| #include <pthread.h> |
| #include <signal.h> |
| #include <stdatomic.h> |
| #include <stdbool.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/auxv.h> |
| #include <sys/mman.h> |
| #include <sys/wait.h> |
| |
| #define STACK_SIZE (1024 * 1024) |
| #define FUTEX_TIMEOUT 3 |
| #define SLEEP_US 100 |
| |
| #if __SIZEOF_LONG__ == 8 |
| # define BUILD_64 |
| #endif |
| |
| static pthread_barrier_t barrier, barrier2; |
| |
| static int set_robust_list(struct robust_list_head *head, size_t len) |
| { |
| return syscall(SYS_set_robust_list, head, len); |
| } |
| |
| static int get_robust_list(int pid, struct robust_list_head **head, size_t *len_ptr) |
| { |
| return syscall(SYS_get_robust_list, pid, head, len_ptr); |
| } |
| |
| static int sys_futex_robust_unlock(_Atomic(uint32_t) *uaddr, unsigned int op, int val, |
| void *list_op_pending, unsigned int val3) |
| { |
| return syscall(SYS_futex, uaddr, op, val, NULL, list_op_pending, val3, 0); |
| } |
| |
| /* |
| * Basic lock struct, contains just the futex word and the robust list element |
| * Real implementations have also a *prev to easily walk in the list |
| */ |
| typedef _Atomic(unsigned int) atomic_futex_t; |
| |
| struct lock_struct { |
| atomic_futex_t futex; |
| struct robust_list list; |
| }; |
| |
| struct child_args { |
| struct __test_metadata *_metadata; |
| void *arg; |
| }; |
| |
| /* |
| * Helper function to spawn a child thread. Returns -1 on error, pid on success |
| */ |
| static int create_child(struct __test_metadata *_metadata, int (*fn)(void *arg), void *arg) |
| { |
| struct child_args *cargs = malloc(sizeof(*cargs)); |
| char *stack; |
| pid_t pid; |
| |
| if (!cargs) |
| return -1; |
| cargs->_metadata = _metadata; |
| cargs->arg = arg; |
| |
| stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, |
| MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); |
| if (stack == MAP_FAILED) { |
| free(cargs); |
| return -1; |
| } |
| |
| stack += STACK_SIZE; |
| |
| pid = clone(fn, stack, CLONE_VM | SIGCHLD, cargs); |
| if (pid == -1) { |
| free(cargs); |
| return -1; |
| } |
| |
| return pid; |
| } |
| |
| /* |
| * Helper function to prepare and register a robust list |
| */ |
| static int set_list(struct robust_list_head *head) |
| { |
| int ret; |
| |
| ret = set_robust_list(head, sizeof(*head)); |
| if (ret) |
| return ret; |
| |
| head->futex_offset = (size_t) offsetof(struct lock_struct, futex) - |
| (size_t) offsetof(struct lock_struct, list); |
| head->list.next = &head->list; |
| head->list_op_pending = NULL; |
| |
| return 0; |
| } |
| |
| /* |
| * A basic (and incomplete) mutex lock function with robustness |
| */ |
| static int mutex_lock(struct lock_struct *lock, struct robust_list_head *head, bool error_inject) |
| { |
| atomic_futex_t *futex = &lock->futex; |
| unsigned int zero = 0; |
| pid_t tid = gettid(); |
| int ret = -1; |
| |
| /* |
| * Set list_op_pending before starting the lock, so the kernel can catch |
| * the case where the thread died during the lock operation |
| */ |
| head->list_op_pending = &lock->list; |
| |
| if (atomic_compare_exchange_strong(futex, &zero, tid)) { |
| /* |
| * We took the lock, insert it in the robust list |
| */ |
| struct robust_list *list = &head->list; |
| |
| /* Error injection to test list_op_pending */ |
| if (error_inject) |
| return 0; |
| |
| while (list->next != &head->list) |
| list = list->next; |
| |
| list->next = &lock->list; |
| lock->list.next = &head->list; |
| |
| ret = 0; |
| } else { |
| /* |
| * We didn't take the lock, wait until the owner wakes (or dies) |
| */ |
| struct timespec to; |
| |
| to.tv_sec = FUTEX_TIMEOUT; |
| to.tv_nsec = 0; |
| |
| tid = atomic_load(futex); |
| /* Kernel ignores futexes without the waiters flag */ |
| tid |= FUTEX_WAITERS; |
| atomic_store(futex, tid); |
| |
| ret = futex_wait((futex_t *) futex, tid, &to, 0); |
| |
| /* |
| * A real mutex_lock() implementation would loop here to finally |
| * take the lock. We don't care about that, so we stop here. |
| */ |
| } |
| |
| head->list_op_pending = NULL; |
| |
| return ret; |
| } |
| |
| /* |
| * This child thread will succeed taking the lock, and then will exit holding it |
| */ |
| static int child_fn_lock(void *arg) |
| { |
| struct child_args *cargs = arg; |
| struct __test_metadata *_metadata = cargs->_metadata; |
| struct lock_struct *lock = cargs->arg; |
| struct robust_list_head head; |
| int ret; |
| |
| free(cargs); |
| |
| ret = set_list(&head); |
| ASSERT_EQ(ret, 0) |
| TH_LOG("set_robust_list error"); |
| |
| ret = mutex_lock(lock, &head, false); |
| ASSERT_EQ(ret, 0) |
| TH_LOG("mutex_lock error"); |
| |
| pthread_barrier_wait(&barrier); |
| |
| /* |
| * There's a race here: the parent thread needs to be inside |
| * futex_wait() before the child thread dies, otherwise it will miss the |
| * wakeup from handle_futex_death() that this child will emit. We wait a |
| * little bit just to make sure that this happens. |
| */ |
| usleep(SLEEP_US); |
| |
| return 0; |
| } |
| |
| /* |
| * Spawns a child thread that will set a robust list, take the lock, register it |
| * in the robust list and die. The parent thread will wait on this futex, and |
| * should be waken up when the child exits. |
| */ |
| TEST(test_robustness) |
| { |
| struct lock_struct lock = { .futex = 0 }; |
| atomic_futex_t *futex = &lock.futex; |
| struct robust_list_head head; |
| int ret, pid, wstatus; |
| |
| ret = set_list(&head); |
| ASSERT_EQ(ret, 0); |
| |
| /* |
| * Lets use a barrier to ensure that the child thread takes the lock |
| * before the parent |
| */ |
| ret = pthread_barrier_init(&barrier, NULL, 2); |
| ASSERT_EQ(ret, 0); |
| |
| pid = create_child(_metadata, &child_fn_lock, &lock); |
| ASSERT_NE(pid, -1); |
| |
| pthread_barrier_wait(&barrier); |
| ret = mutex_lock(&lock, &head, false); |
| |
| /* |
| * futex_wait() should return 0 and the futex word should be marked with |
| * FUTEX_OWNER_DIED |
| */ |
| ASSERT_EQ(ret, 0); |
| |
| ASSERT_TRUE(*futex & FUTEX_OWNER_DIED); |
| |
| wait(&wstatus); |
| pthread_barrier_destroy(&barrier); |
| |
| EXPECT_EQ(WEXITSTATUS(wstatus), 0) |
| TH_LOG("child failed"); |
| } |
| |
| /* |
| * The only valid value for len is sizeof(*head) |
| */ |
| TEST(test_set_robust_list_invalid_size) |
| { |
| struct robust_list_head head; |
| size_t head_size = sizeof(head); |
| int ret; |
| |
| ret = set_robust_list(&head, head_size); |
| ASSERT_EQ(ret, 0); |
| |
| ret = set_robust_list(&head, head_size * 2); |
| ASSERT_EQ(ret, -1); |
| ASSERT_EQ(errno, EINVAL); |
| |
| ret = set_robust_list(&head, head_size - 1); |
| ASSERT_EQ(ret, -1); |
| ASSERT_EQ(errno, EINVAL); |
| |
| ret = set_robust_list(&head, 0); |
| ASSERT_EQ(ret, -1); |
| ASSERT_EQ(errno, EINVAL); |
| } |
| |
| /* |
| * Test get_robust_list with pid = 0, getting the list of the running thread |
| */ |
| TEST(test_get_robust_list_self) |
| { |
| struct robust_list_head head, head2, *get_head; |
| size_t head_size = sizeof(head), len_ptr; |
| int ret; |
| |
| ret = set_robust_list(&head, head_size); |
| ASSERT_EQ(ret, 0); |
| |
| ret = get_robust_list(0, &get_head, &len_ptr); |
| ASSERT_EQ(ret, 0); |
| ASSERT_EQ(get_head, &head); |
| ASSERT_EQ(head_size, len_ptr); |
| |
| ret = set_robust_list(&head2, head_size); |
| ASSERT_EQ(ret, 0); |
| |
| ret = get_robust_list(0, &get_head, &len_ptr); |
| ASSERT_EQ(ret, 0); |
| ASSERT_EQ(get_head, &head2); |
| ASSERT_EQ(head_size, len_ptr); |
| } |
| |
| static int child_list(void *arg) |
| { |
| struct child_args *cargs = arg; |
| struct __test_metadata *_metadata = cargs->_metadata; |
| struct robust_list_head *head = cargs->arg; |
| int ret; |
| |
| free(cargs); |
| |
| ret = set_robust_list(head, sizeof(*head)); |
| ASSERT_EQ(ret, 0) |
| TH_LOG("set_robust_list error"); |
| |
| /* |
| * After setting the list head, wait until the main thread can call |
| * get_robust_list() for this thread before exiting. |
| */ |
| pthread_barrier_wait(&barrier); |
| pthread_barrier_wait(&barrier2); |
| |
| return 0; |
| } |
| |
| /* |
| * Test get_robust_list from another thread. We use two barriers here to ensure |
| * that: |
| * 1) the child thread set the list before we try to get it from the |
| * parent |
| * 2) the child thread still alive when we try to get the list from it |
| */ |
| TEST(test_get_robust_list_child) |
| { |
| struct robust_list_head head, *get_head; |
| int ret, wstatus; |
| size_t len_ptr; |
| pid_t tid; |
| |
| ret = pthread_barrier_init(&barrier, NULL, 2); |
| ret = pthread_barrier_init(&barrier2, NULL, 2); |
| ASSERT_EQ(ret, 0); |
| |
| tid = create_child(_metadata, &child_list, &head); |
| ASSERT_NE(tid, -1); |
| |
| pthread_barrier_wait(&barrier); |
| |
| ret = get_robust_list(tid, &get_head, &len_ptr); |
| ASSERT_EQ(ret, 0); |
| ASSERT_EQ(&head, get_head); |
| |
| pthread_barrier_wait(&barrier2); |
| |
| wait(&wstatus); |
| pthread_barrier_destroy(&barrier); |
| pthread_barrier_destroy(&barrier2); |
| |
| EXPECT_EQ(WEXITSTATUS(wstatus), 0) |
| TH_LOG("child failed"); |
| } |
| |
| static int child_fn_lock_with_error(void *arg) |
| { |
| struct child_args *cargs = arg; |
| struct __test_metadata *_metadata = cargs->_metadata; |
| struct lock_struct *lock = cargs->arg; |
| struct robust_list_head head; |
| int ret; |
| |
| free(cargs); |
| |
| ret = set_list(&head); |
| ASSERT_EQ(ret, 0) |
| TH_LOG("set_robust_list error"); |
| |
| ret = mutex_lock(lock, &head, true); |
| ASSERT_EQ(ret, 0) |
| TH_LOG("mutex_lock error"); |
| |
| pthread_barrier_wait(&barrier); |
| |
| /* See comment at child_fn_lock() */ |
| usleep(SLEEP_US); |
| |
| return 0; |
| } |
| |
| /* |
| * Same as robustness test, but inject an error where the mutex_lock() exits |
| * earlier, just after setting list_op_pending and taking the lock, to test the |
| * list_op_pending mechanism |
| */ |
| TEST(test_set_list_op_pending) |
| { |
| struct lock_struct lock = { .futex = 0 }; |
| atomic_futex_t *futex = &lock.futex; |
| struct robust_list_head head; |
| int ret, wstatus; |
| |
| ret = set_list(&head); |
| ASSERT_EQ(ret, 0); |
| |
| ret = pthread_barrier_init(&barrier, NULL, 2); |
| ASSERT_EQ(ret, 0); |
| |
| ret = create_child(_metadata, &child_fn_lock_with_error, &lock); |
| ASSERT_NE(ret, -1); |
| |
| pthread_barrier_wait(&barrier); |
| ret = mutex_lock(&lock, &head, false); |
| |
| ASSERT_EQ(ret, 0); |
| |
| ASSERT_TRUE(*futex & FUTEX_OWNER_DIED); |
| |
| wait(&wstatus); |
| pthread_barrier_destroy(&barrier); |
| |
| EXPECT_EQ(WEXITSTATUS(wstatus), 0) |
| TH_LOG("child failed"); |
| } |
| |
| #define CHILD_NR 10 |
| |
| static int child_lock_holder(void *arg) |
| { |
| struct child_args *cargs = arg; |
| struct lock_struct *locks = cargs->arg; |
| struct robust_list_head head; |
| int i; |
| |
| free(cargs); |
| |
| set_list(&head); |
| |
| for (i = 0; i < CHILD_NR; i++) { |
| locks[i].futex = 0; |
| mutex_lock(&locks[i], &head, false); |
| } |
| |
| pthread_barrier_wait(&barrier); |
| pthread_barrier_wait(&barrier2); |
| |
| /* See comment at child_fn_lock() */ |
| usleep(SLEEP_US); |
| |
| return 0; |
| } |
| |
| static int child_wait_lock(void *arg) |
| { |
| struct child_args *cargs = arg; |
| struct __test_metadata *_metadata = cargs->_metadata; |
| struct lock_struct *lock = cargs->arg; |
| struct robust_list_head head; |
| int ret; |
| |
| free(cargs); |
| |
| pthread_barrier_wait(&barrier2); |
| ret = mutex_lock(lock, &head, false); |
| ASSERT_EQ(ret, 0) |
| TH_LOG("mutex_lock error"); |
| |
| ASSERT_TRUE(lock->futex & FUTEX_OWNER_DIED) |
| TH_LOG("futex not marked with FUTEX_OWNER_DIED"); |
| |
| return 0; |
| } |
| |
| /* |
| * Test a robust list of more than one element. All the waiters should wake when |
| * the holder dies |
| */ |
| TEST(test_robust_list_multiple_elements) |
| { |
| struct lock_struct locks[CHILD_NR]; |
| pid_t pids[CHILD_NR + 1]; |
| int i, ret, wstatus; |
| |
| ret = pthread_barrier_init(&barrier, NULL, 2); |
| ASSERT_EQ(ret, 0); |
| ret = pthread_barrier_init(&barrier2, NULL, CHILD_NR + 1); |
| ASSERT_EQ(ret, 0); |
| |
| pids[0] = create_child(_metadata, &child_lock_holder, &locks); |
| ASSERT_NE(pids[0], -1); |
| |
| /* Wait until the locker thread takes the look */ |
| pthread_barrier_wait(&barrier); |
| |
| for (i = 0; i < CHILD_NR; i++) { |
| pids[i+1] = create_child(_metadata, &child_wait_lock, &locks[i]); |
| ASSERT_NE(pids[i+1], -1); |
| } |
| |
| /* Wait for all children to return (holder + all waiters) */ |
| ret = 0; |
| for (i = 0; i < CHILD_NR + 1; i++) { |
| waitpid(pids[i], &wstatus, 0); |
| if (WEXITSTATUS(wstatus)) |
| ret = -1; |
| } |
| |
| pthread_barrier_destroy(&barrier); |
| pthread_barrier_destroy(&barrier2); |
| |
| EXPECT_EQ(ret, 0) |
| TH_LOG("One or more children failed"); |
| } |
| |
| static int child_circular_list(void *arg) |
| { |
| struct child_args *cargs = arg; |
| struct __test_metadata *_metadata = cargs->_metadata; |
| static struct lock_struct a, b, c; |
| struct robust_list_head head; |
| int ret; |
| |
| free(cargs); |
| |
| ret = set_list(&head); |
| ASSERT_EQ(ret, 0) |
| TH_LOG("set_list error"); |
| |
| head.list.next = &a.list; |
| |
| /* |
| * The last element should point to head list, but we short circuit it |
| */ |
| a.list.next = &b.list; |
| b.list.next = &c.list; |
| c.list.next = &a.list; |
| |
| return 0; |
| } |
| |
| /* |
| * Create a circular robust list. The kernel should be able to destroy the list |
| * while processing it so it won't be trapped in an infinite loop while handling |
| * a process exit |
| */ |
| TEST(test_circular_list) |
| { |
| int wstatus; |
| pid_t pid; |
| |
| pid = create_child(_metadata, child_circular_list, NULL); |
| ASSERT_NE(pid, -1); |
| |
| wait(&wstatus); |
| |
| EXPECT_EQ(WEXITSTATUS(wstatus), 0) |
| TH_LOG("child failed"); |
| } |
| |
| /* |
| * Below are tests for the fix of robust release race condition. Please read the following |
| * thread to learn more about the issue in the first place and why the following functions fix it: |
| * https://lore.kernel.org/lkml/20260316162316.356674433@kernel.org/ |
| */ |
| |
| /* |
| * Auxiliary code for binding the vDSO functions |
| */ |
| static void *get_vdso_func_addr(const char *function) |
| { |
| const char *vdso_names[] = { |
| "linux-vdso.so.1", "linux-gate.so.1", "linux-vdso32.so.1", "linux-vdso64.so.1", |
| }; |
| |
| for (int i = 0; i < ARRAY_SIZE(vdso_names); i++) { |
| void *vdso = dlopen(vdso_names[i], RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); |
| |
| if (vdso) |
| return dlsym(vdso, function); |
| } |
| return NULL; |
| } |
| |
| /* |
| * These are the real vDSO function signatures: |
| * |
| * __vdso_futex_robust_list64_try_unlock(__u32 *lock, __u32 tid, __u64 *pop) |
| * __vdso_futex_robust_list32_try_unlock(__u32 *lock, __u32 tid, __u32 *pop) |
| * |
| * So for the generic entry point we need to use a void pointer as the last argument |
| */ |
| FIXTURE(vdso_unlock) |
| { |
| uint32_t (*vdso)(_Atomic(uint32_t) *lock, uint32_t tid, void *pop); |
| }; |
| |
| FIXTURE_VARIANT(vdso_unlock) |
| { |
| bool is_32; |
| char func_name[]; |
| }; |
| |
| FIXTURE_SETUP(vdso_unlock) |
| { |
| self->vdso = get_vdso_func_addr(variant->func_name); |
| } |
| |
| FIXTURE_TEARDOWN(vdso_unlock) {} |
| |
| FIXTURE_VARIANT_ADD(vdso_unlock, 32) |
| { |
| .func_name = "__vdso_futex_robust_list32_try_unlock", |
| .is_32 = true, |
| }; |
| |
| FIXTURE_VARIANT_ADD(vdso_unlock, 64) |
| { |
| .func_name = "__vdso_futex_robust_list64_try_unlock", |
| .is_32 = false, |
| }; |
| |
| /* |
| * Test the vDSO robust_listXX_try_unlock() for the uncontended case. The virtual syscall should |
| * return the thread ID of the lock owner, the lock word must be 0 and the list_op_pending should |
| * be NULL. |
| */ |
| TEST_F(vdso_unlock, test_robust_try_unlock_uncontended) |
| { |
| struct lock_struct lock = { .futex = 0 }; |
| _Atomic(unsigned int) *futex = &lock.futex; |
| struct robust_list_head head; |
| uintptr_t exp = (uintptr_t) NULL; |
| pid_t tid = gettid(); |
| int ret; |
| |
| if (!self->vdso) { |
| ksft_test_result_skip("%s not found\n", variant->func_name); |
| return; |
| } |
| |
| *futex = tid; |
| |
| ret = set_list(&head); |
| if (ret) |
| ksft_test_result_fail("set_robust_list error\n"); |
| |
| head.list_op_pending = &lock.list; |
| |
| ret = self->vdso(futex, tid, &head.list_op_pending); |
| |
| ASSERT_EQ(ret, tid); |
| ASSERT_EQ(*futex, 0); |
| |
| /* Check only the lower 32 bits for the 32-bit entry point */ |
| if (variant->is_32) { |
| exp = (uintptr_t)(unsigned long)&lock.list; |
| exp &= ~0xFFFFFFFFULL; |
| } |
| |
| ASSERT_EQ((uintptr_t)(unsigned long)head.list_op_pending, exp); |
| } |
| |
| /* |
| * If the lock is contended, the operation fails. The return value is the value found at the |
| * futex word (tid | FUTEX_WAITERS), the futex word is not modified and the list_op_pending is_32 |
| * not cleared. |
| */ |
| TEST_F(vdso_unlock, test_robust_try_unlock_contended) |
| { |
| struct lock_struct lock = { .futex = 0 }; |
| _Atomic(unsigned int) *futex = &lock.futex; |
| struct robust_list_head head; |
| pid_t tid = gettid(); |
| int ret; |
| |
| if (!self->vdso) { |
| ksft_test_result_skip("%s not found\n", variant->func_name); |
| return; |
| } |
| |
| *futex = tid | FUTEX_WAITERS; |
| |
| ret = set_list(&head); |
| if (ret) |
| ksft_test_result_fail("set_robust_list error\n"); |
| |
| head.list_op_pending = &lock.list; |
| |
| ret = self->vdso(futex, tid, &head.list_op_pending); |
| |
| ASSERT_EQ(ret, tid | FUTEX_WAITERS); |
| ASSERT_EQ(*futex, tid | FUTEX_WAITERS); |
| ASSERT_EQ(head.list_op_pending, &lock.list); |
| } |
| |
| FIXTURE(futex_op) {}; |
| |
| FIXTURE_VARIANT(futex_op) |
| { |
| unsigned int op; |
| unsigned int val3; |
| }; |
| |
| FIXTURE_SETUP(futex_op) {} |
| |
| FIXTURE_TEARDOWN(futex_op) {} |
| |
| FIXTURE_VARIANT_ADD(futex_op, wake) |
| { |
| .op = FUTEX_WAKE, |
| .val3 = 0, |
| }; |
| |
| FIXTURE_VARIANT_ADD(futex_op, wake_bitset) |
| { |
| .op = FUTEX_WAKE_BITSET, |
| .val3 = FUTEX_BITSET_MATCH_ANY, |
| }; |
| |
| FIXTURE_VARIANT_ADD(futex_op, unlock_pi) |
| { |
| .op = FUTEX_UNLOCK_PI, |
| .val3 = 0, |
| }; |
| |
| FIXTURE_VARIANT_ADD(futex_op, wake32) |
| { |
| .op = FUTEX_WAKE | FUTEX_ROBUST_LIST32, |
| .val3 = 0, |
| }; |
| |
| FIXTURE_VARIANT_ADD(futex_op, wake_bitset32) |
| { |
| .op = FUTEX_WAKE_BITSET | FUTEX_ROBUST_LIST32, |
| .val3 = FUTEX_BITSET_MATCH_ANY, |
| }; |
| |
| FIXTURE_VARIANT_ADD(futex_op, unlock_pi32) |
| { |
| .op = FUTEX_UNLOCK_PI | FUTEX_ROBUST_LIST32, |
| .val3 = 0, |
| }; |
| |
| /* |
| * The syscall should return the number of tasks waken (for this test, 0), clear the futex word and |
| * clear list_op_pending |
| */ |
| TEST_F(futex_op, test_futex_robust_unlock) |
| { |
| struct lock_struct lock = { .futex = 0 }; |
| _Atomic(unsigned int) *futex = &lock.futex; |
| uintptr_t exp = (uintptr_t) NULL; |
| struct robust_list_head head; |
| pid_t tid = gettid(); |
| int ret; |
| |
| #ifndef BUILD_64 |
| if (!(variant->op & FUTEX_ROBUST_LIST32)) { |
| ksft_test_result_skip("Not supported for 32 bit build\n"); |
| return; |
| } |
| #endif |
| |
| *futex = tid | FUTEX_WAITERS; |
| |
| ret = set_list(&head); |
| if (ret) |
| ksft_test_result_fail("set_robust_list error\n"); |
| |
| head.list_op_pending = &lock.list; |
| |
| ret = sys_futex_robust_unlock(futex, FUTEX_ROBUST_UNLOCK | variant->op, tid, |
| &head.list_op_pending, variant->val3); |
| |
| ASSERT_EQ(ret, 0); |
| ASSERT_EQ(*futex, 0); |
| |
| if (variant->op & FUTEX_ROBUST_LIST32) { |
| exp = (uint64_t)(unsigned long)&lock.list; |
| exp &= ~0xFFFFFFFFULL; |
| } |
| |
| ASSERT_EQ((uintptr_t)(unsigned long)head.list_op_pending, exp); |
| } |
| |
| TEST_HARNESS_MAIN |