Description
gVisor's PI futex implementation never checks whether the TID stored in the futex word belongs to a live task. Three cases diverge from Linux:
| Case | Linux | gVisor |
|---|---|---|
FUTEX_LOCK_PI, owner TID does not exist | ESRCH | blocks forever |
FUTEX_TRYLOCK_PI, owner TID does not exist | ESRCH | EAGAIN |
FUTEX_LOCK_PI, owner exits without unlocking while the caller is already waiting | waiter acquires the lock (0, FUTEX_OWNER_DIED set) | waiter blocks forever |
The first case also applies when the owner existed but has already exited without unlocking (a non-robust PI mutex held by a thread that exited).
In Linux, attach_to_pi_owner() looks up the owner with
find_get_task_by_vpid(), and handle_exit_race() returns -ESRCH when it is
gone and the futex word is unchanged. When a PI owner exits with waiters,
exit_pi_state_list() releases the rt_mutex and the top waiter takes over.
In gVisor, Manager.lockPILocked() (pkg/sentry/kernel/futex/futex.go) sets
FUTEX_WAITERS and enqueues the waiter whenever cur & FUTEX_TID_MASK is
non-zero and not the caller's TID. Nothing validates the owner, and task exit
only processes the robust list (exitRobustList), so a waiter on a non-robust
PI futex is only woken by FUTEX_UNLOCK_PI from a task that no longer exists.
The blocked task can still be interrupted by signals.
The code path is unchanged on master as of 30ace3e2.
As a side note, the LockPI doc comment says "we don't support robust lists",
which is outdated now that exitRobustList exists.
A possible fix: before enqueueing in lockPILocked (and in the try path),
look up the owner TID in the caller's PID namespace and return ESRCH if it
does not exist, re-reading the word as handle_exit_race() does. The futex
package has no notion of tasks, so the lookup would have to come through
Target or a callback. The third case needs the exit path to hand the lock
to the first waiter (TID | FUTEX_OWNER_DIED), the way exit_pi_state_list()
does. This would require tracking PI ownership per task.
Steps to reproduce
// futex_pi_dead_owner.c
#define _GNU_SOURCE
#include <errno.h>
#include <linux/futex.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>
static void on_alarm(int sig) {
(void)sig;
static const char msg[] = "FUTEX_LOCK_PI: still blocked after 5s\n";
write(1, msg, sizeof(msg) - 1);
_exit(1);
}
static long futex(uint32_t *uaddr, int op) {
return syscall(SYS_futex, uaddr, op, 0, NULL, NULL, 0);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
// A TID that is not in use in the caller's PID namespace.
const uint32_t dead_tid = 4194303;
static uint32_t word;
word = dead_tid;
long r = futex(&word, FUTEX_TRYLOCK_PI_PRIVATE);
printf("FUTEX_TRYLOCK_PI: ret=%ld errno=%s word=0x%x\n", r,
r < 0 ? strerrorname_np(errno) : "0", word);
word = dead_tid;
signal(SIGALRM, on_alarm);
alarm(5);
r = futex(&word, FUTEX_LOCK_PI_PRIVATE);
alarm(0);
printf("FUTEX_LOCK_PI: ret=%ld errno=%s word=0x%x\n", r,
r < 0 ? strerrorname_np(errno) : "0", word);
return 0;
}$ gcc -O2 -static -o futex_pi_dead_owner futex_pi_dead_owner.c
$ docker run --rm --runtime=runc -v $PWD:/t:ro <any image> /t/futex_pi_dead_owner
FUTEX_TRYLOCK_PI: ret=-1 errno=ESRCH word=0x803fffff
FUTEX_LOCK_PI: ret=-1 errno=ESRCH word=0x803fffff
$ docker run --rm --runtime=runsc -v $PWD:/t:ro <any image> /t/futex_pi_dead_owner
FUTEX_TRYLOCK_PI: ret=-1 errno=EAGAIN word=0x3fffff
FUTEX_LOCK_PI: still blocked after 5sOwner thread exited before the lock attempt
// futex_pi_exited_owner.c
#define _GNU_SOURCE
#include <errno.h>
#include <linux/futex.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>
static uint32_t word;
static void on_alarm(int sig) {
(void)sig;
static const char msg[] = "FUTEX_LOCK_PI: still blocked after 5s\n";
write(1, msg, sizeof(msg) - 1);
_exit(1);
}
static long futex(uint32_t *uaddr, int op) {
return syscall(SYS_futex, uaddr, op, 0, NULL, NULL, 0);
}
static void *owner(void *arg) {
(void)arg;
long r = futex(&word, FUTEX_LOCK_PI_PRIVATE);
printf("owner tid=%ld lock ret=%ld word=0x%x, exiting without unlock\n",
(long)gettid(), r, word);
return NULL;
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
pthread_t th;
pthread_create(&th, NULL, owner, NULL);
pthread_join(th, NULL);
signal(SIGALRM, on_alarm);
alarm(5);
long r = futex(&word, FUTEX_LOCK_PI_PRIVATE);
alarm(0);
printf("FUTEX_LOCK_PI: ret=%ld errno=%s word=0x%x\n", r,
r < 0 ? strerrorname_np(errno) : "0", word);
return 0;
}# runc
owner tid=7 lock ret=0 word=0x7, exiting without unlock
FUTEX_LOCK_PI: ret=-1 errno=ESRCH word=0x80000007
# runsc
owner tid=2 lock ret=0 word=0x2, exiting without unlock
FUTEX_LOCK_PI: still blocked after 5sOwner exits while another thread is already waiting
// futex_pi_owner_exits_while_waiting.c
#define _GNU_SOURCE
#include <errno.h>
#include <linux/futex.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>
static uint32_t word;
static void on_alarm(int sig) {
(void)sig;
static const char msg[] = "waiter: still blocked after 5s\n";
write(1, msg, sizeof(msg) - 1);
_exit(1);
}
static long futex(uint32_t *uaddr, int op) {
return syscall(SYS_futex, uaddr, op, 0, NULL, NULL, 0);
}
static void *owner(void *arg) {
(void)arg;
futex(&word, FUTEX_LOCK_PI_PRIVATE);
// Wait until the main thread is queued (FUTEX_WAITERS set), then exit.
while (!(__atomic_load_n(&word, __ATOMIC_SEQ_CST) & FUTEX_WAITERS))
usleep(1000);
usleep(100000);
printf("owner tid=%ld exiting without unlock, word=0x%x\n", (long)gettid(),
word);
return NULL;
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
pthread_t th;
pthread_create(&th, NULL, owner, NULL);
while (__atomic_load_n(&word, __ATOMIC_SEQ_CST) == 0) usleep(1000);
signal(SIGALRM, on_alarm);
alarm(5);
long r = futex(&word, FUTEX_LOCK_PI_PRIVATE);
alarm(0);
printf("waiter tid=%ld FUTEX_LOCK_PI: ret=%ld errno=%s word=0x%x\n",
(long)gettid(), r, r < 0 ? strerrorname_np(errno) : "0", word);
return 0;
}# runc
owner tid=7 exiting without unlock, word=0x80000007
waiter tid=1 FUTEX_LOCK_PI: ret=0 errno=0 word=0xc0000001
# runsc
owner tid=2 exiting without unlock, word=0x80000002
waiter: still blocked after 5srunsc version
runsc version release-20261012.0
spec: 1.2.1docker version (if using docker)
29.9.1uname
Linux 7.0.0-38-generic x86_64
repo state (if built from source)
Not built from source (release binary). Code path checked on master 30ace3e2.
Activity
0No activity yet.