Make read() and write() signal handling atomic

You would think this is an important bug fix, but unfortunately all UNIX
implementations I've evaluated have a bug in read that causes signals to
not be handled atomically. The only exception is the latest iteration of
Cosmopolitan's read/write polyfill on Windows, which is somewhat ironic.
This commit is contained in:
Justine Tunney 2024-09-15 00:03:48 -07:00
parent c260144843
commit baf70af780
No known key found for this signature in database
GPG key ID: BE714B4575D6E328
12 changed files with 520 additions and 153 deletions

View file

@ -21,12 +21,16 @@
#include "libc/intrin/weaken.h" #include "libc/intrin/weaken.h"
#include "libc/thread/posixthread.internal.h" #include "libc/thread/posixthread.internal.h"
textwindows bool _is_canceled(void) {
struct PosixThread *pt;
return _weaken(_pthread_cancel_ack) && (pt = _pthread_self()) &&
atomic_load_explicit(&pt->pt_canceled, memory_order_acquire) &&
!(pt->pt_flags & PT_NOCANCEL);
}
textwindows int _check_cancel(void) { textwindows int _check_cancel(void) {
if (_weaken(_pthread_cancel_ack) && // if (_is_canceled())
_pthread_self() && !(_pthread_self()->pt_flags & PT_NOCANCEL) && // once acknowledged _is_canceled() will return false
atomic_load_explicit(&_pthread_self()->pt_canceled,
memory_order_acquire)) {
return _weaken(_pthread_cancel_ack)(); return _weaken(_pthread_cancel_ack)();
}
return 0; return 0;
} }

View file

