Improve cancellations, randomness, and time

- Exhaustively document cancellation points
- Rename SIGCANCEL to SIGTHR just like BSDs
- Further improve POSIX thread cancellations
- Ensure asynchronous cancellations work correctly
- Elevate the quality of getrandom() and getentropy()
- Make futexes cancel correctly on OpenBSD 6.x and 7.x
- Add reboot.com and shutdown.com to examples directory
- Remove underscore prefix from awesome timespec_*() APIs
- Create assertions that help verify our cancellation points
- Remove bad timespec APIs (cmp generalizes eq/ne/gt/gte/lt/lte)
This commit is contained in:
Justine Tunney 2022-11-05 19:49:41 -07:00
parent 0d7c265392
commit 3f0bcdc3ef
No known key found for this signature in database
GPG key ID: BE714B4575D6E328
173 changed files with 1599 additions and 782 deletions

View file

@ -6,11 +6,13 @@
#include "libc/thread/thread.h"
#include "libc/thread/tls.h"
#define PT_OWNSTACK 1
#define PT_MAINTHREAD 2
#define PT_ASYNC 4
#define PT_NOCANCEL 8
#define PT_MASKED 16
#define PT_OWNSTACK 1
#define PT_MAINTHREAD 2
#define PT_ASYNC 4
#define PT_NOCANCEL 8
#define PT_MASKED 16
#define PT_INCANCEL 32
#define PT_OPENBSD_KLUDGE 64
#if !(__ASSEMBLER__ + __LINKER__ + 0)
COSMOPOLITAN_C_START_

View file

@ -18,6 +18,7 @@
*/
#include "libc/assert.h"
#include "libc/errno.h"
#include "libc/intrin/atomic.h"
#include "libc/intrin/kmalloc.h"
#include "libc/runtime/memtrack.internal.h"
#include "libc/str/str.h"
@ -65,8 +66,14 @@ void _pthread_onfork_parent(void) {
}
void _pthread_onfork_child(void) {
struct CosmoTib *tib;
struct PosixThread *pt;
pthread_mutexattr_t attr;
extern pthread_mutex_t __mmi_lock_obj;
tib = __get_tls();
pt = (struct PosixThread *)tib->tib_pthread;
atomic_store_explicit(&pt->cancelled, false, memory_order_relaxed);
pt->tid = atomic_load_explicit(&tib->tib_tid, memory_order_relaxed);
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&__mmi_lock_obj, &attr);

View file

