diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs
index 5852a1247240393a85020fa1d484e06eea2673cb..2a82ffff21537aaa2d2d09eb2cd7b7896da18ef5 100644
--- a/src/platform/linux/mod.rs
+++ b/src/platform/linux/mod.rs
@@ -235,15 +235,32 @@ impl Pal for Sys {
         e(unsafe { syscall!(FTRUNCATE, fildes, length) }) as c_int
     }
 
-    fn futex(
-        addr: *mut c_int,
-        op: c_int,
-        val: c_int,
-        val2: usize,
-    ) -> Result<c_long, crate::pthread::Errno> {
-        e_raw(unsafe { syscall!(FUTEX, addr, op, val, val2, 0, 0) })
-            .map(|r| r as c_long)
-            .map_err(|e| crate::pthread::Errno(e as c_int))
+    #[inline]
+    unsafe fn futex_wait(
+        addr: *mut u32,
+        val: u32,
+        deadline: *const timespec,
+    ) -> Result<(), crate::pthread::Errno> {
+        e_raw(unsafe {
+            syscall!(
+                FUTEX, addr,       // uaddr
+                9,          // futex_op: FUTEX_WAIT_BITSET
+                val,        // val
+                deadline,   // timeout: deadline
+                0,          // uaddr2/val2: 0/NULL
+                0xffffffff  // val3: FUTEX_BITSET_MATCH_ANY
+            )
+        })
+        .map_err(|e| crate::pthread::Errno(e as c_int))
+        .map(|_| ())
+    }
+    #[inline]
+    unsafe fn futex_wake(addr: *mut u32, num: u32) -> Result<c_int, crate::pthread::Errno> {
+        e_raw(unsafe {
+            syscall!(FUTEX, addr, 1 /* FUTEX_WAKE */, num)
+        })
+        .map_err(|e| crate::pthread::Errno(e as c_int))
+        .map(|n| n as c_int)
     }
 
     fn futimens(fd: c_int, times: *const timespec) -> c_int {
diff --git a/src/platform/pal/mod.rs b/src/platform/pal/mod.rs
index 566de802c3bd6d03d1e2178208e0694f511e33d2..e8a6b21fd2bd5bb0349796aedfd3b2e953ef11e8 100644
--- a/src/platform/pal/mod.rs
+++ b/src/platform/pal/mod.rs
@@ -10,6 +10,7 @@ use crate::{
         sys_utsname::utsname,
         time::timespec,
     },
+    pthread,
 };
 
 pub use self::epoll::PalEpoll;
@@ -78,12 +79,12 @@ pub trait Pal {
 
     fn ftruncate(fildes: c_int, length: off_t) -> c_int;
 
-    fn futex(
-        addr: *mut c_int,
-        op: c_int,
-        val: c_int,
-        val2: usize,
-    ) -> Result<c_long, crate::pthread::Errno>;
+    unsafe fn futex_wait(
+        addr: *mut u32,
+        val: u32,
+        deadline: *const timespec,
+    ) -> Result<(), pthread::Errno>;
+    unsafe fn futex_wake(addr: *mut u32, num: u32) -> Result<c_int, pthread::Errno>;
 
     fn futimens(fd: c_int, times: *const timespec) -> c_int;
 
diff --git a/src/platform/redox/mod.rs b/src/platform/redox/mod.rs
index 811ef9e6d99b771d451bb5536333fe61e0c31c61..69001a0dbf47754d2eb5849cc3bcb294b34a4f98 100644
--- a/src/platform/redox/mod.rs
+++ b/src/platform/redox/mod.rs
@@ -24,6 +24,7 @@ use crate::{
         unistd::{F_OK, R_OK, W_OK, X_OK},
     },
     io::{self, prelude::*, BufReader},
+    pthread,
 };
 
 pub use redox_exec::FdGuard;
@@ -300,25 +301,35 @@ impl Pal for Sys {
         e(syscall::ftruncate(fd as usize, len as usize)) as c_int
     }
 
-    // FIXME: unsound
-    fn futex(
-        addr: *mut c_int,
-        op: c_int,
-        val: c_int,
-        val2: usize,
-    ) -> Result<c_long, crate::pthread::Errno> {
-        match unsafe {
-            syscall::futex(
-                addr as *mut i32,
-                op as usize,
-                val as i32,
-                val2,
-                ptr::null_mut(),
-            )
-        } {
-            Ok(success) => Ok(success as c_long),
-            Err(err) => Err(crate::pthread::Errno(err.errno)),
-        }
+    #[inline]
+    unsafe fn futex_wait(
+        addr: *mut u32,
+        val: u32,
+        deadline: *const timespec,
+    ) -> CoreResult<(), pthread::Errno> {
+        syscall::syscall5(
+            syscall::SYS_FUTEX,
+            addr as usize,
+            syscall::FUTEX_WAIT,
+            val as usize,
+            deadline as usize,
+            0,
+        )
+        .map_err(|s| pthread::Errno(s.errno))
+        .map(|_| ())
+    }
+    #[inline]
+    unsafe fn futex_wake(addr: *mut u32, num: u32) -> Result<c_int, pthread::Errno> {
+        syscall::syscall5(
+            syscall::SYS_FUTEX,
+            addr as usize,
+            syscall::FUTEX_WAKE,
+            num as usize,
+            0,
+            0,
+        )
+        .map_err(|s| pthread::Errno(s.errno))
+        .map(|n| n as c_int)
     }
 
     // FIXME: unsound
@@ -1106,7 +1117,7 @@ impl Pal for Sys {
 
     fn verify() -> bool {
         // GETPID on Redox is 20, which is WRITEV on Linux
-        e(unsafe { syscall::syscall5(syscall::number::SYS_GETPID, !0, !0, !0, !0, !0) }) != !0
+        (unsafe { syscall::syscall5(syscall::number::SYS_GETPID, !0, !0, !0, !0, !0) }).is_ok()
     }
 
     fn exit_thread() -> ! {
diff --git a/src/sync/cond.rs b/src/sync/cond.rs
index ee7ff97bd2eefaf8e3252d96faaa6f08cdf8a4b3..aa3b6247acb86d585274ca0164b1164e7741f036 100644
--- a/src/sync/cond.rs
+++ b/src/sync/cond.rs
@@ -74,7 +74,7 @@ impl Cond {
         unlock: impl FnOnce() -> Result<()>,
         lock: impl FnOnce() -> Result<()>,
         lock_with_timeout: impl FnOnce(&timespec) -> Result<()>,
-        timeout: Option<&timespec>,
+        deadline: Option<&timespec>,
     ) -> Result<(), Errno> {
         // TODO: Error checking for certain types (i.e. robust and errorcheck) of mutexes, e.g. if the
         // mutex is not locked.
@@ -83,14 +83,10 @@ impl Cond {
 
         unlock();
 
-        match timeout {
-            Some(timeout) => {
-                crate::sync::futex_wait(
-                    &self.cur,
-                    current,
-                    timespec::subtract(*timeout, crate::sync::rttime()).as_ref(),
-                );
-                lock_with_timeout(timeout);
+        match deadline {
+            Some(deadline) => {
+                crate::sync::futex_wait(&self.cur, current, Some(&deadline));
+                lock_with_timeout(deadline);
             }
             None => {
                 crate::sync::futex_wait(&self.cur, current, None);
diff --git a/src/sync/mod.rs b/src/sync/mod.rs
index e3cd32c73461f87cf9e823ceb857c9b05735b248..d851f925cad5e63d247ab4458cd9a54e171063f7 100644
--- a/src/sync/mod.rs
+++ b/src/sync/mod.rs
@@ -16,8 +16,12 @@ pub use self::{
 };
 
 use crate::{
-    header::time::timespec,
+    header::{
+        errno::{EAGAIN, ETIMEDOUT},
+        time::timespec,
+    },
     platform::{types::*, Pal, Sys},
+    pthread::Errno,
 };
 use core::{
     mem::MaybeUninit,
@@ -36,7 +40,7 @@ pub enum AttemptStatus {
 }
 
 pub trait FutexTy {
-    fn conv(self) -> i32;
+    fn conv(self) -> u32;
 }
 pub trait FutexAtomicTy {
     type Ty: FutexTy;
@@ -44,13 +48,13 @@ pub trait FutexAtomicTy {
     fn ptr(&self) -> *mut Self::Ty;
 }
 impl FutexTy for u32 {
-    fn conv(self) -> i32 {
-        self as i32
+    fn conv(self) -> u32 {
+        self
     }
 }
 impl FutexTy for i32 {
-    fn conv(self) -> i32 {
-        self
+    fn conv(self) -> u32 {
+        self as u32
     }
 }
 impl FutexAtomicTy for AtomicU32 {
@@ -91,20 +95,26 @@ impl FutexAtomicTy for AtomicI32 {
 
 pub unsafe fn futex_wake_ptr(ptr: *mut impl FutexTy, n: i32) -> usize {
     // TODO: unwrap_unchecked?
-    Sys::futex(ptr.cast(), FUTEX_WAKE, n, 0).unwrap() as usize
+    Sys::futex_wake(ptr.cast(), n as u32).unwrap() as usize
 }
 pub unsafe fn futex_wait_ptr<T: FutexTy>(
     ptr: *mut T,
     value: T,
-    timeout_opt: Option<&timespec>,
-) -> bool {
-    // TODO: unwrap_unchecked?
-    Sys::futex(
+    deadline_opt: Option<&timespec>,
+) -> FutexWaitResult {
+    match Sys::futex_wait(
         ptr.cast(),
-        FUTEX_WAIT,
         value.conv(),
-        timeout_opt.map_or(0, |t| t as *const _ as usize),
-    ) == Ok(0)
+        deadline_opt.map_or(core::ptr::null(), |t| t as *const _),
+    ) {
+        Ok(()) => FutexWaitResult::Waited,
+        Err(Errno(EAGAIN)) => FutexWaitResult::Stale,
+        Err(Errno(ETIMEDOUT)) if deadline_opt.is_some() => FutexWaitResult::TimedOut,
+        Err(other) => {
+            eprintln!("futex failed: {}", other.0);
+            FutexWaitResult::Waited
+        }
+    }
 }
 pub fn futex_wake(atomic: &impl FutexAtomicTy, n: i32) -> usize {
     unsafe { futex_wake_ptr(atomic.ptr(), n) }
@@ -112,9 +122,16 @@ pub fn futex_wake(atomic: &impl FutexAtomicTy, n: i32) -> usize {
 pub fn futex_wait<T: FutexAtomicTy>(
     atomic: &T,
     value: T::Ty,
-    timeout_opt: Option<&timespec>,
-) -> bool {
-    unsafe { futex_wait_ptr(atomic.ptr(), value, timeout_opt) }
+    deadline_opt: Option<&timespec>,
+) -> FutexWaitResult {
+    unsafe { futex_wait_ptr(atomic.ptr(), value, deadline_opt) }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum FutexWaitResult {
+    Waited, // possibly spurious
+    Stale,  // outdated value
+    TimedOut,
 }
 
 pub fn rttime() -> timespec {
@@ -187,7 +204,7 @@ impl AtomicLock {
     pub fn wait_if(&self, value: c_int, timeout_opt: Option<&timespec>) {
         self.wait_if_raw(value, timeout_opt);
     }
-    pub fn wait_if_raw(&self, value: c_int, timeout_opt: Option<&timespec>) -> bool {
+    pub fn wait_if_raw(&self, value: c_int, timeout_opt: Option<&timespec>) -> FutexWaitResult {
         futex_wait(&self.atomic, value, timeout_opt)
     }
 
diff --git a/src/sync/pthread_mutex.rs b/src/sync/pthread_mutex.rs
index cb08dde8cc2ca5da7a7f665bf20f61fb4b4ba74a..030cac85830d8beb0641db19d26d8d6cf8d04308 100644
--- a/src/sync/pthread_mutex.rs
+++ b/src/sync/pthread_mutex.rs
@@ -10,6 +10,8 @@ use crate::{
 
 use crate::platform::{types::*, Pal, Sys};
 
+use super::FutexWaitResult;
+
 pub struct RlctMutex {
     // Actual locking word.
     inner: AtomicUint,
@@ -123,7 +125,11 @@ impl RlctMutex {
 
                     // If the mutex is not robust, simply futex_wait until unblocked.
                     //crate::sync::futex_wait(&self.inner, inner | WAITING_BIT, None);
-                    crate::sync::futex_wait(&self.inner, thread, None);
+                    if crate::sync::futex_wait(&self.inner, thread, deadline)
+                        == FutexWaitResult::TimedOut
+                    {
+                        return Err(Errno(ETIMEDOUT));
+                    }
                 }
             }
         }
diff --git a/src/sync/rwlock.rs b/src/sync/rwlock.rs
index fe066a1798c57a18c2e09f238e202a2f153f572d..f5ecaf12fe601ecdfaf8d022b8ec1d6d21ae2394 100644
--- a/src/sync/rwlock.rs
+++ b/src/sync/rwlock.rs
@@ -21,7 +21,7 @@ impl Rwlock {
             state: AtomicU32::new(0),
         }
     }
-    pub fn acquire_write_lock(&self, _timeout: Option<&timespec>) {
+    pub fn acquire_write_lock(&self, deadline: Option<&timespec>) {
         let mut waiting_wr = self.state.load(Ordering::Relaxed) & WAITING_WR;
 
         loop {
@@ -45,16 +45,15 @@ impl Rwlock {
                     };
                     waiting_wr = expected & WAITING_WR;
 
-                    // TODO: timeout
-                    let _ = crate::sync::futex_wait(&self.state, expected, None);
+                    let _ = crate::sync::futex_wait(&self.state, expected, deadline);
                 }
             }
         }
     }
-    pub fn acquire_read_lock(&self, _timeout: Option<&timespec>) {
+    pub fn acquire_read_lock(&self, deadline: Option<&timespec>) {
         // TODO: timeout
         while let Err(old) = self.try_acquire_read_lock() {
-            crate::sync::futex_wait(&self.state, old, None);
+            crate::sync::futex_wait(&self.state, old, deadline);
         }
     }
     pub fn try_acquire_read_lock(&self) -> Result<(), u32> {
diff --git a/tests/Makefile b/tests/Makefile
index e1682dd0bdddefaafdf5a87fdee7fb09bdc1509c..46690fd8f2904e30a8a8a18a7bf15b859a7665a6 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -186,6 +186,7 @@ NAMES=\
 	pthread/rwlock_trylock \
 	pthread/rwlock_randtest \
 	pthread/mutex_recursive \
+	pthread/timeout \
 	grp/getgrouplist \
 	grp/getgrgid_r \
 	grp/getgrnam_r \
diff --git a/tests/pthread/timeout.c b/tests/pthread/timeout.c
new file mode 100644
index 0000000000000000000000000000000000000000..eea85e1380b2b83a51d24a08589f1cc91b4b2ccf
--- /dev/null
+++ b/tests/pthread/timeout.c
@@ -0,0 +1,91 @@
+#include "../test_helpers.h"
+#include "common.h"
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <pthread.h>
+#include <time.h>
+
+struct arg {
+    int status;
+    bool completed;
+    pthread_barrier_t barrier;
+    pthread_mutex_t mutex;
+};
+
+void *routine(void *arg_raw) {
+    struct arg *arg = (struct arg *)arg_raw;
+
+    int barrier_status = pthread_barrier_wait(&arg->barrier);
+
+    if (barrier_status != 0 && barrier_status != PTHREAD_BARRIER_SERIAL_THREAD) {
+        arg->status = barrier_status;
+        fputs("failed to wait for barrier\n", stderr);
+        return NULL;
+    }
+    puts("thread waited");
+
+    struct timespec abstime;
+    if ((arg->status = clock_gettime(CLOCK_MONOTONIC, &abstime)) != 0) {
+        fputs("failed to get current time\n", stderr);
+        return NULL;
+    }
+    abstime.tv_sec += 1;
+
+    if ((arg->status = pthread_mutex_timedlock(&arg->mutex, &abstime)) != ETIMEDOUT) {
+        fputs("failed to fail at locking mutex\n", stderr);
+        return NULL;
+    }
+    arg->status = 0;
+
+    return NULL;
+}
+
+int main(void) {
+    int status;
+
+    struct arg arg;
+    arg.completed = false;
+
+    status = pthread_barrier_init(&arg.barrier, NULL, 2);
+    ERROR_IF2(pthread_barrier_init, status, != 0);
+
+    status = pthread_mutex_init(&arg.mutex, NULL);
+    ERROR_IF2(pthread_mutex_init, status, != 0);
+
+    status = pthread_mutex_trylock(&arg.mutex);
+    ERROR_IF2(pthread_mutex_trylock, status, != 0);
+
+    pthread_t thread;
+    status = pthread_create(&thread, NULL, routine, &arg);
+    ERROR_IF2(pthread_create, status, != 0);
+
+    status = pthread_barrier_wait(&arg.barrier);
+    if (status != PTHREAD_BARRIER_SERIAL_THREAD) {
+        ERROR_IF2(pthread_barrier_wait, status, != 0);
+    }
+    puts("main waited");
+
+    status = pthread_join(thread, NULL);
+    ERROR_IF2(pthread_join, status, != 0);
+
+    status = pthread_mutex_unlock(&arg.mutex);
+    ERROR_IF2(pthread_mutex_unlock, status, != 0);
+
+    if (arg.status != 0) {
+        fprintf(stderr, "thread failed: %s\n", strerror(arg.status));
+        return EXIT_FAILURE;
+    }
+
+    status = pthread_mutex_destroy(&arg.mutex);
+    ERROR_IF2(pthread_mutex_destroy, status, != 0);
+
+    status = pthread_barrier_destroy(&arg.barrier);
+    ERROR_IF2(pthread_barrier_destroy, status, != 0);
+
+    return EXIT_SUCCESS;
+}
diff --git a/tests/test_helpers.h b/tests/test_helpers.h
index 5d7cd802a2372d970419c239a58a011481087cff..2f9ed80cbae1ff1c6d4a1b9ad220ecb2fe7ea3be 100644
--- a/tests/test_helpers.h
+++ b/tests/test_helpers.h
@@ -46,6 +46,15 @@
         } \
     } while(0)
 
+#define ERROR_IF2(func, status, condition) \
+    do { \
+        if (status condition) { \
+            fprintf(stderr, "%s:%s:%d: '%s' failed: %s (%d)\n", \
+                __FILE__, __func__, __LINE__, #func, strerror(status), status); \
+            _exit(EXIT_FAILURE); \
+        } \
+    } while(0)
+
 // Throws errors on API return values not defined by the standards.
 //
 // Do not pass functions as the status or condition arguments, they might be