[RFC PATCH 1/2] futex: Create reproducer for robust_list race condition
flat view
STALE235d
From: André Almeida <andrealmeid@igalia.com>
Date: 2026-02-20 20:27:40
Also in:
lkml
Subsystem:
the rest · Maintainer:
Linus Torvalds
Create a reproducer for https://sourceware.org/bugzilla/show_bug.cgi?id=14485 This is not supposed to be merged. Signed-off-by: André Almeida <andrealmeid@igalia.com> --- robust_bug.c | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 robust_bug.c
diff --git a/robust_bug.c b/robust_bug.c
new file mode 100644
index 000000000000..1ade4e6d66dd
--- /dev/null
+++ b/robust_bug.c@@ -0,0 +1,178 @@ +/* + * gcc robust_bug.c -o robust_bug + * + * This is a reproducer for "File corruption race condition in robust + * mutex unlocking" from https://sourceware.org/bugzilla/show_bug.cgi?id=14485 + * + * To increase the changes of reaching the race condition, a delay can be added + * to the kernel function handle_futex_death(), just before the user memory + * write futex_cmpxchg_value_locked(). + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <linux/futex.h> +#include <pthread.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/syscall.h> +#include <unistd.h> +#include <time.h> + +#define cpu_relax() asm volatile("rep; nop"); + +/* + * This struct is an example of a lock struct, shared between the threads. + */ +struct lock_struct { + uint32_t futex; + struct robust_list list; +}; + +static struct lock_struct *lock; + +/* + * This is the struct that we are going to use to allocate on top of the + * freed memory to observe the race condition. + */ +struct another_struct { + uint64_t value; +}; + +static pthread_barrier_t barrier; + +static int set_robust_list(struct robust_list_head *head) +{ + return syscall(SYS_set_robust_list, head, sizeof(*head)); +} + +/* + * This thread emulates the behaviour of a thread releasing a robust mutex: + * - It starts by adding the mutex to the op_pending field + * - Remove the mutex from the robust list + * - Release the lock and wake up waiters + * - Remove the mutex from the op_pending field + * + * However, this thread dies before doing this last step, leaving the mutex + * behind in the op_pending field. + */ +void *func_b(void *arg) +{ + static struct robust_list_head head; + pid_t tid = gettid() | FUTEX_WAITERS; + + /* + * Initial thread setup. This would happen in an earlier stage of the + * thread execution. + */ + set_robust_list(&head); + head.list.next = &head.list; + head.futex_offset = (size_t) offsetof(struct lock_struct, futex) - + (size_t) offsetof(struct lock_struct, list); + + /* This thread takes the lock... */ + lock->futex = tid; + + /* ...would do some work here... */ + + /* + * ...and starts the release process. Adds the mutex to be released on + * the op_pending. + */ + head.list_op_pending = &lock->list; + + /* Barrier to synchronize thread B taking the lock */ + pthread_barrier_wait(&barrier); + usleep(100); + + /* + * Here we would release the lock and wake up any waiters. + * + * lock->futex = LOCK_FREE; + * futex_wake(lock->futex, 1); + */ + + /* + * We would remove the lock from op_pending, but we emulate a thread + * exiting before doing it. + */ + return NULL; +} + +int main(int argc, char *argv[]) +{ + struct another_struct *new; + uint64_t original_val; + pthread_t thread_b; + uint32_t value; + int ret; + + ret = pthread_barrier_init(&barrier, NULL, 2); + if (ret) { + puts("pthread_barrier_init failed"); + return -1; + } + + /* Initialize the lock */ + lock = mmap(NULL, sizeof(struct lock_struct), PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + if (lock == MAP_FAILED) { + puts("mmap failed"); + return -1; + } + memset(lock, 0, sizeof(*lock)); + + /* Create the thread B that will take the lock */ + pthread_create(&thread_b, NULL, func_b, NULL); + + /* Barrier to synchronize thread B taking the lock */ + pthread_barrier_wait(&barrier); + + /* Copy this value as we will use it later */ + value = lock->futex; + + /* + * Here, this thread would do the following: + * - It would wait for the lock, and be wake from thread B + * - Take the lock, do some work, and release it + * - After releasing the lock and being the last user, it can correctly + * free it + */ + munmap(lock, sizeof(struct lock_struct)); + + /* + * After freeing the lock, this thread allocates memory, which + * happens to be at the same address of the lock, and by chance, it fills + * the memory with the TID of thread B. + */ + new = mmap(NULL, sizeof(struct another_struct), PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + if (new == MAP_FAILED) { + puts("mmap failed"); + return -1; + } + if ((uintptr_t) lock != (uintptr_t) new) { + puts("mmap got a different address"); + return -1; + } + + new->value = ((uint64_t) value << 32) + value; + + /* Create a backup of the current value */ + original_val = new->value; + + /* Wait for the memory corruption to happen... */ + while (new->value == original_val) + cpu_relax(); + + /* ...and now the kernel just overwrote an unrelated user memory! */ + printf("Memory was corrupted by the kernel: %lx vs %lx\n", + original_val, new->value); + + munmap(new, sizeof(struct another_struct)); + + return 0; +}
--
2.53.0