@ -21,7 +21,9 @@
#include "libc/calls/struct/sigaction.h"
#include "libc/calls/struct/siginfo.h"
#include "libc/calls/struct/sigset.h"
#include "libc/calls/syscall_support-sysv.internal.h"
#include "libc/calls/ucontext.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/intrin/atomic.h"
#include "libc/intrin/kprintf.h"
@ -45,7 +47,7 @@ int _pthread_cancel_sys(void) {
if (!(pt->flags & (PT_NOCANCEL | PT_MASKED)) || (pt->flags & PT_ASYNC)) {
pthread_exit(PTHREAD_CANCELED);
}
pt->flags |= PT_NOCANCEL;
pt->flags |= PT_NOCANCEL | PT_OPENBSD_KLUDGE;
return ecanceled();
}
@ -56,12 +58,14 @@ static void OnSigCancel(int sig, siginfo_t *si, void *ctx) {
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)) {
if (systemfive_cancellable <= (char *)uc->uc_mcontext.rip &&
(char *)uc->uc_mcontext.rip < systemfive_cancellable_end) {
uc->uc_mcontext.rip = (intptr_t)systemfive_cancel;
} else if (pt->flags & PT_ASYNC) {
pthread_exit(PTHREAD_CANCELED);
} else {
tkill(atomic_load_explicit(&tib->tib_tid, memory_order_relaxed), sig);
__tkill(atomic_load_explicit(&tib->tib_tid, memory_order_relaxed), sig,
tib);
}
}
}
@ -71,7 +75,7 @@ static void ListenForSigCancel(void) {
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));
_npassert(!sigaction(SIGTHR, &sa, 0));
}
/**
@ -79,28 +83,142 @@ static void ListenForSigCancel(void) {
*
* 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.
* before the cancellation occurred, in addition to destructing pthread
* keys, before finally, the thread shall abruptly exit.
*
* 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`, and `sem_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() will eagerly call pthread_testcancel_np() to
* help avoid the potential for resource leaks later on.
* By default, pthread_cancel() can only take effect when a thread
* reaches a cancellation point. Such functions are documented with
* @cancellationpoint. They check the cancellation state before the
* underlying system call is issued. If the system call is issued and
* blocks, then pthread_cancel() will interrupt the operation in which
* case the syscall wrapper will check the cancelled state a second
* time, only if the raw system call returned EINTR.
*
* The following system calls are implemented as cancellation points.
*
* - `accept4`
* - `accept`
* - `clock_nanosleep`
* - `connect`
* - `copy_file_range`
* - `creat`
* - `epoll_wait`
* - `fcntl(F_OFD_SETLKW)`
* - `fcntl(F_SETLKW)`
* - `fdatasync`
* - `flock`
* - `fstatfs`
* - `fsync`
* - `ftruncate`
* - `getrandom`
* - `msync`
* - `nanosleep`
* - `open`
* - `openat`
* - `pause`
* - `poll`
* - `ppoll`
* - `pread`
* - `preadv`
* - `pselect`
* - `pwrite`
* - `pwritev`
* - `read`
* - `readv`
* - `recvfrom`
* - `recvmsg`
* - `select`
* - `sendmsg`
* - `sendto`
* - `sigsuspend`
* - `sigtimedwait`
* - `sigwaitinfo`
* - `statfs`
* - `tcdrain`
* - `truncate`
* - `wait3`
* - `wait4`
* - `wait`
* - `waitpid`
* - `write`
* - `writev`
*
* The following library calls are implemented as cancellation points.
*
* - `fopen`
* - `gzopen`, `gzread`, `gzwrite`, etc.
* - `lockf(F_LOCK)`
* - `nsync_cv_wait_with_deadline`
* - `nsync_cv_wait`
* - `opendir`
* - `pclose`
* - `popen`
* - `fwrite`, `printf`, `fprintf`, `putc`, etc.
* - `pthread_cond_timedwait`
* - `pthread_cond_wait`
* - `pthread_join`
* - `sem_timedwait`
* - `sem_wait`
* - `sleep`
* - `timespec_sleep_until`
* - `tmpfd`
* - `tmpfile`
* - `usleep`
*
* Other userspace libraries provided by Cosmopolitan Libc that call the
* cancellation points above will block cancellations while running. The
* following are examples of functions that *aren't* cancellation points
*
* - `INFOF()`, `WARNF()`, etc.
* - `getentropy`
* - `gmtime_r`
* - `kprintf`
* - `localtime_r`
* - `nsync_mu_lock`
* - `nsync_mu_unlock`
* - `openpty`
* - `pthread_getname_np`
* - `pthread_mutex_lock`
* - `pthread_mutex_unlock`
* - `pthread_setname_np`
* - `sem_open`
* - `system`
* - `timespec_sleep`
* - `touch`
*
* The way to block cancellations temporarily is:
*
* int cs;
* pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cs);
* // ...
* pthread_setcancelstate(cs, 0);
*
* In order to support cancellations all your code needs to be rewritten
* so that when resources such as file descriptors are managed they must
* have a cleanup crew pushed to the stack. For example even malloc() is
* technically unsafe w.r.t. leaks without doing something like this:
*
* void *p = malloc(123);
* pthread_cleanup_push(free, p);
* read(0, p, 123);
* pthread_cleanup_pop(1);
*
* Consider using Cosmopolitan Libc's garbage collector since it will be
* executed when a thread exits due to a cancellation.
*
* void *p = _gc(malloc(123));
* read(0, p, 123);
*
* 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.
*
* pthread_setcancelstate(PTHREAD_CANCEL_ASYNCHRONOUS, 0);
* for (;;) donothing;
*
* In which case a thread may be cancelled at any assembly opcode. This
* is useful for immediately halting threads that consume cpu and don't
* use any system calls. It shouldn't be used on threads that will call
* cancellation points since in that case asynchronous mode could cause
* resource leaks to happen, in such a way that can't be worked around.
*
* If none of the above options seem savory to you, then a third way is
* offered for doing cancellations. Cosmopolitan Libc supports the Musl
@ -109,6 +227,33 @@ static void ListenForSigCancel(void) {
* 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.
* For example:
*
* pthread_setcancelstate(PTHREAD_CANCEL_MASKED, 0);
* void *p = malloc(123);
* int rc = read(0, p, 123);
* free(p);
* if (rc == ECANCELED) {
* pthread_exit(0);
* }
*
* Shows how the masked cancellations paradigm can be safely used. Note
* that it's so important that cancellation point error return codes be
* checked. Code such as the following:
*
* pthread_setcancelstate(PTHREAD_CANCEL_MASKED, 0);
* void *p = malloc(123);
* write(2, "log\n", 4); // XXX: fails to check result
* int rc = read(0, p, 123);
* free(p);
* if (rc == ECANCELED) {
* pthread_exit(0); // XXX: not run if write() was cancelled
* }
*
* Isn't safe to use in masked mode. That's because if a cancellation
* occurs during the write() operation then cancellations are blocked
* while running read(). Masked mode doesn't have second chances. You
* must rigorously check the results of each cancellation point call.
*
* @return 0 on success, or errno on error
* @raise ESRCH if thread isn't alive
@ -117,7 +262,6 @@ errno_t pthread_cancel(pthread_t thread) {
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)) {
@ -127,18 +271,18 @@ errno_t pthread_cancel(pthread_t thread) {
default:
break;
}
atomic_exchange_explicit(&pt->cancelled, 1, memory_order_release);
atomic_store_explicit(&pt->cancelled, 1, memory_order_release);
if (thread == __get_tls()->tib_pthread) {
if (!(pt->flags & (PT_NOCANCEL | PT_MASKED)) && (pt->flags & PT_ASYNC)) {
pthread_exit(PTHREAD_CANCELED);
}
return 0;
}
if (IsWindows()) return 0; // TODO(jart): Should we do this?
if (IsWindows()) return 0; // no true cancellations on Windows yet
tid = atomic_load_explicit(&pt->tib->tib_tid, memory_order_acquire);
if (tid <= 0) return 0; // TODO(jart): Do we need this?
if (tid <= 0) return 0; // -1 means still starting, 0 means exited
e = errno;
if (!tkill(tid, SIGCANCEL)) {
if (!__tkill(tid, SIGTHR, pt->tib)) {
return 0;
} else {
rc = errno;

View file

@ -27,7 +27,7 @@
* Waits for condition with optional time limit, e.g.
*
* struct timespec ts; // one second timeout
* ts = _timespec_add(_timespec_real(), _timespec_frommillis(1000));
* ts = timespec_add(timespec_real(), timespec_frommillis(1000));
* if (pthread_cond_timedwait(cond, mutex, &ts) == ETIMEDOUT) {
* // handle timeout...
* }

View file

@ -17,6 +17,7 @@
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/calls.h"
#include "libc/calls/syscall_support-sysv.internal.h"
#include "libc/errno.h"
#include "libc/thread/posixthread.internal.h"
#include "libc/thread/thread.h"
@ -34,7 +35,7 @@
errno_t pthread_kill(pthread_t thread, int sig) {
int rc, e = errno;
struct PosixThread *pt = (struct PosixThread *)thread;
if (!tkill(pt->tid, sig)) {
if (!__tkill(pt->tid, sig, pt->tib)) {
rc = 0;
} else {
rc = errno;

View file

@ -16,6 +16,7 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/thread/posixthread.internal.h"
#include "libc/thread/thread.h"
@ -26,17 +27,22 @@
*
* @param type may be one of:
* - `PTHREAD_CANCEL_DEFERRED` (default)
* - `PTHREAD_CANCEL_ASYNCHRONOUS` (cray cray)
* - `PTHREAD_CANCEL_ASYNCHRONOUS`
* @param oldtype optionally receives old value
* @return 0 on success, or errno on error
* @raise ENOTSUP on Windows if asynchronous
* @raise EINVAL if `type` has bad value
* @see pthread_cancel() for docs
*/
errno_t pthread_setcanceltype(int type, int *oldtype) {
struct PosixThread *pt;
switch (type) {
case PTHREAD_CANCEL_DEFERRED:
case PTHREAD_CANCEL_ASYNCHRONOUS:
if (IsWindows()) {
return ENOTSUP;
}
// fallthrough
case PTHREAD_CANCEL_DEFERRED:
pt = (struct PosixThread *)__get_tls()->tib_pthread;
if (oldtype) {
if (pt->flags & PT_ASYNC) {

View file

@ -17,6 +17,7 @@
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/assert.h"
#include "libc/calls/cp.internal.h"
#include "libc/calls/struct/timespec.h"
#include "libc/calls/struct/timespec.internal.h"
#include "libc/errno.h"
@ -42,11 +43,11 @@ static struct timespec *sem_timeout(struct timespec *memory,
*memory = *abstime;
return memory;
} else {
now = _timespec_real();
if (_timespec_cmp(now, *abstime) > 0) {
now = timespec_real();
if (timespec_cmp(now, *abstime) > 0) {
*memory = (struct timespec){0};
} else {
*memory = _timespec_sub(*abstime, now);
*memory = timespec_sub(*abstime, now);
}
return memory;
}
@ -87,6 +88,7 @@ int sem_timedwait(sem_t *sem, const struct timespec *abstime) {
}
}
BEGIN_CANCELLATION_POINT;
_unassert(atomic_fetch_add_explicit(&sem->sem_waiters, +1,
memory_order_acq_rel) >= 0);
pthread_cleanup_push(sem_timedwait_cleanup, sem);
@ -102,7 +104,7 @@ int sem_timedwait(sem_t *sem, const struct timespec *abstime) {
rc = 0;
} else if (rc == -ETIMEDOUT) {
_npassert(abstime);
if (_timespec_cmp(*abstime, _timespec_real()) <= 0) {
if (timespec_cmp(*abstime, timespec_real()) <= 0) {
rc = etimedout();
} else {
rc = 0;
@ -122,6 +124,7 @@ int sem_timedwait(sem_t *sem, const struct timespec *abstime) {
memory_order_relaxed)));
pthread_cleanup_pop(1);
END_CANCELLATION_POINT;
return rc;
}

View file

@ -16,6 +16,7 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/cp.internal.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/intrin/atomic.h"
@ -33,13 +34,17 @@
*
* @return 0 on success, or errno on error
* @raise ECANCELED if calling thread was cancelled in masked mode
* @cancellationpoint
*/
errno_t _wait0(const atomic_int *ctid) {
int x, rc;
int x, rc = 0;
BEGIN_CANCELLATION_POINT;
while ((x = atomic_load_explicit(ctid, memory_order_acquire))) {
if (nsync_futex_wait_(ctid, x, !IsWindows(), 0) == -ECANCELED) {
return ECANCELED;
rc = ECANCELED;
break;
}
}
return 0;
END_CANCELLATION_POINT;
return rc;
}