Repository navigation
Expand file tree
/
Copy pathio.c
More file actions
2461 lines (2293 loc) · 90 KB
/
Copy pathio.c
File metadata and controls
2461 lines (2293 loc) · 90 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* Included by phasync.c (single translation unit); not compiled on its own. */
/* Non-blocking I/O on streams, sockets and pipes: stream/socket/pipe wrapping, stdio and php://input, preadv2()/RWF_NOWAIT, the wrapped-ops machinery shared across socket and pipe originals. */
/* ---- fd + wait helpers --------------------------------------------------- */
static php_socket_t phasync_stream_fd(php_stream *stream)
{
php_socket_t fd = -1;
php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL,
(void *) &fd, 0);
return fd;
}
/* PHP destroys a fiber dropped while suspended by resuming it once to unwind it:
* its finally blocks and destructors run, and it can no longer suspend
* (Fiber::suspend() throws). A wait there must not reach the loop at all. */
static zend_always_inline bool phasync_unwinding(void)
{
return EG(active_fiber) && (EG(active_fiber)->flags & ZEND_FIBER_FLAG_DESTROYED);
}
/* Fail a wait in a fiber being destroyed as Fiber::suspend() would, unless the
* unwind (or another exception) is already pending. */
static void phasync_throw_force_closed(void)
{
if (!EG(exception)) {
zend_throw_error(zend_hash_str_find_ptr(CG(class_table), ZEND_STRL("fibererror")),
"Cannot suspend in a force-closed fiber");
}
}
static bool phasync_cannot_suspend(void)
{
if (!phasync_unwinding()) {
return false;
}
phasync_throw_force_closed();
return true;
}
/* Call the sleep handler as handler(int $microseconds); -1 if it threw. */
static int phasync_call_sleep(zval *handler, zend_long usec)
{
zval arg, retval;
int rc = 0;
if (phasync_cannot_suspend()) {
return -1;
}
ZVAL_LONG(&arg, usec);
ZVAL_UNDEF(&retval);
if (call_user_function(NULL, NULL, handler, &retval, 1, &arg) == FAILURE || EG(exception)) {
rc = -1;
}
zval_ptr_dtor(&retval);
return rc;
}
/* If the stream is a socket, return its netstream data — which holds the timeout
* and the timed-out flag — otherwise NULL. xp_socket's udp/unix/udg streams share
* php_sockop_read as their read op. But ext/openssl registers itself for tcp://
* too (so crypto can be enabled on a plain TCP stream later), so TCP and ssl/tls
* streams use openssl's static ops, recognisable only by their label; its stream
* data starts with a php_netstream_data_t (xp_ssl.c), so the cast holds. */
static php_netstream_data_t *phasync_sock_data(php_stream *stream)
{
const php_stream_ops *orig = stream ? PHASYNC_ORIG(stream) : NULL;
if (orig && stream->abstract
&& (orig->read == php_stream_socket_ops.read
|| (orig->label && strcmp(orig->label, "tcp_socket/ssl") == 0))) {
return (php_netstream_data_t *) stream->abstract;
}
return NULL;
}
/* The timeout (seconds) native PHP would apply to a would-block wait here: the
* socket's own timeout (stream_set_timeout()/default_socket_timeout; tv_sec == -1
* means none), or INF for non-sockets (pipes/FIFOs/files block forever). */
static double phasync_wait_timeout(php_stream *stream)
{
php_netstream_data_t *sock = phasync_sock_data(stream);
if (sock == NULL || sock->timeout.tv_sec == -1) {
return INFINITY;
}
return (double) sock->timeout.tv_sec + (double) sock->timeout.tv_usec / 1000000.0;
}
/* Flag the stream as timed out exactly as the native socket op does, so
* stream_get_meta_data()['timed_out'] === true. No-op for non-sockets. */
static void phasync_mark_timed_out(php_stream *stream)
{
php_netstream_data_t *sock = phasync_sock_data(stream);
if (sock) {
sock->timeout_event = true;
}
}
/* Since 8.3, native php_sockop_read does not wait at all (MSG_DONTWAIT, returns 0)
* when the current read call has already delivered buffered data, or when the
* socket's timeout is exactly zero. Mirror that for sockets; pipes/files block
* natively. 8.2 has no such rule: a blocking read always waits, and a zero timeout
* times out at once — which the normal wait path already reproduces. */
static bool phasync_read_dont_wait(php_stream *stream)
{
php_netstream_data_t *sock = phasync_sock_data(stream);
return sock && (stream->has_buffered_data
|| (sock->timeout.tv_sec == 0 && sock->timeout.tv_usec == 0));
}
/* Native php_sockop_write reports a timed-out send with a notice; mirror it. */
static void phasync_write_timeout_notice(php_stream *stream, size_t count, int err)
{
char *estr;
if (!phasync_sock_data(stream) || (stream->flags & PHP_STREAM_FLAG_SUPPRESS_ERRORS)) {
return;
}
estr = php_socket_strerror(err, NULL, 0);
#ifdef php_stream_warn
php_stream_warn(stream, NetworkSendFailed,
"Send of %zu bytes failed with errno=%d %s", count, err, estr);
#else
php_error_docref(NULL, E_NOTICE,
"Send of %zu bytes failed with errno=%d %s", count, err, estr);
#endif
efree(estr);
}
static double phasync_now(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double) ts.tv_sec + (double) ts.tv_nsec / 1e9;
}
/* ---- a stream closed under a suspended coroutine (#14) --------------------
*
* A coroutine suspended inside an operation on a stream still has the stream on
* its C stack: in our op, in PHP's stream layer, in mysqlnd (with the connection
* mysqli frees as it closes). Another coroutine closing the stream then must not
* free it before they have left: its close op wakes them, parks the closer until
* the last one is out, and only then lets the close go on. A woken operation fails
* as on a closed descriptor (EBADF), and so does any later one that would wait on
* the closing stream. PHP frees a stream as its close op returns, whatever the
* path (fclose(), mysqli::close(), ...), so the close op is where it waits.
*
* The closer's fiber is held alive while it waits: destroyed there, it could
* neither wait nor keep the stream from being freed under the others (#21). Held,
* it is resumed by its loop once they are out, or, if its loop has let it go, left
* to PHP to destroy once they are (phasync_holds_release()).
*
* A close beginning in a coroutine that is already unwinding (its finally
* blocks) cannot wait this way at all: Fiber::suspend() throws there. For the
* three adapters that route through here -- fclose() (in all its forms, the
* resource destructor and proc_close()/pclose() pipes included) -- such a close
* wakes the waiters and marks the guard closing exactly as above, but skips the
* real free entirely instead of waiting for it to become safe
* (phasync_close_if_unwinding(), #23): the handle is left open, to whichever
* reference outlives the others (each waiter holds its own for the duration of
* its operation), and closes for real, through this same code, once that
* reference is the last one standing. mysqli::close() has no such adapter: it
* stays a fatal error (frees mysqlnd's own structures outside the streams API;
* see the README). */
/* ---- the guard's core: reusable for any handle kind, keyed by its address --
*
* phasync_inflight_enter()/_out()/_leave() are the whole of what a new adapter
* needs from the guard for the wait side (park a coroutine "in flight" on a
* handle, and settle it back out): they take the handle only as an opaque
* (void *) key, with no php_stream-specific code in them. The close side
* (phasync_inflight_close()/_drain(), phasync_close_if_unwinding()) stays
* typed to php_stream *, since waking a handle's waiters (phasync_stream_forget())
* is necessarily specific to the kind of handle; a future adapter (ext/sockets'
* socket_close(), say) writes its own small close-side wrapper around the same
* enter/out/leave, the way the stream adapter below does. */
/* Before suspending in an op on the handle. False (errno EBADF) if it is closing. */
static bool phasync_inflight_enter(void *handle)
{
phasync_inflight *f = zend_hash_index_find_ptr(&PHASYNC_G(inflight), (zend_ulong) (uintptr_t) handle);
if (f == NULL) {
f = pecalloc(1, sizeof(*f), 1);
zend_hash_index_add_new_ptr(&PHASYNC_G(inflight), (zend_ulong) (uintptr_t) handle, f);
} else if (f->closing) {
errno = EBADF;
return false;
}
f->waiters++;
phasync_ledger_cur()->inflight = handle;
return true;
}
/* One coroutine less inside an op on the handle; the last one out of a closing
* one lets the closer go on. True if it is closing. */
static bool phasync_inflight_out(void *handle)
{
phasync_inflight *f = zend_hash_index_find_ptr(&PHASYNC_G(inflight), (zend_ulong) (uintptr_t) handle);
if (--f->waiters == 0) {
if (!f->closing) {
zend_hash_index_del(&PHASYNC_G(inflight), (zend_ulong) (uintptr_t) handle);
return false;
}
if (f->closer) {
phasync_chan_push(f->closer->chan, f->closer_slot);
}
}
return f->closing;
}
/* Back from the suspension. True (errno EBADF) if the handle is closing: the op fails. */
static bool phasync_inflight_leave(void *handle)
{
phasync_ledger_cur()->inflight = NULL;
if (phasync_inflight_out(handle)) {
errno = EBADF;
return true;
}
return false;
}
/* Let go of a closer's fiber once it no longer waits. Resumed by its loop, the
* loop holds it too; if only we do, it is running and must not be freed under
* itself: then the reference is left to the end of the request. */
static void phasync_hold_drop(phasync_inflight *f)
{
zend_object *o = f->hold;
f->hold = NULL;
PHASYNC_G(holds)--;
if (GC_REFCOUNT(o) > 1) {
GC_DELREF(o);
}
}
/* Where no coroutine is inside an op (a Poller's poll(), a manage() starting): let
* PHP have the closers held for streams nobody is inside any more. One whose loop
* let it go is destroyed here, and its close ends as it unwinds. */
static void phasync_holds_release(void)
{
zend_object *release[16];
phasync_inflight *f;
int n = 0;
if (PHASYNC_G(holds) == 0) {
return;
}
ZEND_HASH_FOREACH_PTR(&PHASYNC_G(inflight), f) {
if (f->hold && f->waiters == 0 && n < 16) {
release[n++] = f->hold;
f->hold = NULL;
PHASYNC_G(holds)--;
}
} ZEND_HASH_FOREACH_END();
for (int i = 0; i < n; i++) {
OBJ_RELEASE(release[i]);
}
}
/* The request is ending and PHP destroys every object: a closer destroyed while
* coroutines are still inside an op on its stream cannot wait for them, but they
* are about to be destroyed too. Destroy them first, as PHP would. */
static void phasync_inflight_destroy_waiters(php_stream *stream)
{
zend_object *victims[16];
phasync_ledger *l;
zend_ulong key;
int n = 0;
ZEND_HASH_FOREACH_NUM_KEY_PTR(&PHASYNC_G(ledgers), key, l) {
zend_fiber_context *ctx = (zend_fiber_context *) (uintptr_t) key;
if (l->inflight == stream && ctx->kind == zend_ce_fiber && n < 16) {
zend_object *obj = &((zend_fiber *) ((char *) ctx - offsetof(zend_fiber, context)))->std;
if (!(OBJ_FLAGS(obj) & IS_OBJ_DESTRUCTOR_CALLED)) {
GC_ADDREF(obj);
victims[n++] = obj;
}
}
} ZEND_HASH_FOREACH_END();
for (int i = 0; i < n; i++) {
GC_ADD_FLAGS(victims[i], IS_OBJ_DESTRUCTOR_CALLED);
victims[i]->handlers->dtor_obj(victims[i]);
OBJ_RELEASE(victims[i]);
}
}
/* Wake whoever waits on the closing stream and park until every coroutine
* suspended in an op on it has left. A cancellation of the wait is held until
* they are out. False if the closer is being destroyed while they are still in
* (it began closing while unwinding): it cannot wait, and the unwind is pending. */
static bool phasync_inflight_drain(php_stream *stream, phasync_inflight *f)
{
phasync_poller *p = PHASYNC_G(scope_top)->poller;
phasync_ledger *l = phasync_ledger_cur();
zend_object *held = NULL;
bool destroyed_waiters = false;
phasync_stream_forget(stream);
f->closing = true;
f->closer = p;
GC_ADDREF(&p->std);
l->closing = stream;
l->closing_p = p;
if (!phasync_unwinding()) {
f->hold = &EG(active_fiber)->std;
GC_ADDREF(f->hold);
PHASYNC_G(holds)++;
}
while (f->waiters) {
if (phasync_unwinding()) {
if ((EG(flags) & EG_FLAGS_IN_SHUTDOWN) && !destroyed_waiters) {
destroyed_waiters = true;
phasync_inflight_destroy_waiters(stream);
continue;
}
break; /* the unwind stays pending */
}
if ((f->closer_slot = phasync_get_slot(p)) < 0
|| phasync_park(p, f->closer_slot, INFINITY, NULL) != PHASYNC_WAIT_READY) {
if (EG(exception) == NULL) {
zend_error_noreturn(E_ERROR, "phasync: unable to wait for the coroutines using a stream being closed");
}
if (phasync_unwinding()) {
continue;
}
if (held == NULL) {
held = EG(exception);
GC_ADDREF(held);
}
zend_clear_exception();
}
}
l->closing = NULL;
f->closer = NULL;
OBJ_RELEASE(&p->std);
if (f->hold) {
phasync_hold_drop(f);
}
if (f->waiters) {
if (held) {
OBJ_RELEASE(held);
}
return false;
}
if (held) {
zend_throw_exception_internal(held);
}
return true;
}
/* From a close op, before the stream goes: wait until no coroutine is inside an op
* on it (see phasync_inflight_drain()). The closer can only wait in a coroutine;
* anywhere else it would free the stream under them, so that is a fatal error. */
static void phasync_inflight_close(php_stream *stream)
{
phasync_inflight *f = zend_hash_index_find_ptr(&PHASYNC_G(inflight), (zend_ulong) (uintptr_t) stream);
phasync_stream_forget(stream);
if (f == NULL) {
return;
}
if (f->waiters) {
if (PHASYNC_G(scope_top) == NULL || EG(active_fiber) == NULL) {
zend_error_noreturn(E_ERROR, "phasync: a stream was closed outside a coroutine while %u coroutine(s) wait on it",
f->waiters);
}
if (!phasync_inflight_drain(stream, f)) {
zend_error_noreturn(E_ERROR, "phasync: a stream was closed by a coroutine being destroyed while %u coroutine(s) wait on it",
f->waiters);
}
}
zend_hash_index_del(&PHASYNC_G(inflight), (zend_ulong) (uintptr_t) stream);
}
/* An adapter's entry point for a close beginning in a coroutine that is already
* unwinding (#23): it cannot suspend, so phasync_inflight_drain()'s wait is not
* an option. If nothing is inside an op on the stream there is nothing to do
* here: the caller's normal close goes on exactly as without the extension.
* Otherwise: wake the waiters and mark the guard closing (their op fails,
* EBADF), but do not free the stream at all -- leave it open under whichever
* reference outlives the others (each waiter holds its own for the duration of
* its operation; see the top of this section). Its close then goes through this
* same guard for real, once that reference is the last one, by which point
* nothing is left to wait for. True: the caller must treat the close as done
* and must not call the real one. */
static bool phasync_close_if_unwinding(php_stream *stream)
{
phasync_inflight *f;
if (!phasync_unwinding()) {
return false;
}
f = zend_hash_index_find_ptr(&PHASYNC_G(inflight), (zend_ulong) (uintptr_t) stream);
if (f == NULL || f->waiters == 0) {
return false;
}
phasync_stream_forget(stream);
f->closing = true;
return true;
}
/* The hooked operations wait through the manage() scope's Poller, with its
* timeout class. A wait on a stream fails with CLOSED if another coroutine
* closes it meanwhile. There is no concurrency cap: every hooked wait goes on
* this same cooperative path, so a stream whose readiness depends on a SIBLING
* coroutine of the same worker can never be starved by this one. */
static int phasync_wait_fd(int dir, php_stream *stream, php_socket_t fd, double timeout)
{
phasync_scope *s = PHASYNC_G(scope_top);
int rc;
if (stream != NULL && !phasync_inflight_enter(stream)) {
return PHASYNC_WAIT_CLOSED;
}
rc = phasync_poller_wait(s->poller, dir, stream, fd, timeout, s->timeout_ce, false);
if (stream == NULL) {
return rc;
}
return phasync_inflight_leave(stream) && rc != PHASYNC_WAIT_ERROR ? PHASYNC_WAIT_CLOSED : rc;
}
#define PHASYNC_COOP_ERROR 0 /* stop: exception pending */
#define PHASYNC_COOP_RETRY 1 /* ready: retry the op */
#define PHASYNC_COOP_TIMEOUT 2 /* stop: native timeout path taken */
/* One cooperative wait inside a read/write op. *deadline carries the wait's
* deadline across retries within one op call (one fill), so a spurious wake-up
* is handed only the REMAINING time; callers start each op call with
* *deadline < 0, giving every fill a fresh full timeout, as
* php_sock_stream_wait_for_data does. On timeout the stream's timed-out flag is
* set, exactly as the native socket op sets it. */
static int phasync_cooperate(int dir, php_stream *stream, php_socket_t fd, double *deadline)
{
double timeout = phasync_wait_timeout(stream), wait = INFINITY;
int w;
if (!isinf(timeout)) {
double now = phasync_now();
if (*deadline < 0) {
*deadline = now + timeout;
wait = timeout; /* first wait of this fill: the exact native timeout */
} else {
wait = *deadline - now; /* a retry: only the remaining time */
}
if (wait <= 0) { /* time used up (or a zero timeout) */
phasync_mark_timed_out(stream);
return PHASYNC_COOP_TIMEOUT;
}
}
w = phasync_wait_fd(dir, stream, fd, wait);
if (w == PHASYNC_WAIT_READY) {
return PHASYNC_COOP_RETRY;
}
if (w == PHASYNC_WAIT_TIMEOUT) {
phasync_mark_timed_out(stream);
return PHASYNC_COOP_TIMEOUT;
}
return PHASYNC_COOP_ERROR;
}
/* ---- wrapped stream ops (shared across socket + pipe originals) ----------- */
static phasync_hook_entry *phasync_entry(php_stream *stream)
{
return zend_hash_index_find_ptr(&PHASYNC_G(hooked), (zend_ulong) (uintptr_t) stream);
}
static phasync_hook_entry *phasync_entry_ensure(php_stream *stream)
{
phasync_hook_entry *e = phasync_entry(stream);
if (e == NULL) {
e = pemalloc(sizeof(*e), 1);
e->pidfd = -1;
zend_hash_index_add_ptr(&PHASYNC_G(hooked), (zend_ulong) (uintptr_t) stream, e);
}
return e;
}
/* Nothing about a stream's blocking mode is cached: every hooked op reads the
* fd's real O_NONBLOCK state fresh, with fcntl(F_GETFL), right before deciding
* whether to cooperate. Returns -1 (treat as "can't tell, do the native op") when
* fd is -1 or F_GETFL fails. */
static int phasync_fd_flags(php_socket_t fd)
{
return fd == -1 ? -1 : fcntl(fd, F_GETFL, 0);
}
#if PHASYNC_HAVE_NOWAIT
/* issue #42: RWF_NOWAIT support turns out to depend on the fd's DEVICE, not
* just the kernel version -- confirmed empirically on this very machine: an
* anonymous pipe (pipe(2), what proc_open()'s pipes to a Lua/Scribunto child
* actually are) accepts preadv2()/pwritev2() with RWF_NOWAIT; a named FIFO
* on an ordinary filesystem (open(2) on a mkfifo()'d path, exercised by
* tests/pool_fifo_rendezvous.phpt) answers EOPNOTSUPP for the exact same
* call on the exact same kernel. So, same shape as phasync_fs_fd_mode()'s
* own nowait_off list above (for plain files; a FIFO on the SAME device has
* been seen to disagree with its own files, so this is its own, separate
* list): remember which devices refuse it, and skip trying again on that
* device. While none has ever refused (the overwhelmingly common case: a
* worker that only ever talks to anonymous pipes never adds anything here),
* this costs no extra syscall at all -- the comparison is a single integer
* check, done before the fstat() an actual lookup would need. */
static bool phasync_pipe_dev_blocked(php_socket_t fd)
{
struct stat st;
if (PHASYNC_G(npipe_nowait_off) == 0) {
return false;
}
if (fstat(fd, &st) != 0) {
return false; /* can't tell -- try it, same as a cache miss */
}
for (int i = 0; i < PHASYNC_G(npipe_nowait_off); i++) {
if (PHASYNC_G(pipe_nowait_off)[i] == st.st_dev) {
return true;
}
}
return false;
}
/* Records that this fd's device just answered EOPNOTSUPP/EINVAL/ENOSYS to
* RWF_NOWAIT, so later pipes/FIFOs on the same device skip straight to the
* fcntl-toggle fallback instead of paying for the same failed syscall again.
* Best-effort: a failed fstat() or realloc() just means it tries again next
* time, never a correctness problem (the fallback below always runs either
* way). */
static void phasync_pipe_dev_block(php_socket_t fd)
{
struct stat st;
dev_t *nd;
if (fstat(fd, &st) != 0) {
return;
}
for (int i = 0; i < PHASYNC_G(npipe_nowait_off); i++) {
if (PHASYNC_G(pipe_nowait_off)[i] == st.st_dev) {
return;
}
}
nd = realloc(PHASYNC_G(pipe_nowait_off), (PHASYNC_G(npipe_nowait_off) + 1) * sizeof(dev_t));
if (nd) {
PHASYNC_G(pipe_nowait_off) = nd;
nd[PHASYNC_G(npipe_nowait_off)++] = st.st_dev;
}
}
#endif
/* One non-blocking attempt at a pipe/TTY/FIFO read, WITHOUT ever touching
* the fd's O_NONBLOCK flag: preadv2(..., RWF_NOWAIT) asks the kernel to fail
* with EAGAIN instead of blocking, for exactly this one call, like
* MSG_DONTWAIT already does for a socket's recv() just below. Falls back to
* the old toggle-around-the-syscall approach for this fd's device if it
* (or the running kernel) doesn't support RWF_NOWAIT at all. `flags` is the
* fd's real, caller-visible fcntl(F_GETFL) flags (the fallback path's
* restore value). */
static ssize_t phasync_pipe_read_nowait(php_socket_t fd, char *buf, size_t count, int flags)
{
#if PHASYNC_HAVE_NOWAIT
if (!phasync_pipe_dev_blocked(fd)) {
struct iovec iov = { buf, count };
ssize_t n = preadv2(fd, &iov, 1, -1, RWF_NOWAIT);
if (n >= 0 || (errno != ENOSYS && errno != EOPNOTSUPP && errno != EINVAL)) {
PHASYNC_G(sw_pipe_nowait)++;
return n;
}
phasync_pipe_dev_block(fd);
}
#endif
PHASYNC_G(sw_pipe_fcntl_fallback)++;
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
ssize_t n = read(fd, buf, count);
fcntl(fd, F_SETFL, flags);
return n;
}
/* Write-side twin of phasync_pipe_read_nowait(), via pwritev2(). */
static ssize_t phasync_pipe_write_nowait(php_socket_t fd, const char *buf, size_t count, int flags)
{
#if PHASYNC_HAVE_NOWAIT
if (!phasync_pipe_dev_blocked(fd)) {
struct iovec iov = { (void *) buf, count };
ssize_t n = pwritev2(fd, &iov, 1, -1, RWF_NOWAIT);
if (n >= 0 || (errno != ENOSYS && errno != EOPNOTSUPP && errno != EINVAL)) {
PHASYNC_G(sw_pipe_nowait)++;
return n;
}
phasync_pipe_dev_block(fd);
}
#endif
PHASYNC_G(sw_pipe_fcntl_fallback)++;
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
ssize_t n = write(fd, buf, count);
fcntl(fd, F_SETFL, flags);
return n;
}
/* One non-blocking send() on a blocking socket: MSG_DONTWAIT, no flag touched.
* macOS ignores MSG_DONTWAIT in send() (unix and TCP sockets block), so there
* O_NONBLOCK is set around the one call, as the pipe fallback above does. */
static ssize_t phasync_send_nowait(php_socket_t fd, const char *buf, size_t count, int flags)
{
#ifdef __APPLE__
ssize_t n;
int err;
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
n = send(fd, buf, count, MSG_NOSIGNAL);
err = errno;
fcntl(fd, F_SETFL, flags);
errno = err;
return n;
#else
(void) flags;
return send(fd, buf, count, MSG_DONTWAIT | MSG_NOSIGNAL);
#endif
}
/* The wrapped read/write ops emulate the caller's intended semantics:
* - outside a manage() scope -> exactly the native op;
* - an explicitly non-blocking stream -> exactly the native op;
* - a blocking stream inside a scope -> cooperate: wait for
* readiness, then do ONE non-blocking attempt at the syscall, without ever
* leaving O_NONBLOCK set on the fd afterwards. A socket fd gets this via
* MSG_DONTWAIT on recv()/send() (no flag touched at all, since the fd is
* shared with every other holder of the same open file description); a
* pipe/TTY/FIFO fd gets this via preadv2()/pwritev2() with RWF_NOWAIT
* (phasync_pipe_read_nowait()/_write_nowait(), above) -- also no flag
* touched, on any kernel new enough to support it (Linux 4.14+); older
* kernels fall back to the previous approach, O_NONBLOCK set immediately
* before that one syscall and restored immediately after, so the window
* in which another process sharing the descriptor could observe it
* changed is just that syscall, never the wait.
* So a wrapped stream is indistinguishable from an unwrapped one whenever no
* scope is driving it, and never leaves the fd's mode different from how it
* found it. */
static ssize_t phasync_wrapped_read(php_stream *stream, char *buf, size_t count)
{
const php_stream_ops *orig = PHASYNC_ORIG(stream);
php_socket_t fd = phasync_stream_fd(stream);
int dir = phasync_reading();
int flags;
bool is_sock;
if (fd == -1) {
return orig->read(stream, buf, count);
}
flags = phasync_fd_flags(fd);
if (flags == -1 || (flags & O_NONBLOCK) || !dir) {
return orig->read(stream, buf, count);
}
is_sock = phasync_sock_data(stream) != NULL;
double deadline = -1; /* per op call = per fill */
for (;;) {
ssize_t n;
if (is_sock) {
n = recv(fd, buf, count, MSG_DONTWAIT);
} else {
n = phasync_pipe_read_nowait(fd, buf, count, flags);
}
if (n > 0) {
return n;
}
if (n == 0) {
stream->eof = 1;
return 0;
}
if (errno == EINTR) {
continue;
}
if (errno != EAGAIN && errno != EWOULDBLOCK) {
stream->eof = 1;
return -1;
}
if (phasync_read_dont_wait(stream)) {
return 0; /* native MSG_DONTWAIT short read */
}
if (phasync_cooperate(dir, stream, fd, &deadline) != PHASYNC_COOP_RETRY) {
return -1; /* timed out (flag set) or exception pending */
}
}
}
static ssize_t phasync_wrapped_write(php_stream *stream, const char *buf, size_t count)
{
const php_stream_ops *orig = PHASYNC_ORIG(stream);
php_socket_t fd = phasync_stream_fd(stream);
int dir = phasync_writing();
int flags;
bool is_sock;
if (fd == -1) {
return orig->write(stream, buf, count);
}
flags = phasync_fd_flags(fd);
if (flags == -1 || (flags & O_NONBLOCK) || !dir) {
return orig->write(stream, buf, count);
}
is_sock = phasync_sock_data(stream) != NULL;
double deadline = -1; /* per op call = per fill */
for (;;) {
ssize_t n;
int err, c;
if (is_sock) {
n = phasync_send_nowait(fd, buf, count, flags);
} else {
n = phasync_pipe_write_nowait(fd, buf, count, flags);
}
if (n >= 0) {
return n;
}
if (errno == EINTR) {
continue;
}
if (errno != EAGAIN && errno != EWOULDBLOCK) {
return -1;
}
err = errno;
c = phasync_cooperate(dir, stream, fd, &deadline);
if (c == PHASYNC_COOP_RETRY) {
continue;
}
if (c == PHASYNC_COOP_TIMEOUT) {
phasync_write_timeout_notice(stream, count, err);
}
return -1;
}
}
/* Delegate-mode ops for streams whose bytes must go through the original op
* (e.g. TLS: SSL_read/SSL_write). We can't raw read/write the fd (OpenSSL's own
* BIO does the actual recv/send), so inside a scope we tell the stream layer to
* go non-blocking for the duration of this one op (so the original op reports
* would-block as 0 without eof instead of blocking), wait and retry, then put it
* back exactly as found before returning — one op call is the unit a plain
* blocking stream_socket_enable_crypto()-driven read/write is native's
* equivalent of one syscall, so this is the same "restore right after" rule, just
* scoped to the whole delegate call instead of one recv/send. Read/write intent
* is approximated (a read waits for readability); TLS renegotiation wanting the
* opposite direction is a known limitation. */
/* A TLS stream's socket, without php_stream_cast(): openssl's cast for select()
* fills the read buffer when OpenSSL holds decrypted data (SSL_pending()) and
* the buffer is empty, through this stream's read op, which asked for the fd:
* an endless recursion. Its stream data starts with a php_netstream_data_t. */
static zend_always_inline php_socket_t phasync_tls_fd(php_stream *stream)
{
return ((php_netstream_data_t *) stream->abstract)->socket;
}
static ssize_t phasync_wrapped_read_tls(php_stream *stream, char *buf, size_t count)
{
const php_stream_ops *orig = PHASYNC_ORIG(stream);
php_socket_t fd = phasync_tls_fd(stream);
int dir = phasync_reading();
int flags = phasync_fd_flags(fd);
ssize_t n;
if (flags == -1 || (flags & O_NONBLOCK) || !dir) {
return orig->read(stream, buf, count);
}
double deadline = -1; /* per op call = per fill */
if (orig->set_option) {
orig->set_option(stream, PHP_STREAM_OPTION_BLOCKING, 0, NULL);
}
for (;;) {
n = orig->read(stream, buf, count);
if (n > 0 || n < 0 || stream->eof) {
break; /* data, error, or real EOF */
}
fd = phasync_tls_fd(stream); /* n == 0, not eof -> would block */
if (fd == -1 || phasync_cooperate(dir, stream, fd, &deadline) != PHASYNC_COOP_RETRY) {
n = -1;
break;
}
}
if (orig->set_option) {
orig->set_option(stream, PHP_STREAM_OPTION_BLOCKING, 1, NULL);
}
return n;
}
static ssize_t phasync_wrapped_write_tls(php_stream *stream, const char *buf, size_t count)
{
const php_stream_ops *orig = PHASYNC_ORIG(stream);
php_socket_t fd = phasync_tls_fd(stream);
int dir = phasync_writing();
int flags = phasync_fd_flags(fd);
ssize_t n;
if (flags == -1 || (flags & O_NONBLOCK) || !dir) {
return orig->write(stream, buf, count);
}
double deadline = -1; /* per op call = per fill */
if (orig->set_option) {
orig->set_option(stream, PHP_STREAM_OPTION_BLOCKING, 0, NULL);
}
for (;;) {
n = orig->write(stream, buf, count);
if (n > 0 || n < 0) {
break;
}
fd = phasync_tls_fd(stream); /* 0 -> would block */
if (fd == -1 || phasync_cooperate(dir, stream, fd, &deadline) != PHASYNC_COOP_RETRY) {
n = -1;
break;
}
}
if (orig->set_option) {
orig->set_option(stream, PHP_STREAM_OPTION_BLOCKING, 1, NULL);
}
return n;
}
/* Delegate a set_option call to the original ops. xport ops (bind/connect/listen/
* accept/getname/…) all arrive through set_option, and the socket layer decides
* unix-vs-inet by *pointer identity* (php_stream_is(stream,
* &php_stream_unix_socket_ops), xp_socket.c). Our wrapped ops is a copy with a
* different address, which would make a unix socket look like inet ("Failed to
* parse address"), so restore the real ops pointer for the (synchronous) call. */
static int phasync_orig_set_option(php_stream *stream, int option, int value, void *ptrparam)
{
const php_stream_ops *orig = PHASYNC_ORIG(stream), *saved = stream->ops;
int r;
if (!orig->set_option) {
return PHP_STREAM_OPTION_RETURN_NOTIMPL;
}
if (saved->read == phasync_wrapped_read_tls) {
/* ext/openssl never compares ops pointers, and its connect/accept enable
* crypto through stream->ops: keep ours there, so the handshake cooperates
* and accepted clients inherit the wrapped ops. */
return orig->set_option(stream, option, value, ptrparam);
}
stream->ops = (php_stream_ops *) orig;
r = orig->set_option(stream, option, value, ptrparam);
stream->ops = saved;
return r;
}
/* stream_socket_accept() with a timeout, inside a scope and in a fiber: wait for
* the listener to become readable via the read handler instead of blocking the
* process, then accept with a zero timeout. On timeout, report exactly what the
* socket layer reports (php_network_accept_incoming: ETIMEDOUT + its error text),
* so the calling PHP function emits its own native warning. A zero timeout keeps
* its explicit don't-wait meaning and goes straight to the original. */
static int phasync_accept_cooperative(php_stream *stream, php_stream_xport_param *xp)
{
struct timeval *tv = xp->inputs.timeout, zero = { 0, 0 };
double deadline = tv ? phasync_now() + (double) tv->tv_sec + (double) tv->tv_usec / 1000000.0 : 0;
php_socket_t fd = phasync_stream_fd(stream);
if (fd == -1 || (tv && tv->tv_sec == 0 && tv->tv_usec == 0)) {
return phasync_orig_set_option(stream, PHP_STREAM_OPTION_XPORT_API, 0, xp);
}
for (;;) {
double remaining = tv ? deadline - phasync_now() : INFINITY;
int w = remaining > 0
? phasync_wait_fd(PHASYNC_READ, stream, fd, remaining)
: PHASYNC_WAIT_TIMEOUT;
int r;
if (w == PHASYNC_WAIT_TIMEOUT) {
xp->outputs.client = NULL;
xp->outputs.returncode = -1;
xp->outputs.error_code = ETIMEDOUT;
if (xp->want_errortext) {
xp->outputs.error_text = php_socket_error_str(ETIMEDOUT);
}
return PHP_STREAM_OPTION_RETURN_OK;
}
if (w == PHASYNC_WAIT_ERROR || w == PHASYNC_WAIT_CLOSED) {
/* Closed meanwhile (EBADF), or the wait cancelled or its coroutine
* destroyed (ECANCELED, the exception pending propagates). */
xp->outputs.client = NULL;
xp->outputs.returncode = -1;
xp->outputs.error_code = w == PHASYNC_WAIT_CLOSED ? EBADF : ECANCELED;
if (xp->want_errortext) {
xp->outputs.error_text = php_socket_error_str(xp->outputs.error_code);
}
return PHP_STREAM_OPTION_RETURN_OK;
}
xp->inputs.timeout = &zero;
r = phasync_orig_set_option(stream, PHP_STREAM_OPTION_XPORT_API, 0, xp);
xp->inputs.timeout = tv;
if (r != PHP_STREAM_OPTION_RETURN_OK || xp->outputs.client
|| (xp->outputs.error_code != ETIMEDOUT && xp->outputs.error_code != EAGAIN
&& xp->outputs.error_code != EWOULDBLOCK)) {
return r;
}
/* Someone else took the connection between readiness and accept: wait again. */
if (xp->outputs.error_text) {
zend_string_release(xp->outputs.error_text);
xp->outputs.error_text = NULL;
}
}
}
/* tcp:// connect (stream_socket_client/fsockopen/pfsockopen), inside a scope and
* in a fiber. Native resolves the host (blocking getaddrinfo), then tries each
* address in order within one timeout budget, each attempt a blocking connect.
* Here: resolve on the worker pool (same hints as php_network_getaddresses, so
* the same order), then for each address hand the ORIGINAL connect that single IP
* literal in async mode — PHP still creates the socket with every context option
* (bindto, tcp_nodelay, keepalive, linger, buffers) — and wait for writability via
* the write handler with the remaining budget. Errors are recorded exactly as the
* socket layer does, so the calling PHP function emits its own native warning. A
* failed lookup falls back to the native connect so its error message is PHP's. */
static int phasync_ipv6_usable(void)
{
static int usable = -1; /* php_network_getaddresses' ipv6_borked probe */
if (usable == -1) {
int s6 = socket(AF_INET6, SOCK_DGRAM, 0);
usable = s6 >= 0;
if (s6 >= 0) {
close(s6);
}
}
return usable;
}
static int phasync_connect_cooperative(php_stream *stream, php_stream_xport_param *xp)
{
php_netstream_data_t *sock = phasync_sock_data(stream);
char *name = xp->inputs.name, host[256], literal[INET6_ADDRSTRLEN + 16];
size_t namelen = xp->inputs.namelen, hostlen, portlen;
const char *port;
struct timeval *tv = xp->inputs.timeout;
double deadline;
struct addrinfo hints, *res = NULL, *ai;
unsigned char bin[sizeof(struct in6_addr)];
int attempted = 0;
if (sock == NULL || name == NULL || namelen == 0) {
return phasync_orig_set_option(stream, PHP_STREAM_OPTION_XPORT_API, 0, xp);
}
/* "host:port" or "[v6]:port", as xp_socket's parse_ip_address reads it */
if (name[0] == '[') {
const char *end = memchr(name, ']', namelen);
if (!end || end + 1 >= name + namelen || end[1] != ':') {
return phasync_orig_set_option(stream, PHP_STREAM_OPTION_XPORT_API, 0, xp);
}
hostlen = end - name - 1;
memcpy(host, name + 1, MIN(hostlen, sizeof(host) - 1));
port = end + 2;
} else {
const char *colon = zend_memrchr(name, ':', namelen);
if (!colon) {
return phasync_orig_set_option(stream, PHP_STREAM_OPTION_XPORT_API, 0, xp);
}
hostlen = colon - name;
memcpy(host, name, MIN(hostlen, sizeof(host) - 1));
port = colon + 1;
}
if (hostlen >= sizeof(host)) {
return phasync_orig_set_option(stream, PHP_STREAM_OPTION_XPORT_API, 0, xp);
}
host[hostlen] = '\0';
portlen = name + namelen - port;
memset(&hints, 0, sizeof(hints));
hints.ai_family = phasync_ipv6_usable() ? AF_UNSPEC : AF_INET;
hints.ai_socktype = SOCK_STREAM;
if (inet_pton(AF_INET, host, bin) == 1 || inet_pton(AF_INET6, host, bin) == 1) {
hints.ai_flags = AI_NUMERICHOST; /* no DNS to offload */
if (getaddrinfo(host, NULL, &hints, &res) != 0) {
res = NULL;
}
} else {
phasync_task t;
memset(&t, 0, sizeof(t));
t.type = PHASYNC_OP_GETADDRINFO;
t.host = host;
t.hints = hints;
phasync_pool_run(&t);
if (EG(exception)) {
if (t.ai) freeaddrinfo(t.ai);
xp->outputs.returncode = -1;
return PHP_STREAM_OPTION_RETURN_OK; /* exception pending: propagates */
}
res = t.result == 0 ? t.ai : NULL;
if (!res && t.ai) freeaddrinfo(t.ai);
}
if (res == NULL) {
/* Lookup failed: let the native connect produce PHP's exact error. */
return phasync_orig_set_option(stream, PHP_STREAM_OPTION_XPORT_API, 0, xp);
}
/* Like native, the connect budget starts after the lookup, not before it. */
deadline = tv ? phasync_now() + (double) tv->tv_sec + (double) tv->tv_usec / 1000000.0 : 0;
for (ai = res; ai; ai = ai->ai_next) {
double remaining = tv ? deadline - phasync_now() : INFINITY;
char ip[INET6_ADDRSTRLEN];
int r;
if (attempted && remaining <= 0) {