Skip to content

Commit 16fdfa2

Browse files
fix: fatal php shutdowns (#2293)
Fixes #2268 (and maybe others) In that issue, a timeout during a `curl_multi` request leads to a fatal error and bailout during `php_request_shutdown()`. After looking at the [FPM](https://github.com/php/php-src/blob/9011bd31d7c26b2f255e550171548eb024d1e4ce/sapi/fpm/fpm/fpm_main.c#L1926) implementation I realized it also wraps `php_request_shutdown()` with a `zend_bailout`, which we don't. This PR wraps the shutdown function and restarts ZTS in case of an unexpected bailout, which fixes #2268 and prevents any potential crashes from bailouts during shutdown. Still a draft since I think it might make sense to wrap the whole request loop in a `zend_try`.
1 parent df0c892 commit 16fdfa2

File tree

2 files changed

+88
-57
lines changed

2 files changed

+88
-57
lines changed

frankenphp.c

Lines changed: 88 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,32 +1038,113 @@ static void set_thread_name(char *thread_name) {
10381038
#endif
10391039
}
10401040

1041+
static inline void reset_sandboxed_environment() {
1042+
if (sandboxed_env != NULL) {
1043+
zend_hash_release(sandboxed_env);
1044+
sandboxed_env = NULL;
1045+
}
1046+
}
1047+
10411048
static void *php_thread(void *arg) {
10421049
thread_index = (uintptr_t)arg;
10431050
char thread_name[16] = {0};
10441051
snprintf(thread_name, 16, "php-%" PRIxPTR, thread_index);
10451052
set_thread_name(thread_name);
10461053

1054+
/* Initial allocation of all global PHP memory for this thread */
10471055
#ifdef ZTS
1048-
/* initial resource fetch */
10491056
(void)ts_resource(0);
10501057
#ifdef PHP_WIN32
10511058
ZEND_TSRMLS_CACHE_UPDATE();
10521059
#endif
10531060
#endif
10541061

1055-
// loop until Go signals to stop
1056-
char *scriptName = NULL;
1057-
while ((scriptName = go_frankenphp_before_script_execution(thread_index))) {
1058-
go_frankenphp_after_script_execution(thread_index,
1059-
frankenphp_execute_script(scriptName));
1062+
bool thread_is_healthy = true;
1063+
bool has_attempted_shutdown = false;
1064+
1065+
/* Main loop of the PHP thread, execute a PHP script and repeat until Go
1066+
* signals to stop */
1067+
zend_first_try {
1068+
char *scriptName = NULL;
1069+
while ((scriptName = go_frankenphp_before_script_execution(thread_index))) {
1070+
has_attempted_shutdown = false;
1071+
1072+
frankenphp_update_request_context();
1073+
1074+
if (UNEXPECTED(php_request_startup() == FAILURE)) {
1075+
/* Request startup failed, bail out to zend_catch */
1076+
frankenphp_log_message("Request startup failed, thread is unhealthy",
1077+
LOG_ERR);
1078+
zend_bailout();
1079+
}
1080+
1081+
zend_file_handle file_handle;
1082+
zend_stream_init_filename(&file_handle, scriptName);
1083+
1084+
file_handle.primary_script = 1;
1085+
EG(exit_status) = 0;
1086+
1087+
/* Execute the PHP script, potential bailout to zend_catch */
1088+
php_execute_script(&file_handle);
1089+
zend_destroy_file_handle(&file_handle);
1090+
reset_sandboxed_environment();
1091+
1092+
/* Update the last memory usage for metrics */
1093+
__atomic_store_n(&thread_metrics[thread_index].last_memory_usage,
1094+
zend_memory_usage(0), __ATOMIC_RELAXED);
1095+
1096+
has_attempted_shutdown = true;
1097+
1098+
/* shutdown the request, potential bailout to zend_catch */
1099+
php_request_shutdown((void *)0);
1100+
frankenphp_free_request_context();
1101+
go_frankenphp_after_script_execution(thread_index, EG(exit_status));
1102+
}
10601103
}
1104+
zend_catch {
1105+
/* Critical failure from php_execute_script or php_request_shutdown, mark
1106+
* the thread as unhealthy */
1107+
thread_is_healthy = false;
1108+
if (!has_attempted_shutdown) {
1109+
/* php_request_shutdown() was not called, force a shutdown now */
1110+
reset_sandboxed_environment();
1111+
zend_try { php_request_shutdown((void *)0); }
1112+
zend_catch {}
1113+
zend_end_try();
1114+
}
10611115

1116+
/* Log the last error message, it must be cleared to prevent a crash when
1117+
* freeing execution globals */
1118+
if (PG(last_error_message)) {
1119+
go_log_attrs(thread_index, PG(last_error_message), 8, NULL);
1120+
PG(last_error_message) = NULL;
1121+
PG(last_error_file) = NULL;
1122+
}
1123+
frankenphp_free_request_context();
1124+
go_frankenphp_after_script_execution(thread_index, EG(exit_status));
1125+
}
1126+
zend_end_try();
1127+
1128+
/* free all global PHP memory reserved for this thread */
10621129
#ifdef ZTS
10631130
ts_free_thread();
10641131
#endif
10651132

1066-
go_frankenphp_on_thread_shutdown(thread_index);
1133+
/* Thread is healthy, signal to Go that the thread has shut down */
1134+
if (thread_is_healthy) {
1135+
go_frankenphp_on_thread_shutdown(thread_index);
1136+
1137+
return NULL;
1138+
}
1139+
1140+
/* Thread is unhealthy, PHP globals might be in a bad state after a bailout,
1141+
* restart the entire thread */
1142+
frankenphp_log_message("Restarting unhealthy thread", LOG_WARNING);
1143+
1144+
if (!frankenphp_new_php_thread(thread_index)) {
1145+
/* probably unreachable */
1146+
frankenphp_log_message("Failed to restart an unhealthy thread", LOG_ERR);
1147+
}
10671148

10681149
return NULL;
10691150
}
@@ -1197,55 +1278,6 @@ bool frankenphp_new_php_thread(uintptr_t thread_index) {
11971278
return true;
11981279
}
11991280

1200-
static int frankenphp_request_startup() {
1201-
frankenphp_update_request_context();
1202-
if (php_request_startup() == SUCCESS) {
1203-
return SUCCESS;
1204-
}
1205-
1206-
php_request_shutdown((void *)0);
1207-
frankenphp_free_request_context();
1208-
1209-
return FAILURE;
1210-
}
1211-
1212-
int frankenphp_execute_script(char *file_name) {
1213-
if (frankenphp_request_startup() == FAILURE) {
1214-
1215-
return FAILURE;
1216-
}
1217-
1218-
int status = SUCCESS;
1219-
1220-
zend_file_handle file_handle;
1221-
zend_stream_init_filename(&file_handle, file_name);
1222-
1223-
file_handle.primary_script = 1;
1224-
1225-
zend_first_try {
1226-
EG(exit_status) = 0;
1227-
php_execute_script(&file_handle);
1228-
status = EG(exit_status);
1229-
}
1230-
zend_catch { status = EG(exit_status); }
1231-
zend_end_try();
1232-
1233-
zend_destroy_file_handle(&file_handle);
1234-
1235-
/* Reset the sandboxed environment if it is in use */
1236-
if (sandboxed_env != NULL) {
1237-
zend_hash_release(sandboxed_env);
1238-
sandboxed_env = NULL;
1239-
}
1240-
1241-
__atomic_store_n(&thread_metrics[thread_index].last_memory_usage,
1242-
zend_memory_usage(0), __ATOMIC_RELAXED);
1243-
php_request_shutdown((void *)0);
1244-
frankenphp_free_request_context();
1245-
1246-
return status;
1247-
}
1248-
12491281
/* Use global variables to store CLI arguments to prevent useless allocations */
12501282
static char *cli_script;
12511283
static int cli_argc;

frankenphp.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ int frankenphp_new_main_thread(int num_threads);
169169
bool frankenphp_new_php_thread(uintptr_t thread_index);
170170

171171
bool frankenphp_shutdown_dummy_request(void);
172-
int frankenphp_execute_script(char *file_name);
173172
void frankenphp_update_local_thread_context(bool is_worker);
174173

175174
int frankenphp_execute_script_cli(char *script, int argc, char **argv,

0 commit comments

Comments
 (0)