mirror of
https://github.com/jart/cosmopolitan.git
synced 2025-02-07 06:53:33 +00:00
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:
parent
c260144843
commit
baf70af780
12 changed files with 520 additions and 153 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
27
libc/calls/islinuxmodern.c
Normal file
27
libc/calls/islinuxmodern.c
Normal 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;
|
||||||
|
}
|
|
@ -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__ */
|
||||||
|
|
|
@ -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__ */
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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_
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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__ */
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
209
test/posix/signal_torture_read_test.c
Normal file
209
test/posix/signal_torture_read_test.c
Normal 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;
|
||||||
|
}
|
Loading…
Reference in a new issue