Implement support for POSIX thread cancellations

This change makes some miracle modifications to the System Five system
call support, which lets us have safe, correct, and atomic handling of
thread cancellations. It all turned out to be cheaper than anticipated
because it wasn't necessary to modify the system call veneers. We were
able to encode the cancellability of each system call into the magnums
found in libc/sysv/syscalls.sh. Since cancellations are so waq, we are
also supporting a lovely Musl Libc mask feature for raising ECANCELED.
This commit is contained in:
Justine Tunney 2022-11-04 01:04:43 -07:00
parent 37d40e087f
commit 2278327eba
No known key found for this signature in database
GPG key ID: BE714B4575D6E328
145 changed files with 715 additions and 265 deletions

View file

@ -8,14 +8,13 @@
#define PT_OWNSTACK 1
#define PT_MAINTHREAD 2
#define PT_ASYNC 4
#define PT_NOCANCEL 8
#define PT_MASKED 16
#if !(__ASSEMBLER__ + __LINKER__ + 0)
COSMOPOLITAN_C_START_
/**
* @fileoverview Cosmopolitan POSIX Thread Internals
*/
// LEGAL TRANSITIONS ┌──> TERMINATED
// pthread_create ─┬─> JOINABLE ─┴┬─> DETACHED ──> ZOMBIE
// └──────────────┘
@ -60,15 +59,13 @@ enum PosixThreadStatus {
};
struct PosixThread {
int flags; // 0x00: see PT_* constants
_Atomic(int) cancelled; // 0x04: thread has bad beliefs
_Atomic(enum PosixThreadStatus) status;
void *(*start_routine)(void *);
void *arg; // start_routine's parameter
void *rc; // start_routine's return value
int tid; // clone parent tid
int flags; // see PT_* constants
_Atomic(int) cancelled; // thread has bad beliefs
char cancelasync; // PTHREAD_CANCEL_DEFERRED/ASYNCHRONOUS
char canceldisable; // PTHREAD_CANCEL_ENABLE/DISABLE
void *(*start)(void *); // creation callback
void *arg; // start's parameter
void *rc; // start's return value
char *altstack; // thread sigaltstack
char *tls; // bottom of tls allocation
struct CosmoTib *tib; // middle of tls allocation
@ -98,6 +95,7 @@ void _pthread_key_destruct(void) hidden;
void _pthread_onfork_prepare(void) hidden;
void _pthread_onfork_parent(void) hidden;
void _pthread_onfork_child(void) hidden;
int _pthread_cancel_sys(void) hidden;
COSMOPOLITAN_C_END_
#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */

View file

@ -16,52 +16,184 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/intrin/kprintf.h"
#include "libc/assert.h"
#include "libc/calls/calls.h"
#include "libc/calls/struct/sigaction.h"
#include "libc/calls/struct/siginfo.h"
#include "libc/calls/struct/sigset.h"
#include "libc/calls/ucontext.h"
#include "libc/errno.h"
#include "libc/intrin/atomic.h"
#include "libc/runtime/runtime.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/sa.h"
#include "libc/sysv/consts/sig.h"
#include "libc/sysv/errfuns.h"
#include "libc/thread/posixthread.internal.h"
#include "libc/thread/thread.h"
#include "libc/thread/tls.h"
extern const char systemfive_cancellable[] hidden;
extern const char systemfive_cancellable_end[] hidden;
int _pthread_cancel_sys(void) {
struct PosixThread *pt;
pt = (struct PosixThread *)__get_tls()->tib_pthread;
if (!(pt->flags & (PT_NOCANCEL | PT_MASKED)) || (pt->flags & PT_ASYNC)) {
pthread_exit(PTHREAD_CANCELED);
}
pt->flags |= PT_NOCANCEL;
return ecanceled();
}
static void OnSigCancel(int sig, siginfo_t *si, void *ctx) {
ucontext_t *uc = ctx;
struct CosmoTib *tib = __get_tls();
struct PosixThread *pt = (struct PosixThread *)tib->tib_pthread;
if (pt && !(pt->flags & PT_NOCANCEL) &&
atomic_load_explicit(&pt->cancelled, memory_order_acquire)) {
sigaddset(&uc->uc_sigmask, sig);
if ((pt->flags & PT_ASYNC) ||
(systemfive_cancellable <= (char *)uc->uc_mcontext.rip &&
(char *)uc->uc_mcontext.rip < systemfive_cancellable_end)) {
uc->uc_mcontext.rip = (intptr_t)_pthread_cancel_sys;
} else {
tkill(atomic_load_explicit(&tib->tib_tid, memory_order_relaxed), sig);
}
}
}
static void ListenForSigCancel(void) {
struct sigaction sa;
sa.sa_sigaction = OnSigCancel;
sa.sa_flags = SA_SIGINFO | SA_RESTART | SA_ONSTACK;
memset(&sa.sa_mask, -1, sizeof(sa.sa_mask));
_npassert(!sigaction(SIGCANCEL, &sa, 0));
}
/**
* Cancels thread.
*
* This function currently isn't supported. In order to support this
* function we'd need to redesign the system call interface, and add
* bloat and complexity to every function that can return EINTR. You
* might want to consider using `nsync_note` instead, which provides
* much better cancellations because posix cancellations is a broken
* design. If you need to cancel i/o operations, try doing this:
* When a thread is cancelled, it'll interrupt blocking i/o calls,
* invoke any cleanup handlers that were pushed on the thread's stack
* as well as key destructors, and then the thread exits.
*
* _Thread_local bool gotusr1;
* void OnUsr1(int sig) { gotusr1 = true; }
* struct sigaction sa = {.sa_handler = OnUsr1};
* sigaction(SIGUSR1, &sa, 0);
* pthread_kill(thread, SIGUSR1);
* By default, pthread_cancel() can only take effect when a thread is
* blocked on a @cancellationpoint, which is any system call that's
* specified as raising `EINTR`. For example, `openat`, `poll`, `ppoll`,
* `select`, `pselect`, `read`, `readv`, `pread`, `preadv`, `write`,
* `writev`, `pwrite`, `pwritev`, `accept`, `connect`, `recvmsg`,
* `sendmsg`, `recv`, `send`, `tcdrain`, `clock_nanosleep`, `fsync`,
* `fdatasync`, `fcntl(F_SETLKW)`, `epoll`, `sigsuspend`, `msync`,
* `wait4`, `getrandom`, `pthread_cond_timedwait` are most cancellation
* points, plus many userspace libraries that call the above functions,
* unless they're using pthread_setcancelstate() to temporarily disable
* the cancellation mechanism. Some userspace functions, e.g. system()
* and popen() will eagerly call pthread_testcancel_np() to help avoid
* the potential for resource leaks later on.
*
* The above code should successfully cancel a thread's blocking io
* operations in most cases, e.g.
* It's possible to put a thread in asynchronous cancellation mode using
* pthread_setcanceltype(), thus allowing a cancellation to occur at any
* assembly opcode. Please be warned that doing so is risky since it can
* easily result in resource leaks. For example, a cancellation might be
* triggered between calling open() and pthread_cleanup_push(), in which
* case the application will leak a file descriptor.
*
* void *MyThread(void *arg) {
* sigset_t ss;
* sigfillset(&ss);
* sigdelset(&ss, SIGUSR1);
* sigprocmask(SIG_SETMASK, &ss, 0);
* while (!gotusr1) {
* char buf[512];
* ssize_t rc = read(0, buf, sizeof(buf));
* if (rc == -1 && errno == EINTR) continue;
* write(1, buf, rc);
* }
* return 0;
* }
* If none of the above options seem savory to you, then a third way is
* offered for doing cancellations. Cosmopolitan Libc supports the Musl
* Libc `PTHREAD_CANCEL_MASKED` non-POSIX extension. Any thread may pass
* this setting to pthread_setcancelstate(), in which case threads won't
* be abruptly destroyed upon cancellation and have their stack unwound;
* instead, cancellation points will simply raise an `ECANCELED` error,
* which can be more safely and intuitively handled for many use cases.
*
* This has the same correctness issue as glibc, but it's usually
* "good enough" if you only need cancellations to perform things
* like server shutdown and socket options like `SO_RCVTIMEO` can
* ensure it's even safer, since it can't possibly block forever.
*
* @see https://sourceware.org/bugzilla/show_bug.cgi?id=12683
* @return 0 on success, or errno on error
* @raise ESRCH if thread isn't alive
*/
int pthread_cancel(pthread_t thread) {
kprintf("error: pthread_cancel() isn't supported, please see the"
" cosmopolitan libc documentation for further details\n");
_Exit(1);
int e, rc, tid;
static bool once;
struct PosixThread *pt;
__require_tls();
if (!once) ListenForSigCancel(), once = true;
pt = (struct PosixThread *)thread;
switch (atomic_load_explicit(&pt->status, memory_order_acquire)) {
case kPosixThreadZombie:
case kPosixThreadTerminated:
return ESRCH;
default:
break;
}
atomic_store_explicit(&pt->cancelled, 1, memory_order_release);
if (thread == __get_tls()->tib_pthread) {
if (!(pt->flags & PT_NOCANCEL) && (pt->flags & PT_ASYNC)) {
pthread_exit(PTHREAD_CANCELED);
}
return 0;
}
if (IsWindows()) return 0; // TODO(jart): Should we do this?
tid = atomic_load_explicit(&pt->tib->tib_tid, memory_order_acquire);
if (tid <= 0) return 0; // TODO(jart): Do we need this?
e = errno;
if (!tkill(tid, SIGCANCEL)) {
return 0;
} else {
rc = errno;
errno = e;
return rc;
}
return 0;
}
/**
* Creates cancellation point in calling thread.
*
* This function can be used to force `PTHREAD_CANCEL_DEFERRED` threads
* to cancel without needing to invoke an interruptible system call. If
* the calling thread is in the `PTHREAD_CANCEL_DISABLE` then this will
* do nothing. If the calling thread hasn't yet been cancelled, this'll
* do nothing. In `PTHREAD_CANCEL_MASKED` mode, this also does nothing.
*
* @see pthread_testcancel_np()
*/
void pthread_testcancel(void) {
struct PosixThread *pt;
if (!__tls_enabled) return;
pt = (struct PosixThread *)__get_tls()->tib_pthread;
if (pt->flags & PT_NOCANCEL) return;
if ((!(pt->flags & PT_MASKED) || (pt->flags & PT_ASYNC)) &&
atomic_load_explicit(&pt->cancelled, memory_order_acquire)) {
pthread_exit(PTHREAD_CANCELED);
}
}
/**
* Creates cancellation point in calling thread.
*
* This function can be used to force `PTHREAD_CANCEL_DEFERRED` threads
* to cancel without needing to invoke an interruptible system call. If
* the calling thread is in the `PTHREAD_CANCEL_DISABLE` then this will
* do nothing. If the calling thread hasn't yet been cancelled, this'll
* do nothing. If the calling thread uses `PTHREAD_CANCEL_MASKED`, then
* this function returns `ECANCELED` if a cancellation occurred, rather
* than the normal behavior which is to destroy and cleanup the thread.
* Any `ECANCELED` result must not be ignored, because the thread shall
* have cancellations disabled once it occurs.
*
* @return 0 if not cancelled or cancellation is blocked or `ECANCELED`
* in masked mode when the calling thread has been cancelled
*/
int pthread_testcancel_np(void) {
int rc;
struct PosixThread *pt;
if (!__tls_enabled) return 0;
pt = (struct PosixThread *)__get_tls()->tib_pthread;
if (pt->flags & PT_NOCANCEL) return 0;
if (!atomic_load_explicit(&pt->cancelled, memory_order_acquire)) return 0;
if (!(pt->flags & PT_MASKED) || (pt->flags & PT_ASYNC)) {
pthread_exit(PTHREAD_CANCELED);
} else {
pt->flags |= PT_NOCANCEL;
return ECANCELED;
}
}

View file

@ -43,6 +43,7 @@
* @raise EINVAL if `0 abstime->tv_nsec < 1000000000` wasn't the case
* @see pthread_cond_broadcast
* @see pthread_cond_signal
* @cancellationpoint
*/
errno_t pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *abstime) {

View file

@ -36,6 +36,7 @@
* @see pthread_cond_timedwait
* @see pthread_cond_broadcast
* @see pthread_cond_signal
* @cancellationpoint
*/
errno_t pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) {
return pthread_cond_timedwait(cond, mutex, 0);

View file

@ -98,7 +98,7 @@ static int PosixThread(void *arg, int tid) {
if (!setjmp(pt->exiter)) {
__get_tls()->tib_pthread = (pthread_t)pt;
_sigsetmask(pt->sigmask);
pt->rc = pt->start_routine(pt->arg);
pt->rc = pt->start(pt->arg);
// ensure pthread_cleanup_pop(), and pthread_exit() popped cleanup
_npassert(!pt->cleanup);
}
@ -150,7 +150,7 @@ static errno_t pthread_create_impl(pthread_t *thread,
errno = e;
return EAGAIN;
}
pt->start_routine = start_routine;
pt->start = start_routine;
pt->arg = arg;
// create thread local storage memory

View file

@ -25,9 +25,12 @@
* Waits for thread to terminate.
*
* @param value_ptr if non-null will receive pthread_exit() argument
* if the thread called pthread_exit(), or `PTHREAD_CANCELED` if
* pthread_cancel() destroyed the thread instead
* @return 0 on success, or errno with error
* @raise EDEADLK if `thread` is the current thread
* @raise EINVAL if `thread` is detached
* @cancellationpoint
* @returnserrno
* @threadsafe
*/

View file

@ -24,22 +24,47 @@
/**
* Sets cancelability state.
*
* This function may be used to temporarily disable cancellation for the
* calling thread, which is necessary in cases when a @cancellationpoint
* function is invoked from an @asyncsignalsafe function.
*
* Cosmopolitan Libc supports the Musl Libc `PTHREAD_CANCEL_MASKED`
* non-POSIX extension. Any thread may use this setting, in which case
* the thread won't be abruptly destroyed upon a cancellation and have
* its stack unwound; instead, the thread will encounter an `ECANCELED`
* errno the next time it calls a cancellation point.
*
* @param state may be one of:
* - `PTHREAD_CANCEL_ENABLE` (default)
* - `PTHREAD_CANCEL_DISABLE`
* - `PTHREAD_CANCEL_MASKED`
* @param oldstate optionally receives old value
* @return 0 on success, or errno on error
* @raise EINVAL if `state` has bad value
* @see pthread_cancel() for docs
* @asyncsignalsafe
*/
int pthread_setcancelstate(int state, int *oldstate) {
struct PosixThread *pt;
switch (state) {
case PTHREAD_CANCEL_ENABLE:
case PTHREAD_CANCEL_DISABLE:
case PTHREAD_CANCEL_MASKED:
pt = (struct PosixThread *)__get_tls()->tib_pthread;
if (oldstate) *oldstate = pt->canceldisable;
pt->canceldisable = state;
if (oldstate) {
if (pt->flags & PT_NOCANCEL) {
*oldstate = PTHREAD_CANCEL_DISABLE;
} else if (pt->flags & PT_MASKED) {
*oldstate = PTHREAD_CANCEL_MASKED;
} else {
*oldstate = PTHREAD_CANCEL_ENABLE;
}
}
pt->flags &= ~(PT_NOCANCEL | PT_MASKED);
if (state == PTHREAD_CANCEL_MASKED) {
pt->flags |= PT_MASKED;
} else if (state == PTHREAD_CANCEL_DISABLE) {
pt->flags |= PT_NOCANCEL;
}
return 0;
default:
return EINVAL;

View file

@ -26,7 +26,7 @@
*
* @param type may be one of:
* - `PTHREAD_CANCEL_DEFERRED` (default)
* - `PTHREAD_CANCEL_ASYNCHRONOUS`
* - `PTHREAD_CANCEL_ASYNCHRONOUS` (cray cray)
* @param oldtype optionally receives old value
* @return 0 on success, or errno on error
* @raise EINVAL if `type` has bad value
@ -38,8 +38,18 @@ int pthread_setcanceltype(int type, int *oldtype) {
case PTHREAD_CANCEL_DEFERRED:
case PTHREAD_CANCEL_ASYNCHRONOUS:
pt = (struct PosixThread *)__get_tls()->tib_pthread;
if (oldtype) *oldtype = pt->cancelasync;
pt->cancelasync = type;
if (oldtype) {
if (pt->flags & PT_ASYNC) {
*oldtype = PTHREAD_CANCEL_ASYNCHRONOUS;
} else {
*oldtype = PTHREAD_CANCEL_DEFERRED;
}
}
if (type == PTHREAD_CANCEL_DEFERRED) {
pt->flags &= ~PT_ASYNC;
} else {
pt->flags |= PT_ASYNC;
}
return 0;
default:
return EINVAL;

View file

@ -60,6 +60,7 @@ static struct timespec *sem_timeout(struct timespec *memory,
* @raise EDEADLK if deadlock was detected
* @raise ETIMEDOUT if deadline expired
* @raise EINVAL if `sem` is invalid
* @cancellationpoint
*/
int sem_timedwait(sem_t *sem, const struct timespec *abstime) {
int e, i, v, rc;

View file

@ -25,6 +25,7 @@
* @raise EINTR if signal was delivered instead
* @raise EDEADLK if deadlock was detected
* @raise EINVAL if `sem` is invalid
* @cancellationpoint
*/
int sem_wait(sem_t *sem) {
return sem_timedwait(sem, 0);

View file

@ -27,6 +27,7 @@
#define PTHREAD_CANCEL_ENABLE 0
#define PTHREAD_CANCEL_DISABLE 1
#define PTHREAD_CANCEL_MASKED 2
#define PTHREAD_CANCEL_DEFERRED 0
#define PTHREAD_CANCEL_ASYNCHRONOUS 1
@ -109,6 +110,8 @@ int pthread_create(pthread_t *, const pthread_attr_t *, void *(*)(void *),
void *);
int pthread_yield(void);
void pthread_testcancel(void);
int pthread_testcancel_np(void);
void pthread_exit(void *) wontreturn;
pthread_t pthread_self(void) pureconst;
pthread_id_np_t pthread_getthreadid_np(void);

View file

@ -50,11 +50,6 @@ $(LIBC_THREAD_A).pkg: \
$(LIBC_THREAD_A_OBJS) \
$(foreach x,$(LIBC_THREAD_A_DIRECTDEPS),$($(x)_A).pkg)
$(LIBC_THREAD_A_OBJS): private \
OVERRIDE_CCFLAGS += \
-ffunction-sections \
-fdata-sections
LIBC_THREAD_LIBS = $(foreach x,$(LIBC_THREAD_ARTIFACTS),$($(x)))
LIBC_THREAD_SRCS = $(foreach x,$(LIBC_THREAD_ARTIFACTS),$($(x)_SRCS))
LIBC_THREAD_HDRS = $(foreach x,$(LIBC_THREAD_ARTIFACTS),$($(x)_HDRS))