@ -43,6 +43,7 @@ forceinline bool __isfdkind(int fd, int kind) {
int _check_signal(bool); int _check_signal(bool);
int _check_cancel(void); int _check_cancel(void);
bool _is_canceled(void);
int sys_close_nt(int, int); int sys_close_nt(int, int);
int _park_norestart(uint32_t, uint64_t); int _park_norestart(uint32_t, uint64_t);
int _park_restartable(uint32_t, uint64_t); int _park_restartable(uint32_t, uint64_t);

View file

@ -0,0 +1,27 @@
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi
Copyright 2024 Justine Alexandra Roberts Tunney
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/calls.h"
#include "libc/calls/syscall-sysv.internal.h"
#include "libc/calls/syscall_support-sysv.internal.h"
#include "libc/dce.h"
#include "libc/errno.h"
bool IsLinuxModern(void) {
return IsLinux() && sys_close_range(-1, -2, 0) == -1 && errno == EINVAL;
}

View file

@ -16,21 +16,25 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE. PERFORMANCE OF THIS SOFTWARE.
*/ */
#include "libc/assert.h"
#include "libc/calls/createfileflags.internal.h" #include "libc/calls/createfileflags.internal.h"
#include "libc/calls/internal.h" #include "libc/calls/internal.h"
#include "libc/calls/sig.internal.h" #include "libc/calls/sig.internal.h"
#include "libc/calls/struct/sigset.h" #include "libc/calls/struct/sigset.h"
#include "libc/calls/syscall_support-nt.internal.h" #include "libc/calls/syscall_support-nt.internal.h"
#include "libc/errno.h"
#include "libc/intrin/fds.h" #include "libc/intrin/fds.h"
#include "libc/intrin/weaken.h" #include "libc/intrin/weaken.h"
#include "libc/nt/enum/filetype.h" #include "libc/nt/enum/filetype.h"
#include "libc/nt/errors.h" #include "libc/nt/errors.h"
#include "libc/nt/events.h" #include "libc/nt/events.h"
#include "libc/nt/files.h" #include "libc/nt/files.h"
#include "libc/nt/process.h"
#include "libc/nt/runtime.h" #include "libc/nt/runtime.h"
#include "libc/nt/struct/overlapped.h" #include "libc/nt/struct/overlapped.h"
#include "libc/nt/synchronization.h" #include "libc/nt/synchronization.h"
#include "libc/nt/thread.h" #include "libc/nt/thread.h"
#include "libc/sock/internal.h"
#include "libc/stdio/sysparam.h" #include "libc/stdio/sysparam.h"
#include "libc/sysv/consts/sicode.h" #include "libc/sysv/consts/sicode.h"
#include "libc/sysv/errfuns.h" #include "libc/sysv/errfuns.h"
@ -47,8 +51,6 @@ sys_readwrite_nt(int fd, void *data, size_t size, ssize_t offset,
int64_t handle, sigset_t waitmask, int64_t handle, sigset_t waitmask,
bool32 ReadOrWriteFile(int64_t, void *, uint32_t, uint32_t *, bool32 ReadOrWriteFile(int64_t, void *, uint32_t, uint32_t *,
struct NtOverlapped *)) { struct NtOverlapped *)) {
int sig;
uint32_t exchanged;
struct Fd *f = g_fds.p + fd; struct Fd *f = g_fds.p + fd;
// pread() and pwrite() perform an implicit lseek() operation, so // pread() and pwrite() perform an implicit lseek() operation, so
@ -60,105 +62,153 @@ sys_readwrite_nt(int fd, void *data, size_t size, ssize_t offset,
if (pwriting && !seekable) if (pwriting && !seekable)
return espipe(); return espipe();
// determine if we need to lock a file descriptor across processes // determine if we need to lock file descriptor
bool locked = isdisk && !pwriting && f->cursor; bool locked = isdisk && !pwriting && f->cursor;
if (locked)
__cursor_lock(f->cursor);
RestartOperation: for (;;) {
// when a file is opened in overlapped mode win32 requires that we int got_sig = 0;
// take over full responsibility for managing our own file pointer bool got_eagain = false;
// which is fine, because the one win32 has was never very good in uint32_t other_error = 0;
// the sense that it behaves so differently from linux, that using
// win32 i/o required more compatibilty toil than doing it by hand
if (!pwriting) {
if (seekable && f->cursor) {
offset = f->cursor->shared->pointer;
} else {
offset = 0;
}
}
bool eagained = false; // create event handle for overlapped i/o
// check for signals and cancelation intptr_t event;
if (_check_cancel() == -1) { if (!(event = CreateEvent(0, 1, 0, 0)))
return __winerr();
// ensure iops are ordered across threads and processes if seeking
if (locked) if (locked)
__cursor_unlock(f->cursor); __cursor_lock(f->cursor);
return -1; // ECANCELED
}
if (_weaken(__sig_get) && (sig = _weaken(__sig_get)(waitmask)))
goto HandleInterrupt;
// signals have already been fully blocked by caller // when a file is opened in overlapped mode win32 requires that we
// perform i/o operation with atomic signal/cancel checking // take over full responsibility for managing our own file pointer
struct NtOverlapped overlap = {.hEvent = CreateEvent(0, 1, 0, 0), // which is fine, because the one win32 has was never very good in
.Pointer = offset}; // the sense that it behaves so differently from linux, that using
bool32 ok = ReadOrWriteFile(handle, data, size, 0, &overlap); // win32 i/o required more compatibilty toil than doing it by hand
if (!ok && GetLastError() == kNtErrorIoPending) { if (!pwriting) {
// win32 says this i/o operation needs to block if (seekable && f->cursor) {
if (f->flags & _O_NONBLOCK) { offset = f->cursor->shared->pointer;
// abort the i/o operation if file descriptor is in non-blocking mode } else {
CancelIoEx(handle, &overlap); offset = 0;
eagained = true; }
} else {
// wait until i/o either completes or is canceled by another thread
// we avoid a race condition by having a second mask for unblocking
struct PosixThread *pt;
pt = _pthread_self();
pt->pt_blkmask = waitmask;
pt->pt_iohandle = handle;
pt->pt_ioverlap = &overlap;
atomic_store_explicit(&pt->pt_blocker, PT_BLOCKER_IO,
memory_order_release);
WaitForSingleObject(overlap.hEvent, -1u);
atomic_store_explicit(&pt->pt_blocker, 0, memory_order_release);
} }
ok = true;
}
if (ok)
ok = GetOverlappedResult(handle, &overlap, &exchanged, true);
CloseHandle(overlap.hEvent);
// if i/o succeeded then return its result // initiate asynchronous i/o operation with win32
if (ok) { struct NtOverlapped overlap = {.hEvent = event, .Pointer = offset};
if (!pwriting && seekable && f->cursor) bool32 ok = ReadOrWriteFile(handle, data, size, 0, &overlap);
f->cursor->shared->pointer = offset + exchanged; if (!ok && GetLastError() == kNtErrorIoPending) {
if (locked) if (f->flags & _O_NONBLOCK) {
__cursor_unlock(f->cursor); // immediately back out of blocking i/o if non-blocking
return exchanged; CancelIoEx(handle, &overlap);
} got_eagain = true;
} else {
// atomic block on i/o completion, signal, or cancel
// it's not safe to acknowledge cancelation from here
// it's not safe to call any signal handlers from here
intptr_t sem;
if ((sem = CreateSemaphore(0, 0, 1, 0))) {
// installing semaphore before sig get makes wait atomic
struct PosixThread *pt = _pthread_self();
pt->pt_semaphore = sem;
pt->pt_blkmask = waitmask;
atomic_store_explicit(&pt->pt_blocker, PT_BLOCKER_SEM,
memory_order_release);
if (_is_canceled()) {
CancelIoEx(handle, &overlap);
} else if (_weaken(__sig_get) &&
(got_sig = _weaken(__sig_get)(waitmask))) {
CancelIoEx(handle, &overlap);
} else {
intptr_t hands[] = {event, sem};
uint32_t wi = WaitForMultipleObjects(2, hands, 0, -1u);
if (wi == 1) { // semaphore was signaled by signal enqueue
CancelIoEx(handle, &overlap);
if (_weaken(__sig_get))
got_sig = _weaken(__sig_get)(waitmask);
} else if (wi == -1u) {
other_error = GetLastError();
CancelIoEx(handle, &overlap);
}
}
atomic_store_explicit(&pt->pt_blocker, 0, memory_order_release);
CloseHandle(sem);
} else {
other_error = GetLastError();
CancelIoEx(handle, &overlap);
}
}
ok = true;
}
uint32_t exchanged = 0;
if (ok)
ok = GetOverlappedResult(handle, &overlap, &exchanged, true);
uint32_t io_error = GetLastError();
CloseHandle(event);
// only raise EINTR or EAGAIN if I/O got canceled // check if i/o completed
if (GetLastError() == kNtErrorOperationAborted) { // this could forseeably happen even if CancelIoEx was called
// raise EAGAIN if it's due to O_NONBLOCK mmode if (ok) {
if (eagained) { if (!pwriting && seekable && f->cursor)
f->cursor->shared->pointer = offset + exchanged;
if (locked) if (locked)
__cursor_unlock(f->cursor); __cursor_unlock(f->cursor);
if (got_sig) // swallow dequeued signal
_weaken(__sig_relay)(got_sig, SI_KERNEL, waitmask);
return exchanged;
}
// it's now safe to unlock cursor
if (locked)
__cursor_unlock(f->cursor);
// check if i/o failed
if (io_error != kNtErrorOperationAborted) {
if (got_sig) // swallow dequeued signal
_weaken(__sig_relay)(got_sig, SI_KERNEL, waitmask);
// read() and write() have different error paths
SetLastError(io_error);
return -2;
}
// the i/o operation was successfully canceled
if (got_eagain) {
unassert(!got_sig);
return eagain(); return eagain();
} }
// otherwise it must be due to a kill() via __sig_cancel()
if (_weaken(__sig_relay) && (sig = _weaken(__sig_get)(waitmask))) {
HandleInterrupt:
if (locked)
__cursor_unlock(f->cursor);
int handler_was_called = _weaken(__sig_relay)(sig, SI_KERNEL, waitmask);
if (_check_cancel() == -1)
return -1; // possible if we SIGTHR'd
if (locked)
__cursor_lock(f->cursor);
// read() is @restartable unless non-SA_RESTART hands were called
if (!(handler_was_called & SIG_HANDLED_NO_RESTART))
goto RestartOperation;
}
if (locked)
__cursor_unlock(f->cursor);
return eintr();
}
// read() and write() have generally different error-handling paths // it's now reasonable to report semaphore creation error
if (locked) if (other_error) {
__cursor_unlock(f->cursor); unassert(!got_sig);
return -2; errno = __dos2errno(other_error);
return -1;
}
// check for thread cancelation and acknowledge
if (_check_cancel() == -1)
return -1;
// if signal module has been linked, then
if (_weaken(__sig_get)) {
// gobble up all unmasked pending signals
// it's now safe to recurse into signal handlers
int handler_was_called = 0;
do {
if (got_sig)
handler_was_called |=
_weaken(__sig_relay)(got_sig, SI_KERNEL, waitmask);
} while ((got_sig = _weaken(__sig_get)(waitmask)));
// check if SIGTHR handler was called
if (_check_cancel() == -1)
return -1;
// check if signal handler without SA_RESTART was called
if (handler_was_called & SIG_HANDLED_NO_RESTART)
return eintr();
}
// otherwise try the i/o operation again
}
} }
#endif /* __x86_64__ */ #endif /* __x86_64__ */

