Skip to content

Commit baf70af

Browse files
committed
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.
1 parent c260144 commit baf70af

File tree

12 files changed

+520
-153
lines changed

12 files changed

+520
-153
lines changed

libc/calls/checkcancel.c

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@
2121
#include "libc/intrin/weaken.h"
2222
#include "libc/thread/posixthread.internal.h"
2323

24+
textwindows bool _is_canceled(void) {
25+
struct PosixThread *pt;
26+
return _weaken(_pthread_cancel_ack) && (pt = _pthread_self()) &&
27+
atomic_load_explicit(&pt->pt_canceled, memory_order_acquire) &&
28+
!(pt->pt_flags & PT_NOCANCEL);
29+
}
30+
2431
textwindows int _check_cancel(void) {
25-
if (_weaken(_pthread_cancel_ack) && //
26-
_pthread_self() && !(_pthread_self()->pt_flags & PT_NOCANCEL) &&
27-
atomic_load_explicit(&_pthread_self()->pt_canceled,
28-
memory_order_acquire)) {
32+
if (_is_canceled())
33+
// once acknowledged _is_canceled() will return false
2934
return _weaken(_pthread_cancel_ack)();
30-
}
3135
return 0;
3236
}

libc/calls/internal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ forceinline bool __isfdkind(int fd, int kind) {
4343

4444
int _check_signal(bool);
4545
int _check_cancel(void);
46+
bool _is_canceled(void);
4647
int sys_close_nt(int, int);
4748
int _park_norestart(uint32_t, uint64_t);
4849
int _park_restartable(uint32_t, uint64_t);

libc/calls/islinuxmodern.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
2+
│ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi │
3+
╞══════════════════════════════════════════════════════════════════════════════╡
4+
│ Copyright 2024 Justine Alexandra Roberts Tunney │
5+
│ │
6+
│ Permission to use, copy, modify, and/or distribute this software for │
7+
│ any purpose with or without fee is hereby granted, provided that the │
8+
│ above copyright notice and this permission notice appear in all copies. │
9+
│ │
10+
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
11+
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
12+
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
13+
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
14+
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
15+
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
16+
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
17+
│ PERFORMANCE OF THIS SOFTWARE. │
18+
╚─────────────────────────────────────────────────────────────────────────────*/
19+
#include "libc/calls/calls.h"
20+
#include "libc/calls/syscall-sysv.internal.h"
21+
#include "libc/calls/syscall_support-sysv.internal.h"
22+
#include "libc/dce.h"
23+
#include "libc/errno.h"
24+
25+
bool IsLinuxModern(void) {
26+
return IsLinux() && sys_close_range(-1, -2, 0) == -1 && errno == EINVAL;
27+
}

libc/calls/readwrite-nt.c

Lines changed: 139 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,25 @@
1616
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
1717
│ PERFORMANCE OF THIS SOFTWARE. │
1818
╚─────────────────────────────────────────────────────────────────────────────*/
19+
#include "libc/assert.h"
1920
#include "libc/calls/createfileflags.internal.h"
2021
#include "libc/calls/internal.h"
2122
#include "libc/calls/sig.internal.h"
2223
#include "libc/calls/struct/sigset.h"
2324
#include "libc/calls/syscall_support-nt.internal.h"
25+
#include "libc/errno.h"
2426
#include "libc/intrin/fds.h"
2527
#include "libc/intrin/weaken.h"
2628
#include "libc/nt/enum/filetype.h"
2729
#include "libc/nt/errors.h"
2830
#include "libc/nt/events.h"
2931
#include "libc/nt/files.h"
32+
#include "libc/nt/process.h"
3033
#include "libc/nt/runtime.h"
3134
#include "libc/nt/struct/overlapped.h"
3235
#include "libc/nt/synchronization.h"
3336
#include "libc/nt/thread.h"
37+
#include "libc/sock/internal.h"
3438
#include "libc/stdio/sysparam.h"
3539
#include "libc/sysv/consts/sicode.h"
3640
#include "libc/sysv/errfuns.h"
@@ -47,8 +51,6 @@ sys_readwrite_nt(int fd, void *data, size_t size, ssize_t offset,
4751
int64_t handle, sigset_t waitmask,
4852
bool32 ReadOrWriteFile(int64_t, void *, uint32_t, uint32_t *,
4953
struct NtOverlapped *)) {
50-
int sig;
51-
uint32_t exchanged;
5254
struct Fd *f = g_fds.p + fd;
5355

5456
// 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,
6062
if (pwriting && !seekable)
6163
return espipe();
6264

63-
// determine if we need to lock a file descriptor across processes
65+
// determine if we need to lock file descriptor
6466
bool locked = isdisk && !pwriting && f->cursor;
65-
if (locked)
66-
__cursor_lock(f->cursor);
67-
68-
RestartOperation:
69-
// when a file is opened in overlapped mode win32 requires that we
70-
// take over full responsibility for managing our own file pointer
71-
// which is fine, because the one win32 has was never very good in
72-
// the sense that it behaves so differently from linux, that using
73-
// win32 i/o required more compatibilty toil than doing it by hand
74-
if (!pwriting) {
75-
if (seekable && f->cursor) {
76-
offset = f->cursor->shared->pointer;
77-
} else {
78-
offset = 0;
79-
}
80-
}
8167

82-
bool eagained = false;
83-
// check for signals and cancelation
84-
if (_check_cancel() == -1) {
68+
for (;;) {
69+
int got_sig = 0;
70+
bool got_eagain = false;
71+
uint32_t other_error = 0;
72+
73+
// create event handle for overlapped i/o
74+
intptr_t event;
75+
if (!(event = CreateEvent(0, 1, 0, 0)))
76+
return __winerr();
77+
78+
// ensure iops are ordered across threads and processes if seeking
8579
if (locked)
86-
__cursor_unlock(f->cursor);
87-
return -1; // ECANCELED
88-
}
89-
if (_weaken(__sig_get) && (sig = _weaken(__sig_get)(waitmask)))
90-
goto HandleInterrupt;
91-
92-
// signals have already been fully blocked by caller
93-
// perform i/o operation with atomic signal/cancel checking
94-
struct NtOverlapped overlap = {.hEvent = CreateEvent(0, 1, 0, 0),
95-
.Pointer = offset};
96-
bool32 ok = ReadOrWriteFile(handle, data, size, 0, &overlap);
97-
if (!ok && GetLastError() == kNtErrorIoPending) {
98-
// win32 says this i/o operation needs to block
99-
if (f->flags & _O_NONBLOCK) {
100-
// abort the i/o operation if file descriptor is in non-blocking mode
101-
CancelIoEx(handle, &overlap);
102-
eagained = true;
103-
} else {
104-
// wait until i/o either completes or is canceled by another thread
105-
// we avoid a race condition by having a second mask for unblocking
106-
struct PosixThread *pt;
107-
pt = _pthread_self();
108-
pt->pt_blkmask = waitmask;
109-
pt->pt_iohandle = handle;
110-
pt->pt_ioverlap = &overlap;
111-
atomic_store_explicit(&pt->pt_blocker, PT_BLOCKER_IO,
112-
memory_order_release);
113-
WaitForSingleObject(overlap.hEvent, -1u);
114-
atomic_store_explicit(&pt->pt_blocker, 0, memory_order_release);
80+
__cursor_lock(f->cursor);
81+
82+
// when a file is opened in overlapped mode win32 requires that we
83+
// take over full responsibility for managing our own file pointer
84+
// which is fine, because the one win32 has was never very good in
85+
// the sense that it behaves so differently from linux, that using
86+
// win32 i/o required more compatibilty toil than doing it by hand
87+
if (!pwriting) {
88+
if (seekable && f->cursor) {
89+
offset = f->cursor->shared->pointer;
90+
} else {
91+
offset = 0;
92+
}
11593
}
116-
ok = true;
117-
}
118-
if (ok)
119-
ok = GetOverlappedResult(handle, &overlap, &exchanged, true);
120-
CloseHandle(overlap.hEvent);
121-
122-
// if i/o succeeded then return its result
123-
if (ok) {
124-
if (!pwriting && seekable && f->cursor)
125-
f->cursor->shared->pointer = offset + exchanged;
126-
if (locked)
127-
__cursor_unlock(f->cursor);
128-
return exchanged;
129-
}
13094

131-
// only raise EINTR or EAGAIN if I/O got canceled
132-
if (GetLastError() == kNtErrorOperationAborted) {
133-
// raise EAGAIN if it's due to O_NONBLOCK mmode
134-
if (eagained) {
135-
if (locked)
136-
__cursor_unlock(f->cursor);
137-
return eagain();
95+
// initiate asynchronous i/o operation with win32
96+
struct NtOverlapped overlap = {.hEvent = event, .Pointer = offset};
97+
bool32 ok = ReadOrWriteFile(handle, data, size, 0, &overlap);
98+
if (!ok && GetLastError() == kNtErrorIoPending) {
99+
if (f->flags & _O_NONBLOCK) {
100+
// immediately back out of blocking i/o if non-blocking
101+
CancelIoEx(handle, &overlap);
102+
got_eagain = true;
103+
} else {
104+
// atomic block on i/o completion, signal, or cancel
105+
// it's not safe to acknowledge cancelation from here
106+
// it's not safe to call any signal handlers from here
107+
intptr_t sem;
108+
if ((sem = CreateSemaphore(0, 0, 1, 0))) {
109+
// installing semaphore before sig get makes wait atomic
110+
struct PosixThread *pt = _pthread_self();
111+
pt->pt_semaphore = sem;
112+
pt->pt_blkmask = waitmask;
113+
atomic_store_explicit(&pt->pt_blocker, PT_BLOCKER_SEM,
114+
memory_order_release);
115+
if (_is_canceled()) {
116+
CancelIoEx(handle, &overlap);
117+
} else if (_weaken(__sig_get) &&
118+
(got_sig = _weaken(__sig_get)(waitmask))) {
119+
CancelIoEx(handle, &overlap);
120+
} else {
121+
intptr_t hands[] = {event, sem};
122+
uint32_t wi = WaitForMultipleObjects(2, hands, 0, -1u);
123+
if (wi == 1) { // semaphore was signaled by signal enqueue
124+
CancelIoEx(handle, &overlap);
125+
if (_weaken(__sig_get))
126+
got_sig = _weaken(__sig_get)(waitmask);
127+
} else if (wi == -1u) {
128+
other_error = GetLastError();
129+
CancelIoEx(handle, &overlap);
130+
}
131+
}
132+
atomic_store_explicit(&pt->pt_blocker, 0, memory_order_release);
133+
CloseHandle(sem);
134+
} else {
135+
other_error = GetLastError();
136+
CancelIoEx(handle, &overlap);
137+
}
138+
}
139+
ok = true;
138140
}
139-
// otherwise it must be due to a kill() via __sig_cancel()
140-
if (_weaken(__sig_relay) && (sig = _weaken(__sig_get)(waitmask))) {
141-
HandleInterrupt:
141+
uint32_t exchanged = 0;
142+
if (ok)
143+
ok = GetOverlappedResult(handle, &overlap, &exchanged, true);
144+
uint32_t io_error = GetLastError();
145+
CloseHandle(event);
146+
147+
// check if i/o completed
148+
// this could forseeably happen even if CancelIoEx was called
149+
if (ok) {
150+
if (!pwriting && seekable && f->cursor)
151+
f->cursor->shared->pointer = offset + exchanged;
142152
if (locked)
143153
__cursor_unlock(f->cursor);
144-
int handler_was_called = _weaken(__sig_relay)(sig, SI_KERNEL, waitmask);
145-
if (_check_cancel() == -1)
146-
return -1; // possible if we SIGTHR'd
147-
if (locked)
148-
__cursor_lock(f->cursor);
149-
// read() is @restartable unless non-SA_RESTART hands were called
150-
if (!(handler_was_called & SIG_HANDLED_NO_RESTART))
151-
goto RestartOperation;
154+
if (got_sig) // swallow dequeued signal
155+
_weaken(__sig_relay)(got_sig, SI_KERNEL, waitmask);
156+
return exchanged;
152157
}
158+
159+
// it's now safe to unlock cursor
153160
if (locked)
154161
__cursor_unlock(f->cursor);
155-
return eintr();
156-
}
157162

158-
// read() and write() have generally different error-handling paths
159-
if (locked)
160-
__cursor_unlock(f->cursor);
161-
return -2;
163+
// check if i/o failed
164+
if (io_error != kNtErrorOperationAborted) {
165+
if (got_sig) // swallow dequeued signal
166+
_weaken(__sig_relay)(got_sig, SI_KERNEL, waitmask);
167+
// read() and write() have different error paths
168+
SetLastError(io_error);
169+
return -2;
170+
}
171+
172+
// the i/o operation was successfully canceled
173+
if (got_eagain) {
174+
unassert(!got_sig);
175+
return eagain();
176+
}
177+
178+
// it's now reasonable to report semaphore creation error
179+
if (other_error) {
180+
unassert(!got_sig);
181+
errno = __dos2errno(other_error);
182+
return -1;
183+
}
184+
185+
// check for thread cancelation and acknowledge
186+
if (_check_cancel() == -1)
187+
return -1;
188+
189+
// if signal module has been linked, then
190+
if (_weaken(__sig_get)) {
191+
192+
// gobble up all unmasked pending signals
193+
// it's now safe to recurse into signal handlers
194+
int handler_was_called = 0;
195+
do {
196+
if (got_sig)
197+
handler_was_called |=
198+
_weaken(__sig_relay)(got_sig, SI_KERNEL, waitmask);
199+
} while ((got_sig = _weaken(__sig_get)(waitmask)));
200+
201+
// check if SIGTHR handler was called
202+
if (_check_cancel() == -1)
203+
return -1;
204+
205+
// check if signal handler without SA_RESTART was called
206+
if (handler_was_called & SIG_HANDLED_NO_RESTART)
207+
return eintr();
208+
}
209+
210+
// otherwise try the i/o operation again
211+
}
162212
}
163213

164214
#endif /* __x86_64__ */

libc/calls/sigtimedwait-nt.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "libc/sysv/consts/sig.h"
3333
#include "libc/sysv/errfuns.h"
3434
#include "libc/thread/posixthread.internal.h"
35+
#ifdef __x86_64__
3536

3637
textwindows static int sys_sigtimedwait_nt_check(sigset_t syncsigs,
3738
siginfo_t *opt_info,
@@ -111,3 +112,5 @@ textwindows int sys_sigtimedwait_nt(const sigset_t *set, siginfo_t *opt_info,
111112
ALLOW_SIGNALS;
112113
return rc;
113114
}
115+
116+
#endif /* __x86_64__ */

libc/calls/write-nt.c

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,17 @@ static textwindows ssize_t sys_write_nt_impl(int fd, void *data, size_t size,
5353
bool isconsole = f->kind == kFdConsole;
5454

5555
// not implemented, XNU returns eperm();
56-
if (f->kind == kFdDevRandom) {
56+
if (f->kind == kFdDevRandom)
5757
return eperm();
58-
}
5958

6059
// determine win32 handle for writing
6160
int64_t handle = f->handle;
62-
if (isconsole && _weaken(GetConsoleOutputHandle)) {
61+
if (isconsole && _weaken(GetConsoleOutputHandle))
6362
handle = _weaken(GetConsoleOutputHandle)();
64-
}
6563

6664
// intercept ansi tty configuration sequences
67-
if (isconsole && _weaken(GetConsoleOutputHandle)) {
65+
if (isconsole && _weaken(GetConsoleOutputHandle))
6866
_weaken(InterceptTerminalCommands)(data, size);
69-
}
7067

7168
// perform heavy lifting
7269
ssize_t rc;

libc/cosmo.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ char *GetProgramExecutableName(void) libcesque;
1414
void unleaf(void) libcesque;
1515
int __demangle(char *, const char *, size_t) libcesque;
1616
int __is_mangled(const char *) libcesque;
17+
bool IsLinuxModern(void) libcesque;
1718
int LoadZipArgs(int *, char ***) libcesque;
1819

1920
COSMOPOLITAN_C_END_

libc/sock/connect-nt.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ textwindows static int sys_connect_nt_impl(struct Fd *f, const void *addr,
146146
// check if we still need more time
147147
if (!ready) {
148148
if (f->flags & O_NONBLOCK) {
149-
return ealready();
149+
return etimedout();
150150
} else {
151151
continue;
152152
}

0 commit comments

Comments
 (0)