xref: /PHP-8.2/Zend/zend_fibers.c (revision cd255007)
1 /*
2    +----------------------------------------------------------------------+
3    | Zend Engine                                                          |
4    +----------------------------------------------------------------------+
5    | Copyright (c) Zend Technologies Ltd. (http://www.zend.com)           |
6    +----------------------------------------------------------------------+
7    | This source file is subject to version 2.00 of the Zend license,     |
8    | that is bundled with this package in the file LICENSE, and is        |
9    | available through the world-wide-web at the following url:           |
10    | http://www.zend.com/license/2_00.txt.                                |
11    | If you did not receive a copy of the Zend license and are unable to  |
12    | obtain it through the world-wide-web, please send a note to          |
13    | license@zend.com so we can mail you a copy immediately.              |
14    +----------------------------------------------------------------------+
15    | Authors: Aaron Piotrowski <aaron@trowski.com>                        |
16    |          Martin Schröder <m.schroeder2007@gmail.com>                 |
17    +----------------------------------------------------------------------+
18 */
19 
20 #include "zend.h"
21 #include "zend_API.h"
22 #include "zend_gc.h"
23 #include "zend_ini.h"
24 #include "zend_vm.h"
25 #include "zend_exceptions.h"
26 #include "zend_builtin_functions.h"
27 #include "zend_observer.h"
28 #include "zend_mmap.h"
29 #include "zend_compile.h"
30 #include "zend_closures.h"
31 #include "zend_generators.h"
32 
33 #include "zend_fibers.h"
34 #include "zend_fibers_arginfo.h"
35 
36 #ifdef HAVE_VALGRIND
37 # include <valgrind/valgrind.h>
38 #endif
39 
40 #ifdef ZEND_FIBER_UCONTEXT
41 # include <ucontext.h>
42 #endif
43 
44 #ifndef ZEND_WIN32
45 # include <unistd.h>
46 # include <sys/mman.h>
47 # include <limits.h>
48 
49 # if !defined(MAP_ANONYMOUS) && defined(MAP_ANON)
50 #  define MAP_ANONYMOUS MAP_ANON
51 # endif
52 
53 /* FreeBSD require a first (i.e. addr) argument of mmap(2) is not NULL
54  * if MAP_STACK is passed.
55  * http://www.FreeBSD.org/cgi/query-pr.cgi?pr=158755 */
56 # if !defined(MAP_STACK) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
57 #  undef MAP_STACK
58 #  define MAP_STACK 0
59 # endif
60 
61 # ifndef MAP_FAILED
62 #  define MAP_FAILED ((void * ) -1)
63 # endif
64 #endif
65 
66 #ifdef __SANITIZE_ADDRESS__
67 # include <sanitizer/asan_interface.h>
68 # include <sanitizer/common_interface_defs.h>
69 #endif
70 
71 /* Encapsulates the fiber C stack with extension for debugging tools. */
72 struct _zend_fiber_stack {
73 	void *pointer;
74 	size_t size;
75 
76 #ifdef HAVE_VALGRIND
77 	unsigned int valgrind_stack_id;
78 #endif
79 
80 #ifdef __SANITIZE_ADDRESS__
81 	const void *asan_pointer;
82 	size_t asan_size;
83 #endif
84 
85 #ifdef ZEND_FIBER_UCONTEXT
86 	/* Embedded ucontext to avoid unnecessary memory allocations. */
87 	ucontext_t ucontext;
88 #endif
89 };
90 
91 /* Zend VM state that needs to be captured / restored during fiber context switch. */
92 typedef struct _zend_fiber_vm_state {
93 	zend_vm_stack vm_stack;
94 	zval *vm_stack_top;
95 	zval *vm_stack_end;
96 	size_t vm_stack_page_size;
97 	zend_execute_data *current_execute_data;
98 	int error_reporting;
99 	uint32_t jit_trace_num;
100 	JMP_BUF *bailout;
101 	zend_fiber *active_fiber;
102 } zend_fiber_vm_state;
103 
zend_fiber_capture_vm_state(zend_fiber_vm_state * state)104 static zend_always_inline void zend_fiber_capture_vm_state(zend_fiber_vm_state *state)
105 {
106 	state->vm_stack = EG(vm_stack);
107 	state->vm_stack_top = EG(vm_stack_top);
108 	state->vm_stack_end = EG(vm_stack_end);
109 	state->vm_stack_page_size = EG(vm_stack_page_size);
110 	state->current_execute_data = EG(current_execute_data);
111 	state->error_reporting = EG(error_reporting);
112 	state->jit_trace_num = EG(jit_trace_num);
113 	state->bailout = EG(bailout);
114 	state->active_fiber = EG(active_fiber);
115 }
116 
zend_fiber_restore_vm_state(zend_fiber_vm_state * state)117 static zend_always_inline void zend_fiber_restore_vm_state(zend_fiber_vm_state *state)
118 {
119 	EG(vm_stack) = state->vm_stack;
120 	EG(vm_stack_top) = state->vm_stack_top;
121 	EG(vm_stack_end) = state->vm_stack_end;
122 	EG(vm_stack_page_size) = state->vm_stack_page_size;
123 	EG(current_execute_data) = state->current_execute_data;
124 	EG(error_reporting) = state->error_reporting;
125 	EG(jit_trace_num) = state->jit_trace_num;
126 	EG(bailout) = state->bailout;
127 	EG(active_fiber) = state->active_fiber;
128 }
129 
130 #ifdef ZEND_FIBER_UCONTEXT
131 ZEND_TLS zend_fiber_transfer *transfer_data;
132 #else
133 /* boost_context_data is our customized definition of struct transfer_t as
134  * provided by boost.context in fcontext.hpp:
135  *
136  * typedef void* fcontext_t;
137  *
138  * struct transfer_t {
139  *     fcontext_t fctx;
140  *     void *data;
141  * }; */
142 
143 typedef struct {
144 	void *handle;
145 	zend_fiber_transfer *transfer;
146 } boost_context_data;
147 
148 /* These functions are defined in assembler files provided by boost.context (located in "Zend/asm"). */
149 extern void *make_fcontext(void *sp, size_t size, void (*fn)(boost_context_data));
150 extern ZEND_INDIRECT_RETURN boost_context_data jump_fcontext(void *to, zend_fiber_transfer *transfer);
151 #endif
152 
153 ZEND_API zend_class_entry *zend_ce_fiber;
154 static zend_class_entry *zend_ce_fiber_error;
155 
156 static zend_object_handlers zend_fiber_handlers;
157 
158 static zend_function zend_fiber_function = { ZEND_INTERNAL_FUNCTION };
159 
160 ZEND_TLS uint32_t zend_fiber_switch_blocking = 0;
161 
162 #define ZEND_FIBER_DEFAULT_PAGE_SIZE 4096
163 
zend_fiber_get_page_size(void)164 static size_t zend_fiber_get_page_size(void)
165 {
166 	static size_t page_size = 0;
167 
168 	if (!page_size) {
169 		page_size = zend_get_page_size();
170 		if (!page_size || (page_size & (page_size - 1))) {
171 			/* anyway, we have to return a valid result */
172 			page_size = ZEND_FIBER_DEFAULT_PAGE_SIZE;
173 		}
174 	}
175 
176 	return page_size;
177 }
178 
zend_fiber_stack_allocate(size_t size)179 static zend_fiber_stack *zend_fiber_stack_allocate(size_t size)
180 {
181 	void *pointer;
182 	const size_t page_size = zend_fiber_get_page_size();
183 	const size_t minimum_stack_size = page_size + ZEND_FIBER_GUARD_PAGES * page_size;
184 
185 	if (size < minimum_stack_size) {
186 		zend_throw_exception_ex(NULL, 0, "Fiber stack size is too small, it needs to be at least %zu bytes", minimum_stack_size);
187 		return NULL;
188 	}
189 
190 	const size_t stack_size = (size + page_size - 1) / page_size * page_size;
191 	const size_t alloc_size = stack_size + ZEND_FIBER_GUARD_PAGES * page_size;
192 
193 #ifdef ZEND_WIN32
194 	pointer = VirtualAlloc(0, alloc_size, MEM_COMMIT, PAGE_READWRITE);
195 
196 	if (!pointer) {
197 		DWORD err = GetLastError();
198 		char *errmsg = php_win32_error_to_msg(err);
199 		zend_throw_exception_ex(NULL, 0, "Fiber stack allocate failed: VirtualAlloc failed: [0x%08lx] %s", err, errmsg[0] ? errmsg : "Unknown");
200 		php_win32_error_msg_free(errmsg);
201 		return NULL;
202 	}
203 
204 # if ZEND_FIBER_GUARD_PAGES
205 	DWORD protect;
206 
207 	if (!VirtualProtect(pointer, ZEND_FIBER_GUARD_PAGES * page_size, PAGE_READWRITE | PAGE_GUARD, &protect)) {
208 		DWORD err = GetLastError();
209 		char *errmsg = php_win32_error_to_msg(err);
210 		zend_throw_exception_ex(NULL, 0, "Fiber stack protect failed: VirtualProtect failed: [0x%08lx] %s", err, errmsg[0] ? errmsg : "Unknown");
211 		php_win32_error_msg_free(errmsg);
212 		VirtualFree(pointer, 0, MEM_RELEASE);
213 		return NULL;
214 	}
215 # endif
216 #else
217 	pointer = mmap(NULL, alloc_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
218 
219 	if (pointer == MAP_FAILED) {
220 		zend_throw_exception_ex(NULL, 0, "Fiber stack allocate failed: mmap failed: %s (%d)", strerror(errno), errno);
221 		return NULL;
222 	}
223 
224 	zend_mmap_set_name(pointer, alloc_size, "zend_fiber_stack");
225 
226 # if ZEND_FIBER_GUARD_PAGES
227 	if (mprotect(pointer, ZEND_FIBER_GUARD_PAGES * page_size, PROT_NONE) < 0) {
228 		zend_throw_exception_ex(NULL, 0, "Fiber stack protect failed: mprotect failed: %s (%d)", strerror(errno), errno);
229 		munmap(pointer, alloc_size);
230 		return NULL;
231 	}
232 # endif
233 #endif
234 
235 	zend_fiber_stack *stack = emalloc(sizeof(zend_fiber_stack));
236 
237 	stack->pointer = (void *) ((uintptr_t) pointer + ZEND_FIBER_GUARD_PAGES * page_size);
238 	stack->size = stack_size;
239 
240 #ifdef VALGRIND_STACK_REGISTER
241 	uintptr_t base = (uintptr_t) stack->pointer;
242 	stack->valgrind_stack_id = VALGRIND_STACK_REGISTER(base, base + stack->size);
243 #endif
244 
245 #ifdef __SANITIZE_ADDRESS__
246 	stack->asan_pointer = stack->pointer;
247 	stack->asan_size = stack->size;
248 #endif
249 
250 	return stack;
251 }
252 
zend_fiber_stack_free(zend_fiber_stack * stack)253 static void zend_fiber_stack_free(zend_fiber_stack *stack)
254 {
255 #ifdef VALGRIND_STACK_DEREGISTER
256 	VALGRIND_STACK_DEREGISTER(stack->valgrind_stack_id);
257 #endif
258 
259 	const size_t page_size = zend_fiber_get_page_size();
260 
261 	void *pointer = (void *) ((uintptr_t) stack->pointer - ZEND_FIBER_GUARD_PAGES * page_size);
262 
263 #ifdef __SANITIZE_ADDRESS__
264 	/* If another mmap happens after unmapping, it may trigger the stale stack red zones
265 	 * so we have to unpoison it before unmapping. */
266 	ASAN_UNPOISON_MEMORY_REGION(pointer, stack->size + ZEND_FIBER_GUARD_PAGES * page_size);
267 #endif
268 
269 #ifdef ZEND_WIN32
270 	VirtualFree(pointer, 0, MEM_RELEASE);
271 #else
272 	munmap(pointer, stack->size + ZEND_FIBER_GUARD_PAGES * page_size);
273 #endif
274 
275 	efree(stack);
276 }
277 #ifdef ZEND_FIBER_UCONTEXT
zend_fiber_trampoline(void)278 static ZEND_NORETURN void zend_fiber_trampoline(void)
279 #else
280 static ZEND_NORETURN void zend_fiber_trampoline(boost_context_data data)
281 #endif
282 {
283 	/* Initialize transfer struct with a copy of passed data. */
284 #ifdef ZEND_FIBER_UCONTEXT
285 	zend_fiber_transfer transfer = *transfer_data;
286 #else
287 	zend_fiber_transfer transfer = *data.transfer;
288 #endif
289 
290 	zend_fiber_context *from = transfer.context;
291 
292 #ifdef __SANITIZE_ADDRESS__
293 	__sanitizer_finish_switch_fiber(NULL, &from->stack->asan_pointer, &from->stack->asan_size);
294 #endif
295 
296 #ifndef ZEND_FIBER_UCONTEXT
297 	/* Get the context that resumed us and update its handle to allow for symmetric coroutines. */
298 	from->handle = data.handle;
299 #endif
300 
301 	/* Ensure that previous fiber will be cleaned up (needed by symmetric coroutines). */
302 	if (from->status == ZEND_FIBER_STATUS_DEAD) {
303 		zend_fiber_destroy_context(from);
304 	}
305 
306 	zend_fiber_context *context = EG(current_fiber_context);
307 
308 	context->function(&transfer);
309 	context->status = ZEND_FIBER_STATUS_DEAD;
310 
311 	/* Final context switch, the fiber must not be resumed afterwards! */
312 	zend_fiber_switch_context(&transfer);
313 
314 	/* Abort here because we are in an inconsistent program state. */
315 	abort();
316 }
317 
zend_fiber_switch_block(void)318 ZEND_API void zend_fiber_switch_block(void)
319 {
320 	++zend_fiber_switch_blocking;
321 }
322 
zend_fiber_switch_unblock(void)323 ZEND_API void zend_fiber_switch_unblock(void)
324 {
325 	ZEND_ASSERT(zend_fiber_switch_blocking && "Fiber switching was not blocked");
326 	--zend_fiber_switch_blocking;
327 }
328 
zend_fiber_switch_blocked(void)329 ZEND_API bool zend_fiber_switch_blocked(void)
330 {
331 	return zend_fiber_switch_blocking;
332 }
333 
zend_fiber_init_context(zend_fiber_context * context,void * kind,zend_fiber_coroutine coroutine,size_t stack_size)334 ZEND_API bool zend_fiber_init_context(zend_fiber_context *context, void *kind, zend_fiber_coroutine coroutine, size_t stack_size)
335 {
336 	context->stack = zend_fiber_stack_allocate(stack_size);
337 
338 	if (UNEXPECTED(!context->stack)) {
339 		return false;
340 	}
341 
342 #ifdef ZEND_FIBER_UCONTEXT
343 	ucontext_t *handle = &context->stack->ucontext;
344 
345 	getcontext(handle);
346 
347 	handle->uc_stack.ss_size = context->stack->size;
348 	handle->uc_stack.ss_sp = context->stack->pointer;
349 	handle->uc_stack.ss_flags = 0;
350 	handle->uc_link = NULL;
351 
352 	makecontext(handle, (void (*)(void)) zend_fiber_trampoline, 0);
353 
354 	context->handle = handle;
355 #else
356 	// Stack grows down, calculate the top of the stack. make_fcontext then shifts pointer to lower 16-byte boundary.
357 	void *stack = (void *) ((uintptr_t) context->stack->pointer + context->stack->size);
358 
359 	context->handle = make_fcontext(stack, context->stack->size, zend_fiber_trampoline);
360 	ZEND_ASSERT(context->handle != NULL && "make_fcontext() never returns NULL");
361 #endif
362 
363 	context->kind = kind;
364 	context->function = coroutine;
365 
366 	// Set status in case memory has not been zeroed.
367 	context->status = ZEND_FIBER_STATUS_INIT;
368 
369 	zend_observer_fiber_init_notify(context);
370 
371 	return true;
372 }
373 
zend_fiber_destroy_context(zend_fiber_context * context)374 ZEND_API void zend_fiber_destroy_context(zend_fiber_context *context)
375 {
376 	zend_observer_fiber_destroy_notify(context);
377 
378 	if (context->cleanup) {
379 		context->cleanup(context);
380 	}
381 
382 	zend_fiber_stack_free(context->stack);
383 }
384 
zend_fiber_switch_context(zend_fiber_transfer * transfer)385 ZEND_API void zend_fiber_switch_context(zend_fiber_transfer *transfer)
386 {
387 	zend_fiber_context *from = EG(current_fiber_context);
388 	zend_fiber_context *to = transfer->context;
389 	zend_fiber_vm_state state;
390 
391 	ZEND_ASSERT(to && to->handle && to->status != ZEND_FIBER_STATUS_DEAD && "Invalid fiber context");
392 	ZEND_ASSERT(from && "From fiber context must be present");
393 	ZEND_ASSERT(to != from && "Cannot switch into the running fiber context");
394 
395 	/* Assert that all error transfers hold a Throwable value. */
396 	ZEND_ASSERT((
397 		!(transfer->flags & ZEND_FIBER_TRANSFER_FLAG_ERROR) ||
398 		(Z_TYPE(transfer->value) == IS_OBJECT && (
399 			zend_is_unwind_exit(Z_OBJ(transfer->value)) ||
400 			zend_is_graceful_exit(Z_OBJ(transfer->value)) ||
401 			instanceof_function(Z_OBJCE(transfer->value), zend_ce_throwable)
402 		))
403 	) && "Error transfer requires a throwable value");
404 
405 	zend_observer_fiber_switch_notify(from, to);
406 
407 	zend_fiber_capture_vm_state(&state);
408 
409 	to->status = ZEND_FIBER_STATUS_RUNNING;
410 
411 	if (EXPECTED(from->status == ZEND_FIBER_STATUS_RUNNING)) {
412 		from->status = ZEND_FIBER_STATUS_SUSPENDED;
413 	}
414 
415 	/* Update transfer context with the current fiber before switching. */
416 	transfer->context = from;
417 
418 	EG(current_fiber_context) = to;
419 
420 #ifdef __SANITIZE_ADDRESS__
421 	void *fake_stack = NULL;
422 	__sanitizer_start_switch_fiber(
423 		from->status != ZEND_FIBER_STATUS_DEAD ? &fake_stack : NULL,
424 		to->stack->asan_pointer,
425 		to->stack->asan_size);
426 #endif
427 
428 #ifdef ZEND_FIBER_UCONTEXT
429 	transfer_data = transfer;
430 
431 	swapcontext(from->handle, to->handle);
432 
433 	/* Copy transfer struct because it might live on the other fiber's stack that will eventually be destroyed. */
434 	*transfer = *transfer_data;
435 #else
436 	boost_context_data data = jump_fcontext(to->handle, transfer);
437 
438 	/* Copy transfer struct because it might live on the other fiber's stack that will eventually be destroyed. */
439 	*transfer = *data.transfer;
440 #endif
441 
442 	to = transfer->context;
443 
444 #ifndef ZEND_FIBER_UCONTEXT
445 	/* Get the context that resumed us and update its handle to allow for symmetric coroutines. */
446 	to->handle = data.handle;
447 #endif
448 
449 #ifdef __SANITIZE_ADDRESS__
450 	__sanitizer_finish_switch_fiber(fake_stack, &to->stack->asan_pointer, &to->stack->asan_size);
451 #endif
452 
453 	EG(current_fiber_context) = from;
454 
455 	zend_fiber_restore_vm_state(&state);
456 
457 	/* Destroy prior context if it has been marked as dead. */
458 	if (to->status == ZEND_FIBER_STATUS_DEAD) {
459 		zend_fiber_destroy_context(to);
460 	}
461 }
462 
zend_fiber_cleanup(zend_fiber_context * context)463 static void zend_fiber_cleanup(zend_fiber_context *context)
464 {
465 	zend_fiber *fiber = zend_fiber_from_context(context);
466 
467 	zend_vm_stack current_stack = EG(vm_stack);
468 	EG(vm_stack) = fiber->vm_stack;
469 	zend_vm_stack_destroy();
470 	EG(vm_stack) = current_stack;
471 	fiber->execute_data = NULL;
472 	fiber->stack_bottom = NULL;
473 	fiber->caller = NULL;
474 }
475 
zend_fiber_execute(zend_fiber_transfer * transfer)476 static ZEND_STACK_ALIGNED void zend_fiber_execute(zend_fiber_transfer *transfer)
477 {
478 	ZEND_ASSERT(Z_TYPE(transfer->value) == IS_NULL && "Initial transfer value to fiber context must be NULL");
479 	ZEND_ASSERT(!transfer->flags && "No flags should be set on initial transfer");
480 
481 	zend_fiber *fiber = EG(active_fiber);
482 
483 	/* Determine the current error_reporting ini setting. */
484 	zend_long error_reporting = INI_INT("error_reporting");
485 	/* If error_reporting is 0 and not explicitly set to 0, INI_STR returns a null pointer. */
486 	if (!error_reporting && !INI_STR("error_reporting")) {
487 		error_reporting = E_ALL;
488 	}
489 
490 	EG(vm_stack) = NULL;
491 
492 	zend_first_try {
493 		zend_vm_stack stack = zend_vm_stack_new_page(ZEND_FIBER_VM_STACK_SIZE, NULL);
494 		EG(vm_stack) = stack;
495 		EG(vm_stack_top) = stack->top + ZEND_CALL_FRAME_SLOT;
496 		EG(vm_stack_end) = stack->end;
497 		EG(vm_stack_page_size) = ZEND_FIBER_VM_STACK_SIZE;
498 
499 		fiber->execute_data = (zend_execute_data *) stack->top;
500 		fiber->stack_bottom = fiber->execute_data;
501 
502 		memset(fiber->execute_data, 0, sizeof(zend_execute_data));
503 
504 		fiber->execute_data->func = &zend_fiber_function;
505 		fiber->stack_bottom->prev_execute_data = EG(current_execute_data);
506 
507 		EG(current_execute_data) = fiber->execute_data;
508 		EG(jit_trace_num) = 0;
509 		EG(error_reporting) = error_reporting;
510 
511 		fiber->fci.retval = &fiber->result;
512 
513 		zend_call_function(&fiber->fci, &fiber->fci_cache);
514 
515 		/* Cleanup callback and unset field to prevent GC / duplicate dtor issues. */
516 		zval_ptr_dtor(&fiber->fci.function_name);
517 		ZVAL_UNDEF(&fiber->fci.function_name);
518 
519 		if (EG(exception)) {
520 			if (!(fiber->flags & ZEND_FIBER_FLAG_DESTROYED)
521 				|| !(zend_is_graceful_exit(EG(exception)) || zend_is_unwind_exit(EG(exception)))
522 			) {
523 				fiber->flags |= ZEND_FIBER_FLAG_THREW;
524 				transfer->flags = ZEND_FIBER_TRANSFER_FLAG_ERROR;
525 
526 				ZVAL_OBJ_COPY(&transfer->value, EG(exception));
527 			}
528 
529 			zend_clear_exception();
530 		}
531 	} zend_catch {
532 		fiber->flags |= ZEND_FIBER_FLAG_BAILOUT;
533 		transfer->flags = ZEND_FIBER_TRANSFER_FLAG_BAILOUT;
534 	} zend_end_try();
535 
536 	fiber->context.cleanup = &zend_fiber_cleanup;
537 	fiber->vm_stack = EG(vm_stack);
538 
539 	transfer->context = fiber->caller;
540 }
541 
542 /* Handles forwarding of result / error from a transfer into the running fiber. */
zend_fiber_delegate_transfer_result(zend_fiber_transfer * transfer,INTERNAL_FUNCTION_PARAMETERS)543 static zend_always_inline void zend_fiber_delegate_transfer_result(
544 	zend_fiber_transfer *transfer, INTERNAL_FUNCTION_PARAMETERS
545 ) {
546 	if (transfer->flags & ZEND_FIBER_TRANSFER_FLAG_ERROR) {
547 		/* Use internal throw to skip the Throwable-check that would fail for (graceful) exit. */
548 		zend_throw_exception_internal(Z_OBJ(transfer->value));
549 		RETURN_THROWS();
550 	}
551 
552 	RETURN_COPY_VALUE(&transfer->value);
553 }
554 
zend_fiber_switch_to(zend_fiber_context * context,zval * value,bool exception)555 static zend_always_inline zend_fiber_transfer zend_fiber_switch_to(
556 	zend_fiber_context *context, zval *value, bool exception
557 ) {
558 	zend_fiber_transfer transfer = {
559 		.context = context,
560 		.flags = exception ? ZEND_FIBER_TRANSFER_FLAG_ERROR : 0,
561 	};
562 
563 	if (value) {
564 		ZVAL_COPY(&transfer.value, value);
565 	} else {
566 		ZVAL_NULL(&transfer.value);
567 	}
568 
569 	zend_fiber_switch_context(&transfer);
570 
571 	/* Forward bailout into current fiber. */
572 	if (UNEXPECTED(transfer.flags & ZEND_FIBER_TRANSFER_FLAG_BAILOUT)) {
573 		EG(active_fiber) = NULL;
574 		zend_bailout();
575 	}
576 
577 	return transfer;
578 }
579 
zend_fiber_resume(zend_fiber * fiber,zval * value,bool exception)580 static zend_always_inline zend_fiber_transfer zend_fiber_resume(zend_fiber *fiber, zval *value, bool exception)
581 {
582 	zend_fiber *previous = EG(active_fiber);
583 
584 	if (previous) {
585 		previous->execute_data = EG(current_execute_data);
586 	}
587 
588 	fiber->caller = EG(current_fiber_context);
589 	EG(active_fiber) = fiber;
590 
591 	zend_fiber_transfer transfer = zend_fiber_switch_to(fiber->previous, value, exception);
592 
593 	EG(active_fiber) = previous;
594 
595 	return transfer;
596 }
597 
zend_fiber_suspend(zend_fiber * fiber,zval * value)598 static zend_always_inline zend_fiber_transfer zend_fiber_suspend(zend_fiber *fiber, zval *value)
599 {
600 	ZEND_ASSERT(fiber->caller != NULL);
601 
602 	zend_fiber_context *caller = fiber->caller;
603 	fiber->previous = EG(current_fiber_context);
604 	fiber->caller = NULL;
605 	fiber->execute_data = EG(current_execute_data);
606 
607 	return zend_fiber_switch_to(caller, value, false);
608 }
609 
zend_fiber_object_create(zend_class_entry * ce)610 static zend_object *zend_fiber_object_create(zend_class_entry *ce)
611 {
612 	zend_fiber *fiber = emalloc(sizeof(zend_fiber));
613 
614 	memset(fiber, 0, sizeof(zend_fiber));
615 
616 	zend_object_std_init(&fiber->std, ce);
617 	fiber->std.handlers = &zend_fiber_handlers;
618 
619 	return &fiber->std;
620 }
621 
zend_fiber_object_destroy(zend_object * object)622 static void zend_fiber_object_destroy(zend_object *object)
623 {
624 	zend_fiber *fiber = (zend_fiber *) object;
625 
626 	if (fiber->context.status != ZEND_FIBER_STATUS_SUSPENDED) {
627 		return;
628 	}
629 
630 	zend_object *exception = EG(exception);
631 	EG(exception) = NULL;
632 
633 	zval graceful_exit;
634 	ZVAL_OBJ(&graceful_exit, zend_create_graceful_exit());
635 
636 	fiber->flags |= ZEND_FIBER_FLAG_DESTROYED;
637 
638 	zend_fiber_transfer transfer = zend_fiber_resume(fiber, &graceful_exit, true);
639 
640 	zval_ptr_dtor(&graceful_exit);
641 
642 	if (transfer.flags & ZEND_FIBER_TRANSFER_FLAG_ERROR) {
643 		EG(exception) = Z_OBJ(transfer.value);
644 
645 		if (!exception && EG(current_execute_data) && EG(current_execute_data)->func
646 				&& ZEND_USER_CODE(EG(current_execute_data)->func->common.type)) {
647 			zend_rethrow_exception(EG(current_execute_data));
648 		}
649 
650 		zend_exception_set_previous(EG(exception), exception);
651 
652 		if (!EG(current_execute_data)) {
653 			zend_exception_error(EG(exception), E_ERROR);
654 		}
655 	} else {
656 		zval_ptr_dtor(&transfer.value);
657 		EG(exception) = exception;
658 	}
659 }
660 
zend_fiber_object_free(zend_object * object)661 static void zend_fiber_object_free(zend_object *object)
662 {
663 	zend_fiber *fiber = (zend_fiber *) object;
664 
665 	zval_ptr_dtor(&fiber->fci.function_name);
666 	zval_ptr_dtor(&fiber->result);
667 
668 	zend_object_std_dtor(&fiber->std);
669 }
670 
zend_fiber_object_gc(zend_object * object,zval ** table,int * num)671 static HashTable *zend_fiber_object_gc(zend_object *object, zval **table, int *num)
672 {
673 	zend_fiber *fiber = (zend_fiber *) object;
674 	zend_get_gc_buffer *buf = zend_get_gc_buffer_create();
675 
676 	zend_get_gc_buffer_add_zval(buf, &fiber->fci.function_name);
677 	zend_get_gc_buffer_add_zval(buf, &fiber->result);
678 
679 	if (fiber->context.status != ZEND_FIBER_STATUS_SUSPENDED || fiber->caller != NULL) {
680 		zend_get_gc_buffer_use(buf, table, num);
681 		return NULL;
682 	}
683 
684 	HashTable *lastSymTable = NULL;
685 	zend_execute_data *ex = fiber->execute_data;
686 	for (; ex; ex = ex->prev_execute_data) {
687 		HashTable *symTable;
688 		if (ZEND_CALL_INFO(ex) & ZEND_CALL_GENERATOR) {
689 			/* The generator object is stored in ex->return_value */
690 			zend_generator *generator = (zend_generator*)ex->return_value;
691 			/* There are two cases to consider:
692 			 * - If the generator is currently running, the Generator's GC
693 			 *   handler will ignore it because it is not collectable. However,
694 			 *   in this context the generator is suspended in Fiber::suspend()
695 			 *   and may be collectable, so we can inspect it.
696 			 * - If the generator is not running, the Generator's GC handler
697 			 *   will inspect it. In this case we have to skip the frame.
698 			 */
699 			if (!(generator->flags & ZEND_GENERATOR_CURRENTLY_RUNNING)) {
700 				continue;
701 			}
702 			symTable = zend_generator_frame_gc(buf, generator);
703 		} else {
704 			symTable = zend_unfinished_execution_gc_ex(ex, ex->func && ZEND_USER_CODE(ex->func->type) ? ex->call : NULL, buf, false);
705 		}
706 		if (symTable) {
707 			if (lastSymTable) {
708 				zval *val;
709 				ZEND_HASH_FOREACH_VAL(lastSymTable, val) {
710 					if (EXPECTED(Z_TYPE_P(val) == IS_INDIRECT)) {
711 						val = Z_INDIRECT_P(val);
712 					}
713 					zend_get_gc_buffer_add_zval(buf, val);
714 				} ZEND_HASH_FOREACH_END();
715 			}
716 			lastSymTable = symTable;
717 		}
718 	}
719 
720 	zend_get_gc_buffer_use(buf, table, num);
721 
722 	return lastSymTable;
723 }
724 
ZEND_METHOD(Fiber,__construct)725 ZEND_METHOD(Fiber, __construct)
726 {
727 	zend_fcall_info fci;
728 	zend_fcall_info_cache fcc;
729 
730 	ZEND_PARSE_PARAMETERS_START(1, 1)
731 		Z_PARAM_FUNC(fci, fcc)
732 	ZEND_PARSE_PARAMETERS_END();
733 
734 	zend_fiber *fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS);
735 
736 	if (UNEXPECTED(fiber->context.status != ZEND_FIBER_STATUS_INIT || Z_TYPE(fiber->fci.function_name) != IS_UNDEF)) {
737 		zend_throw_error(zend_ce_fiber_error, "Cannot call constructor twice");
738 		RETURN_THROWS();
739 	}
740 
741 	fiber->fci = fci;
742 	fiber->fci_cache = fcc;
743 
744 	// Keep a reference to closures or callable objects while the fiber is running.
745 	Z_TRY_ADDREF(fiber->fci.function_name);
746 }
747 
ZEND_METHOD(Fiber,start)748 ZEND_METHOD(Fiber, start)
749 {
750 	zend_fiber *fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS);
751 
752 	ZEND_PARSE_PARAMETERS_START(0, -1)
753 		Z_PARAM_VARIADIC_WITH_NAMED(fiber->fci.params, fiber->fci.param_count, fiber->fci.named_params);
754 	ZEND_PARSE_PARAMETERS_END();
755 
756 	if (UNEXPECTED(zend_fiber_switch_blocked())) {
757 		zend_throw_error(zend_ce_fiber_error, "Cannot switch fibers in current execution context");
758 		RETURN_THROWS();
759 	}
760 
761 	if (fiber->context.status != ZEND_FIBER_STATUS_INIT) {
762 		zend_throw_error(zend_ce_fiber_error, "Cannot start a fiber that has already been started");
763 		RETURN_THROWS();
764 	}
765 
766 	if (!zend_fiber_init_context(&fiber->context, zend_ce_fiber, zend_fiber_execute, EG(fiber_stack_size))) {
767 		RETURN_THROWS();
768 	}
769 
770 	fiber->previous = &fiber->context;
771 
772 	zend_fiber_transfer transfer = zend_fiber_resume(fiber, NULL, false);
773 
774 	zend_fiber_delegate_transfer_result(&transfer, INTERNAL_FUNCTION_PARAM_PASSTHRU);
775 }
776 
ZEND_METHOD(Fiber,suspend)777 ZEND_METHOD(Fiber, suspend)
778 {
779 	zval *value = NULL;
780 
781 	ZEND_PARSE_PARAMETERS_START(0, 1)
782 		Z_PARAM_OPTIONAL
783 		Z_PARAM_ZVAL(value);
784 	ZEND_PARSE_PARAMETERS_END();
785 
786 	zend_fiber *fiber = EG(active_fiber);
787 
788 	if (UNEXPECTED(!fiber)) {
789 		zend_throw_error(zend_ce_fiber_error, "Cannot suspend outside of a fiber");
790 		RETURN_THROWS();
791 	}
792 
793 	if (UNEXPECTED(fiber->flags & ZEND_FIBER_FLAG_DESTROYED)) {
794 		zend_throw_error(zend_ce_fiber_error, "Cannot suspend in a force-closed fiber");
795 		RETURN_THROWS();
796 	}
797 
798 	if (UNEXPECTED(zend_fiber_switch_blocked())) {
799 		zend_throw_error(zend_ce_fiber_error, "Cannot switch fibers in current execution context");
800 		RETURN_THROWS();
801 	}
802 
803 	ZEND_ASSERT(fiber->context.status == ZEND_FIBER_STATUS_RUNNING || fiber->context.status == ZEND_FIBER_STATUS_SUSPENDED);
804 
805 	fiber->stack_bottom->prev_execute_data = NULL;
806 
807 	zend_fiber_transfer transfer = zend_fiber_suspend(fiber, value);
808 
809 	zend_fiber_delegate_transfer_result(&transfer, INTERNAL_FUNCTION_PARAM_PASSTHRU);
810 }
811 
ZEND_METHOD(Fiber,resume)812 ZEND_METHOD(Fiber, resume)
813 {
814 	zend_fiber *fiber;
815 	zval *value = NULL;
816 
817 	ZEND_PARSE_PARAMETERS_START(0, 1)
818 		Z_PARAM_OPTIONAL
819 		Z_PARAM_ZVAL(value);
820 	ZEND_PARSE_PARAMETERS_END();
821 
822 	if (UNEXPECTED(zend_fiber_switch_blocked())) {
823 		zend_throw_error(zend_ce_fiber_error, "Cannot switch fibers in current execution context");
824 		RETURN_THROWS();
825 	}
826 
827 	fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS);
828 
829 	if (UNEXPECTED(fiber->context.status != ZEND_FIBER_STATUS_SUSPENDED || fiber->caller != NULL)) {
830 		zend_throw_error(zend_ce_fiber_error, "Cannot resume a fiber that is not suspended");
831 		RETURN_THROWS();
832 	}
833 
834 	fiber->stack_bottom->prev_execute_data = EG(current_execute_data);
835 
836 	zend_fiber_transfer transfer = zend_fiber_resume(fiber, value, false);
837 
838 	zend_fiber_delegate_transfer_result(&transfer, INTERNAL_FUNCTION_PARAM_PASSTHRU);
839 }
840 
ZEND_METHOD(Fiber,throw)841 ZEND_METHOD(Fiber, throw)
842 {
843 	zend_fiber *fiber;
844 	zval *exception;
845 
846 	ZEND_PARSE_PARAMETERS_START(1, 1)
847 		Z_PARAM_OBJECT_OF_CLASS(exception, zend_ce_throwable)
848 	ZEND_PARSE_PARAMETERS_END();
849 
850 	if (UNEXPECTED(zend_fiber_switch_blocked())) {
851 		zend_throw_error(zend_ce_fiber_error, "Cannot switch fibers in current execution context");
852 		RETURN_THROWS();
853 	}
854 
855 	fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS);
856 
857 	if (UNEXPECTED(fiber->context.status != ZEND_FIBER_STATUS_SUSPENDED || fiber->caller != NULL)) {
858 		zend_throw_error(zend_ce_fiber_error, "Cannot resume a fiber that is not suspended");
859 		RETURN_THROWS();
860 	}
861 
862 	fiber->stack_bottom->prev_execute_data = EG(current_execute_data);
863 
864 	zend_fiber_transfer transfer = zend_fiber_resume(fiber, exception, true);
865 
866 	zend_fiber_delegate_transfer_result(&transfer, INTERNAL_FUNCTION_PARAM_PASSTHRU);
867 }
868 
ZEND_METHOD(Fiber,isStarted)869 ZEND_METHOD(Fiber, isStarted)
870 {
871 	zend_fiber *fiber;
872 
873 	ZEND_PARSE_PARAMETERS_NONE();
874 
875 	fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS);
876 
877 	RETURN_BOOL(fiber->context.status != ZEND_FIBER_STATUS_INIT);
878 }
879 
ZEND_METHOD(Fiber,isSuspended)880 ZEND_METHOD(Fiber, isSuspended)
881 {
882 	zend_fiber *fiber;
883 
884 	ZEND_PARSE_PARAMETERS_NONE();
885 
886 	fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS);
887 
888 	RETURN_BOOL(fiber->context.status == ZEND_FIBER_STATUS_SUSPENDED && fiber->caller == NULL);
889 }
890 
ZEND_METHOD(Fiber,isRunning)891 ZEND_METHOD(Fiber, isRunning)
892 {
893 	zend_fiber *fiber;
894 
895 	ZEND_PARSE_PARAMETERS_NONE();
896 
897 	fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS);
898 
899 	RETURN_BOOL(fiber->context.status == ZEND_FIBER_STATUS_RUNNING || fiber->caller != NULL);
900 }
901 
ZEND_METHOD(Fiber,isTerminated)902 ZEND_METHOD(Fiber, isTerminated)
903 {
904 	zend_fiber *fiber;
905 
906 	ZEND_PARSE_PARAMETERS_NONE();
907 
908 	fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS);
909 
910 	RETURN_BOOL(fiber->context.status == ZEND_FIBER_STATUS_DEAD);
911 }
912 
ZEND_METHOD(Fiber,getReturn)913 ZEND_METHOD(Fiber, getReturn)
914 {
915 	zend_fiber *fiber;
916 	const char *message;
917 
918 	ZEND_PARSE_PARAMETERS_NONE();
919 
920 	fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS);
921 
922 	if (fiber->context.status == ZEND_FIBER_STATUS_DEAD) {
923 		if (fiber->flags & ZEND_FIBER_FLAG_THREW) {
924 			message = "The fiber threw an exception";
925 		} else if (fiber->flags & ZEND_FIBER_FLAG_BAILOUT) {
926 			message = "The fiber exited with a fatal error";
927 		} else {
928 			RETURN_COPY_DEREF(&fiber->result);
929 		}
930 	} else if (fiber->context.status == ZEND_FIBER_STATUS_INIT) {
931 		message = "The fiber has not been started";
932 	} else {
933 		message = "The fiber has not returned";
934 	}
935 
936 	zend_throw_error(zend_ce_fiber_error, "Cannot get fiber return value: %s", message);
937 	RETURN_THROWS();
938 }
939 
ZEND_METHOD(Fiber,getCurrent)940 ZEND_METHOD(Fiber, getCurrent)
941 {
942 	ZEND_PARSE_PARAMETERS_NONE();
943 
944 	zend_fiber *fiber = EG(active_fiber);
945 
946 	if (!fiber) {
947 		RETURN_NULL();
948 	}
949 
950 	RETURN_OBJ_COPY(&fiber->std);
951 }
952 
ZEND_METHOD(FiberError,__construct)953 ZEND_METHOD(FiberError, __construct)
954 {
955 	zend_throw_error(
956 		NULL,
957 		"The \"%s\" class is reserved for internal use and cannot be manually instantiated",
958 		ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name)
959 	);
960 }
961 
962 
zend_register_fiber_ce(void)963 void zend_register_fiber_ce(void)
964 {
965 	zend_ce_fiber = register_class_Fiber();
966 	zend_ce_fiber->create_object = zend_fiber_object_create;
967 
968 	zend_fiber_handlers = std_object_handlers;
969 	zend_fiber_handlers.dtor_obj = zend_fiber_object_destroy;
970 	zend_fiber_handlers.free_obj = zend_fiber_object_free;
971 	zend_fiber_handlers.get_gc = zend_fiber_object_gc;
972 	zend_fiber_handlers.clone_obj = NULL;
973 
974 	zend_ce_fiber_error = register_class_FiberError(zend_ce_error);
975 	zend_ce_fiber_error->create_object = zend_ce_error->create_object;
976 }
977 
zend_fiber_init(void)978 void zend_fiber_init(void)
979 {
980 	zend_fiber_context *context = ecalloc(1, sizeof(zend_fiber_context));
981 
982 #if defined(__SANITIZE_ADDRESS__) || defined(ZEND_FIBER_UCONTEXT)
983 	// Main fiber stack is only needed if ASan or ucontext is enabled.
984 	context->stack = emalloc(sizeof(zend_fiber_stack));
985 
986 #ifdef ZEND_FIBER_UCONTEXT
987 	context->handle = &context->stack->ucontext;
988 #endif
989 #endif
990 
991 	context->status = ZEND_FIBER_STATUS_RUNNING;
992 
993 	EG(main_fiber_context) = context;
994 	EG(current_fiber_context) = context;
995 	EG(active_fiber) = NULL;
996 
997 	zend_fiber_switch_blocking = 0;
998 }
999 
zend_fiber_shutdown(void)1000 void zend_fiber_shutdown(void)
1001 {
1002 #if defined(__SANITIZE_ADDRESS__) || defined(ZEND_FIBER_UCONTEXT)
1003 	efree(EG(main_fiber_context)->stack);
1004 #endif
1005 
1006 	efree(EG(main_fiber_context));
1007 
1008 	zend_fiber_switch_block();
1009 }
1010