View file

@ -32,6 +32,7 @@
#include "libc/sysv/consts/sig.h" #include "libc/sysv/consts/sig.h"
#include "libc/sysv/errfuns.h" #include "libc/sysv/errfuns.h"
#include "libc/thread/posixthread.internal.h" #include "libc/thread/posixthread.internal.h"
#ifdef __x86_64__
textwindows static int sys_sigtimedwait_nt_check(sigset_t syncsigs, textwindows static int sys_sigtimedwait_nt_check(sigset_t syncsigs,
siginfo_t *opt_info, siginfo_t *opt_info,
@ -111,3 +112,5 @@ textwindows int sys_sigtimedwait_nt(const sigset_t *set, siginfo_t *opt_info,
ALLOW_SIGNALS; ALLOW_SIGNALS;
return rc; return rc;
} }
#endif /* __x86_64__ */

View file

@ -53,20 +53,17 @@ static textwindows ssize_t sys_write_nt_impl(int fd, void *data, size_t size,
bool isconsole = f->kind == kFdConsole; bool isconsole = f->kind == kFdConsole;
// not implemented, XNU returns eperm(); // not implemented, XNU returns eperm();
if (f->kind == kFdDevRandom) { if (f->kind == kFdDevRandom)
return eperm(); return eperm();
}
// determine win32 handle for writing // determine win32 handle for writing
int64_t handle = f->handle; int64_t handle = f->handle;
if (isconsole && _weaken(GetConsoleOutputHandle)) { if (isconsole && _weaken(GetConsoleOutputHandle))
handle = _weaken(GetConsoleOutputHandle)(); handle = _weaken(GetConsoleOutputHandle)();
}
// intercept ansi tty configuration sequences // intercept ansi tty configuration sequences
if (isconsole && _weaken(GetConsoleOutputHandle)) { if (isconsole && _weaken(GetConsoleOutputHandle))
_weaken(InterceptTerminalCommands)(data, size); _weaken(InterceptTerminalCommands)(data, size);
}
// perform heavy lifting // perform heavy lifting
ssize_t rc; ssize_t rc;

View file

@ -14,6 +14,7 @@ char *GetProgramExecutableName(void) libcesque;
void unleaf(void) libcesque; void unleaf(void) libcesque;
int __demangle(char *, const char *, size_t) libcesque; int __demangle(char *, const char *, size_t) libcesque;
int __is_mangled(const char *) libcesque; int __is_mangled(const char *) libcesque;
bool IsLinuxModern(void) libcesque;
int LoadZipArgs(int *, char ***) libcesque; int LoadZipArgs(int *, char ***) libcesque;
COSMOPOLITAN_C_END_ COSMOPOLITAN_C_END_

View file

@ -146,7 +146,7 @@ textwindows static int sys_connect_nt_impl(struct Fd *f, const void *addr,
// check if we still need more time // check if we still need more time
if (!ready) { if (!ready) {
if (f->flags & O_NONBLOCK) { if (f->flags & O_NONBLOCK) {
return ealready(); return etimedout();
} else { } else {
continue; continue;
} }

View file

@ -16,14 +16,20 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE. PERFORMANCE OF THIS SOFTWARE.
*/ */
#include "libc/assert.h"
#include "libc/calls/internal.h" #include "libc/calls/internal.h"
#include "libc/calls/sig.internal.h" #include "libc/calls/sig.internal.h"
#include "libc/calls/struct/sigset.h" #include "libc/calls/struct/sigset.h"
#include "libc/calls/struct/timespec.internal.h"
#include "libc/calls/syscall_support-nt.internal.h"
#include "libc/errno.h" #include "libc/errno.h"
#include "libc/intrin/atomic.h"
#include "libc/intrin/weaken.h" #include "libc/intrin/weaken.h"
#include "libc/nt/enum/wait.h" #include "libc/nt/enum/wait.h"
#include "libc/nt/errors.h" #include "libc/nt/errors.h"
#include "libc/nt/runtime.h"
#include "libc/nt/struct/overlapped.h" #include "libc/nt/struct/overlapped.h"
#include "libc/nt/synchronization.h"
#include "libc/nt/thread.h" #include "libc/nt/thread.h"
#include "libc/nt/winsock.h" #include "libc/nt/winsock.h"
#include "libc/sock/internal.h" #include "libc/sock/internal.h"
@ -39,69 +45,138 @@ __winsock_block(int64_t handle, uint32_t flags, bool nonblock,
uint32_t *flags, void *arg), uint32_t *flags, void *arg),
void *arg) { void *arg) {
RestartOperation: // convert relative to absolute timeout
int rc, sig, reason = 0; struct timespec deadline;
uint32_t status, exchanged; if (srwtimeout) {
if (_check_cancel() == -1) deadline = timespec_add(sys_clock_gettime_monotonic_nt(),
return -1; // ECANCELED timespec_frommillis(srwtimeout));
if (_weaken(__sig_get) && (sig = _weaken(__sig_get)(waitmask))) { } else {
goto HandleInterrupt; deadline = timespec_max;
} }
struct NtOverlapped overlap = {.hEvent = WSACreateEvent()}; for (;;) {
rc = StartSocketOp(handle, &overlap, &flags, arg); int got_sig = 0;
if (rc && WSAGetLastError() == kNtErrorIoPending) { bool got_cancel = false;
if (nonblock) { bool got_eagain = false;
CancelIoEx(handle, &overlap); uint32_t other_error = 0;
reason = EAGAIN;
} else { // create event handle for overlapped i/o
struct PosixThread *pt; intptr_t event;
pt = _pthread_self(); if (!(event = WSACreateEvent()))
pt->pt_blkmask = waitmask; return __winsockerr();
pt->pt_iohandle = handle;
pt->pt_ioverlap = &overlap; struct NtOverlapped overlap = {.hEvent = event};
atomic_store_explicit(&pt->pt_blocker, PT_BLOCKER_IO, bool32 ok = !StartSocketOp(handle, &overlap, &flags, arg);
memory_order_release); if (!ok && WSAGetLastError() == kNtErrorIoPending) {
status = WSAWaitForMultipleEvents(1, &overlap.hEvent, 0, if (nonblock) {
srwtimeout ? srwtimeout : -1u, 0);
atomic_store_explicit(&pt->pt_blocker, 0, memory_order_release);
if (status) {
if (status == kNtWaitTimeout) {
reason = EAGAIN; // SO_RCVTIMEO or SO_SNDTIMEO elapsed
} else {
reason = WSAGetLastError(); // ENETDOWN or ENOBUFS
}
CancelIoEx(handle, &overlap); CancelIoEx(handle, &overlap);
got_eagain = true;
} else {
// atomic block on i/o completion, signal, or cancel
// it's not safe to acknowledge cancelation from here
// it's not safe to call any signal handlers from here
intptr_t sem;
if ((sem = CreateSemaphore(0, 0, 1, 0))) {
// installing semaphore before sig get makes wait atomic
struct PosixThread *pt = _pthread_self();
pt->pt_semaphore = sem;
pt->pt_blkmask = waitmask;
atomic_store_explicit(&pt->pt_blocker, PT_BLOCKER_SEM,
memory_order_release);
if (_is_canceled()) {
got_cancel = true;
CancelIoEx(handle, &overlap);
} else if (_weaken(__sig_get) &&
(got_sig = _weaken(__sig_get)(waitmask))) {
CancelIoEx(handle, &overlap);
} else {
struct timespec now = sys_clock_gettime_monotonic_nt();
struct timespec remain = timespec_subz(deadline, now);
int64_t millis = timespec_tomillis(remain);
uint32_t waitms = MIN(millis, 0xffffffffu);
intptr_t hands[] = {event, sem};
uint32_t wi = WSAWaitForMultipleEvents(2, hands, 0, waitms, 0);
if (wi == 1) { // semaphore was signaled by signal enqueue
CancelIoEx(handle, &overlap);
if (_weaken(__sig_get))
got_sig = _weaken(__sig_get)(waitmask);
} else if (wi == kNtWaitTimeout) {
CancelIoEx(handle, &overlap);
got_eagain = true;
} else if (wi == -1u) {
other_error = WSAGetLastError();
CancelIoEx(handle, &overlap);
}
}
atomic_store_explicit(&pt->pt_blocker, 0, memory_order_release);
CloseHandle(sem);
} else {
other_error = GetLastError();
CancelIoEx(handle, &overlap);
}
} }
ok = true;
} }
rc = 0; uint32_t exchanged = 0;
} if (ok)
if (!rc) { ok = WSAGetOverlappedResult(handle, &overlap, &exchanged, true, &flags);
rc = WSAGetOverlappedResult(handle, &overlap, &exchanged, true, &flags) uint32_t io_error = WSAGetLastError();
? 0 WSACloseEvent(event);
: -1;
}
WSACloseEvent(overlap.hEvent);
if (!rc) { // check if i/o completed
return exchanged; // this could forseeably happen even if CancelIoEx was called
} if (ok) {
if (WSAGetLastError() == kNtErrorOperationAborted) { if (got_sig) // swallow dequeued signal
if (reason) { _weaken(__sig_relay)(got_sig, SI_KERNEL, waitmask);
errno = reason; return exchanged;
}
// check if i/o failed
if (io_error != kNtErrorOperationAborted) {
if (got_sig) // swallow dequeued signal
_weaken(__sig_relay)(got_sig, SI_KERNEL, waitmask);
errno = __dos2errno(io_error);
return -1; return -1;
} }
if (_weaken(__sig_get) && (sig = _weaken(__sig_get)(waitmask))) {
HandleInterrupt: // it's now reasonable to report semaphore creation error
int handler_was_called = _weaken(__sig_relay)(sig, SI_KERNEL, waitmask); if (other_error) {
unassert(!got_sig);
errno = __dos2errno(other_error);
return -1;
}
// check for non-block cancel or timeout
if (got_eagain && !got_sig && !got_cancel)
return eagain();
// check for thread cancelation and acknowledge
if (_check_cancel() == -1)
return -1;
// if signal module has been linked, then
if (_weaken(__sig_get)) {
// gobble up all unmasked pending signals
// it's now safe to recurse into signal handlers
int handler_was_called = 0;
do {
if (got_sig)
handler_was_called |=
_weaken(__sig_relay)(got_sig, SI_KERNEL, waitmask);
} while ((got_sig = _weaken(__sig_get)(waitmask)));
// check if SIGTHR handler was called
if (_check_cancel() == -1) if (_check_cancel() == -1)
return -1; return -1;
if (handler_was_called != 1)
goto RestartOperation; // check if signal handler without SA_RESTART was called
if (handler_was_called & SIG_HANDLED_NO_RESTART)
return eintr();
} }
return eintr();
// otherwise try the i/o operation again
} }
return __winsockerr();
} }
#endif /* __x86_64__ */ #endif /* __x86_64__ */

View file

@ -465,7 +465,7 @@ syscon rlimit RLIMIT_RSS 5 5 5 5 5 5 5 127 # max physical mem
syscon rlimit RLIMIT_NPROC 6 6 7 7 7 7 7 127 # max number of processes; see fork()→EAGAIN; bsd consensus syscon rlimit RLIMIT_NPROC 6 6 7 7 7 7 7 127 # max number of processes; see fork()→EAGAIN; bsd consensus
syscon rlimit RLIMIT_NOFILE 7 7 8 8 8 8 8 127 # max number of open files; see accept()→EMFILE/ENFILE; bsd consensus syscon rlimit RLIMIT_NOFILE 7 7 8 8 8 8 8 127 # max number of open files; see accept()→EMFILE/ENFILE; bsd consensus
syscon rlimit RLIMIT_MEMLOCK 8 8 6 6 6 6 6 127 # max locked-in-memory address space; bsd consensus syscon rlimit RLIMIT_MEMLOCK 8 8 6 6 6 6 6 127 # max locked-in-memory address space; bsd consensus
syscon rlimit RLIMIT_AS 9\ 9 5 5 10 2 10 0 # max virtual memory size in bytes; this one actually works; fudged as RLIMIT_DATA on OpenBSD syscon rlimit RLIMIT_AS 9 9 5 5 10 2 10 0 # max virtual memory size in bytes; this one actually works; fudged as RLIMIT_DATA on OpenBSD
syscon rlimit RLIMIT_LOCKS 10 10 127 127 127 127 127 127 # max flock() / fcntl() locks; bsd consensus syscon rlimit RLIMIT_LOCKS 10 10 127 127 127 127 127 127 # max flock() / fcntl() locks; bsd consensus
syscon rlimit RLIMIT_SIGPENDING 11 11 127 127 127 127 127 127 # max sigqueue() can enqueue; bsd consensus syscon rlimit RLIMIT_SIGPENDING 11 11 127 127 127 127 127 127 # max sigqueue() can enqueue; bsd consensus
syscon rlimit RLIMIT_MSGQUEUE 12 12 127 127 127 127 127 127 # meh posix message queues; bsd consensus syscon rlimit RLIMIT_MSGQUEUE 12 12 127 127 127 127 127 127 # meh posix message queues; bsd consensus

View file

@ -1,2 +1,2 @@
#include "libc/sysv/consts/syscon.internal.h" #include "libc/sysv/consts/syscon.internal.h"
.syscon rlimit,RLIMIT_AS,9 ,9,5,5,10,2,10,0 .syscon rlimit,RLIMIT_AS,9,9,5,5,10,2,10,0

View file

@ -0,0 +1,209 @@
// Copyright 2024 Justine Alexandra Roberts Tunney
//
// Permission to use, copy, modify, and/or distribute this software for
// any purpose with or without fee is hereby granted, provided that the
// above copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
// PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#include <cosmo.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/**
* @fileoverview i/o signal handling torture test
*
* This test tries to trigger race conditions in the kernel's read()
* implementation, by sending a massive amount of SA_RESTART signals
* which cause churn in its internal code, and finally an interrupt.
* This should reveal if the kernel code that checks for any pending
* signals before blocking on i/o happens non-atomically. Expect the
* test to hang indefinitely in such cases.
*
* "This flag affects the behavior of interruptible functions; that is,
* those specified to fail with errno set to EINTR. If set, and a
* function specified as interruptible is interrupted by this signal,
* the function shall restart and shall not fail with EINTR unless
* otherwise specified. If an interruptible function which uses a
* timeout is restarted, the duration of the timeout following the
* restart is set to an unspecified value that does not exceed the
* original timeout value. If the flag is not set, interruptible
* functions interrupted by this signal shall fail with errno set to
* EINTR." —Quoth IEEE Std 1003.1-2017 (POSIX.1) on SA_RESTART
*
* Every OS except Windows fails this test.
*
* @see sys_readwrite_nt()
*/
#define COUNT 1000
volatile sig_atomic_t got_sigusr1;
volatile sig_atomic_t got_sigusr2;
volatile sig_atomic_t thread_ready;
volatile sig_atomic_t read_interrupted;
void sigusr1_handler(int signo) {
++got_sigusr1;
// printf("got %d sigusr1\n", got_sigusr1);
}
void sigusr2_handler(int signo) {
++got_sigusr2;
// printf("got %d sigusr2\n", got_sigusr2);
}
void setup_signal_handlers() {
struct sigaction sa;
// Set up SIGUSR1 handler with SA_RESTART
sa.sa_handler = sigusr1_handler;
sa.sa_flags = SA_RESTART; // Signal handler with SA_RESTART
sigemptyset(&sa.sa_mask);
if (sigaction(SIGUSR1, &sa, NULL) == -1)
exit(97);
// Set up SIGUSR2 handler without SA_RESTART
sa.sa_handler = sigusr2_handler;
sa.sa_flags = 0; // Signal handler without SA_RESTART
sigemptyset(&sa.sa_mask);
if (sigaction(SIGUSR2, &sa, NULL) == -1)
exit(98);
}
void block_signals() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigaddset(&set, SIGUSR2);
if (pthread_sigmask(SIG_BLOCK, &set, 0))
exit(99);
}
void *thread_func(void *arg) {
int *pipefd = (int *)arg;
char buf[1];
ssize_t ret;
// Unblock SIGUSR1 and SIGUSR2 in this thread
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigaddset(&set, SIGUSR2);
if (pthread_sigmask(SIG_UNBLOCK, &set, 0))
exit(100);
// Indicate that the thread is ready
thread_ready = 1;
// Call read() on the pipe
ret = read(pipefd[0], buf, 1);
if (ret == -1) {
if (errno == EINTR) {
read_interrupted = 1;
// printf("read interrupted\n");
} else {
perror("read");
exit(78);
}
} else {
exit(77);
}
return NULL;
}
int main() {
int pipefd[2];
pthread_t thread;
// this test exposes bugs in macos
if (IsXnu())
return 0;
// this test exposes bugs in linux
if (IsLinux())
return 0;
// this test exposes bugs in netbsd
if (IsNetbsd())
return 0;
// this test exposes bugs in freebsd
if (IsFreebsd())
return 0;
// this test exposes bugs in openbsd
if (IsOpenbsd())
return 0;
ShowCrashReports();
// Block SIGUSR1 and SIGUSR2 in the main thread
block_signals();
// Set up signal handlers
setup_signal_handlers();
// Create a pipe
if (pipe(pipefd) == -1)
exit(95);
// Create a thread
if (pthread_create(&thread, NULL, thread_func, pipefd) != 0)
exit(90);
// Wait until the thread is ready
while (!thread_ready)
if (pthread_yield_np())
exit(101);
// Send SIGUSR1 signals
// This will cause read() to restart internally
for (int i = 0; i < COUNT; i++) {
if (pthread_kill(thread, SIGUSR1) != 0)
exit(91);
if (i % (COUNT / 10) == 0)
usleep(1);
}
// Send SIGUSR2 to the thread
// This will trigger an EINTR
fflush(stdout);
if (pthread_kill(thread, SIGUSR2))
exit(92);
// Join the thread
if (pthread_join(thread, NULL))
exit(93);
// Close the pipe
close(pipefd[0]);
close(pipefd[1]);
// Check if read() was interrupted by EINTR
if (!read_interrupted)
exit(94);
if (!got_sigusr1)
exit(60);
if (!got_sigusr2)
exit(61);
// printf("got %d got_sigusr1\n", got_sigusr1);
CheckForMemoryLeaks();
return 0;
}