#16184

FUTEX_LOCK_PI blocks forever when the futex owner does not exist or exits (Linux returns ESRCH or hands off the lock)

Opensotomayorlucasopened 8d ago

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:

CaseLinuxgVisor
FUTEX_LOCK_PI, owner TID does not existESRCHblocks forever
FUTEX_TRYLOCK_PI, owner TID does not existESRCHEAGAIN
FUTEX_LOCK_PI, owner exits without unlocking while the caller is already waitingwaiter 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 5s
Owner 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 5s
Owner 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 5s

runsc version

runsc version release-20261012.0
spec: 1.2.1

docker version (if using docker)

29.9.1

uname

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

0

No activity yet.