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