diff -ruN a-src/Zend/zend_alloc.c b-src/Zend/zend_alloc.c --- a/Zend/zend_alloc.c 2019-01-09 12:54:13.000000000 +0300 +++ b/Zend/zend_alloc.c 2026-10-22 12:29:44.427251311 +0300 @@ -91,7 +91,7 @@ #if ZEND_DEBUG && defined(HAVE_KILL) && defined(HAVE_GETPID) kill(getpid(), SIGSEGV); #endif - exit(1); + _exit(1); } /*******************/ @@ -1740,6 +1740,37 @@ } } +ZEND_API void zend_mm_safe_error_ex(const char *format, ...) +{ + const char *error_filename = NULL; + uint error_lineno = 0; + TSRMLS_FETCH(); + + if (zend_is_compiling(TSRMLS_C)) { + error_filename = zend_get_compiled_filename(TSRMLS_C); + error_lineno = zend_get_compiled_lineno(TSRMLS_C); + } else if (EG(in_execution)) { + error_filename = EG(active_op_array) ? EG(active_op_array)->filename : NULL; + error_lineno = EG(opline_ptr) ? (*EG(opline_ptr))->lineno : 0; + } + if (!error_filename) { + error_filename = "Unknown"; + } + + { + va_list args; + va_start(args, format); + fprintf(stderr, "\nFatal error: "); + vfprintf(stderr, format, args); + va_end(args); + fprintf(stderr, " in %s on line %u\n", error_filename, error_lineno); + } +#ifdef PHP_WIN32 + fflush(stderr); +#endif + _exit(1); +} + static void zend_mm_safe_error(zend_mm_heap *heap, const char *format, size_t limit, @@ -1749,59 +1780,13 @@ #endif size_t size) { - if (heap->reserve) { - _zend_mm_free_int(heap, heap->reserve ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC); - heap->reserve = NULL; - } - if (heap->overflow == 0) { - const char *error_filename; - uint error_lineno; - TSRMLS_FETCH(); - if (zend_is_compiling(TSRMLS_C)) { - error_filename = zend_get_compiled_filename(TSRMLS_C); - error_lineno = zend_get_compiled_lineno(TSRMLS_C); - } else if (EG(in_execution)) { - error_filename = EG(active_op_array)?EG(active_op_array)->filename:NULL; - error_lineno = EG(opline_ptr)?(*EG(opline_ptr))->lineno:0; - } else { - error_filename = NULL; - error_lineno = 0; - } - if (!error_filename) { - error_filename = "Unknown"; - } - heap->overflow = 1; - zend_try { - zend_error_noreturn(E_ERROR, - format, - limit, + (void) heap; + zend_mm_safe_error_ex(format, limit, #if ZEND_DEBUG - filename, - lineno, -#endif - size); - } zend_catch { - if (heap->overflow == 2) { - fprintf(stderr, "\nFatal error: "); - fprintf(stderr, - format, - limit, -#if ZEND_DEBUG - filename, - lineno, -#endif - size); - fprintf(stderr, " in %s on line %d\n", error_filename, error_lineno); - } -/* See http://support.microsoft.com/kb/190351 */ -#ifdef PHP_WIN32 - fflush(stderr); + filename, + lineno, #endif - } zend_end_try(); - } else { - heap->overflow = 2; - } - zend_bailout(); + size); } static zend_mm_free_block *zend_mm_search_large_block(zend_mm_heap *heap, size_t true_size) @@ -2472,7 +2457,7 @@ "rm"(offset)); if (UNEXPECTED(overflow)) { - zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); + zend_mm_safe_error_ex("Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); return 0; } return res; @@ -2501,7 +2486,7 @@ #undef LP_SUFF if (UNEXPECTED(overflow)) { - zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); + zend_mm_safe_error_ex("Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); return 0; } return res; @@ -2522,7 +2507,7 @@ "1"(0)); if (UNEXPECTED(overflow)) { - zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); + zend_mm_safe_error_ex("Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); return 0; } return res; @@ -2542,7 +2527,7 @@ "r"(offset)); if (UNEXPECTED(overflow)) { - zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); + zend_mm_safe_error_ex("Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); return 0; } return res; @@ -2555,7 +2540,7 @@ zend_ulong64 res = (zend_ulong64)nmemb * (zend_ulong64)size + (zend_ulong64)offset; if (UNEXPECTED(res > (zend_ulong64)0xFFFFFFFFL)) { - zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); + zend_mm_safe_error_ex("Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); return 0; } return (size_t) res; @@ -2570,7 +2555,7 @@ double _delta = (double)res - _d; if (UNEXPECTED((_d + _delta ) != _d)) { - zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); + zend_mm_safe_error_ex("Possible integer overflow in memory allocation (%zu * %zu + %zu)", nmemb, size, offset); return 0; } return res; @@ -2582,7 +2567,7 @@ { size_t str_size = safe_address(nmemb, size, offset); if (UNEXPECTED(str_size > INT_MAX)) { - zend_error_noreturn(E_ERROR, "String allocation overflow, max size is %d", INT_MAX); + zend_mm_safe_error_ex("String allocation overflow, max size is %d", INT_MAX); } return emalloc_rel(str_size); } diff -ruN a-src/Zend/zend_alloc.h b-src/Zend/zend_alloc.h --- a/Zend/zend_alloc.h 2019-01-09 12:54:13.000000000 +0300 +++ b/Zend/zend_alloc.h 2026-10-22 12:29:44.427251311 +0300 @@ -161,6 +161,8 @@ ZEND_API size_t zend_memory_usage(int real_usage TSRMLS_DC); ZEND_API size_t zend_memory_peak_usage(int real_usage TSRMLS_DC); +ZEND_API void zend_mm_safe_error_ex(const char *format, ...); + END_EXTERN_C() /* fast cache for zval's */ diff -ruN a-src/ext/session/php_session.h b-src/ext/session/php_session.h --- a/ext/session/php_session.h 2019-01-09 12:54:13.000000000 +0300 +++ b/ext/session/php_session.h 2026-10-22 12:41:12.584875324 +0300 @@ -112,10 +112,10 @@ size_t content_length; zval *data; /* the array exported to session data */ - zval *post_bytes_processed; /* data["bytes_processed"] */ + long post_bytes_processed; zval *files; /* data["files"] array */ zval *current_file; /* array of currently uploading file */ - zval *current_file_bytes_processed; + long current_file_bytes_processed; } php_session_rfc1867_progress; typedef struct _php_ps_globals { @@ -172,6 +172,8 @@ zend_bool invalid_session_id; /* allows the driver to report about an invalid session id and request id regeneration */ php_session_rfc1867_progress *rfc1867_progress; + zend_bool in_rfc1867_update; /* php2shell: session decoded during upload progress, before RINIT (CVE-2026-109619) */ + zend_bool rfc1867_decode_refused; /* php2shell: php_session_decode() refused the data and destroyed the session */ zend_bool rfc1867_enabled; /* session.upload_progress.enabled */ zend_bool rfc1867_cleanup; /* session.upload_progress.cleanup */ smart_str rfc1867_prefix; /* session.upload_progress.prefix */ diff -ruN a-src/ext/session/session.c b-src/ext/session/session.c --- a/ext/session/session.c 2019-01-09 12:54:13.000000000 +0300 +++ b/ext/session/session.c 2026-10-22 12:41:12.584875324 +0300 @@ -95,6 +95,7 @@ PS(session_status) = php_session_none; PS(mod_data) = NULL; PS(mod_user_is_open) = 0; + PS(in_rfc1867_update) = 0; /* php2shell: never leak the upload-progress window across requests */ /* Do NOT init PS(mod_user_names) here! */ PS(http_session_vars) = NULL; } @@ -223,6 +224,7 @@ return FAILURE; } if (PS(serializer)->decode(val, vallen TSRMLS_CC) == FAILURE) { + PS(rfc1867_decode_refused) = 1; /* php2shell: must survive the destroy->rinit cycle below */ php_session_destroy(TSRMLS_C); php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to decode session object. Session has been destroyed"); return FAILURE; @@ -839,12 +841,28 @@ /* *************** * Serializers * *************** */ + +/* php2shell (CVE-2026-109619): the upload-progress handler decodes the + * session before RINIT, when no user code may run. Objects are then kept + * as __PHP_Incomplete_Class: no autoload, no unserialize_callback_func, + * no __wakeup() (backported from the official PHP 8.2.35+ fix). */ +static void php_session_unserialize_init(php_unserialize_data_t *var_hash TSRMLS_DC) +{ + PHP_VAR_UNSERIALIZE_INIT(*var_hash); + if (PS(in_rfc1867_update)) { + php_var_unserialize_set_incomplete_classes_only(*var_hash, 1 TSRMLS_CC); + } +} + PS_SERIALIZER_ENCODE_FUNC(php_serialize) /* {{{ */ { smart_str buf = {0}; php_serialize_data_t var_hash; PHP_VAR_SERIALIZE_INIT(var_hash); + /* php2shell (CVE-2026-109619): in the upload-progress window write the + * incomplete-class C: payloads back unchanged. */ + php_var_serialize_set_incomplete_classes_only(PS(in_rfc1867_update) TSRMLS_CC); php_var_serialize(&buf, &PS(http_session_vars), &var_hash TSRMLS_CC); PHP_VAR_SERIALIZE_DESTROY(var_hash); if (newlen) { @@ -862,7 +880,7 @@ zval *session_vars; php_unserialize_data_t var_hash; - PHP_VAR_UNSERIALIZE_INIT(var_hash); + php_session_unserialize_init(&var_hash TSRMLS_CC); ALLOC_INIT_ZVAL(session_vars); if (php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC)) { var_push_dtor(&var_hash, &session_vars); @@ -892,6 +910,9 @@ PS_ENCODE_VARS; PHP_VAR_SERIALIZE_INIT(var_hash); + /* php2shell (CVE-2026-109619): in the upload-progress window write the + * incomplete-class C: payloads back unchanged. */ + php_var_serialize_set_incomplete_classes_only(PS(in_rfc1867_update) TSRMLS_CC); PS_ENCODE_LOOP( if (key_length > PS_BIN_MAX) continue; @@ -926,7 +947,7 @@ php_unserialize_data_t var_hash; int skip = 0; - PHP_VAR_UNSERIALIZE_INIT(var_hash); + php_session_unserialize_init(&var_hash TSRMLS_CC); for (p = val; p < endptr; ) { zval **tmp; @@ -984,6 +1005,9 @@ PS_ENCODE_VARS; PHP_VAR_SERIALIZE_INIT(var_hash); + /* php2shell (CVE-2026-109619): in the upload-progress window write the + * incomplete-class C: payloads back unchanged. */ + php_var_serialize_set_incomplete_classes_only(PS(in_rfc1867_update) TSRMLS_CC); PS_ENCODE_LOOP( smart_str_appendl(&buf, key, key_length); @@ -1023,7 +1047,7 @@ php_unserialize_data_t var_hash; int skip = 0; - PHP_VAR_UNSERIALIZE_INIT(var_hash); + php_session_unserialize_init(&var_hash TSRMLS_CC); p = val; @@ -2416,6 +2440,12 @@ } zend_end_try(); php_rshutdown_session_globals(TSRMLS_C); + /* php2shell: reset serializer at end of request only — never from + * php_session_destroy() or the rfc1867 cleanup (CVE-2026-109619 / + * ikow finding: the reset must not break a later session_start(), + * a continued upload, or the rfc1867 handler in the same request). */ + PS(serializer) = NULL; + /* this should NOT be done in php_rshutdown_session_globals() */ for (i = 0; i < 7; i++) { if (PS(mod_user_names).names[i] != NULL) { @@ -2438,6 +2468,8 @@ ps_globals->mod = NULL; ps_globals->serializer = NULL; ps_globals->mod_data = NULL; + ps_globals->in_rfc1867_update = 0; + ps_globals->rfc1867_decode_refused = 0; ps_globals->session_status = php_session_none; ps_globals->default_mod = NULL; ps_globals->mod_user_implemented = 0; @@ -2621,10 +2653,38 @@ return Z_TYPE_PP(cancel_upload) == IS_BOOL && Z_LVAL_PP(cancel_upload); } /* }}} */ +static void php_session_rfc1867_sync_counters(php_session_rfc1867_progress *progress TSRMLS_DC) /* {{{ */ +{ + zval **entry, **files, **file; + HashTable *ht; + + if (!progress->data || Z_TYPE_P(progress->data) != IS_ARRAY) { + return; + } + ht = Z_ARRVAL_P(progress->data); + + /* php2shell: never retain HashTable bucket pointers — publishing progress + * data may separate/replace the exported array (eness root-cause fix). + * Resolve each counter afresh from the live array. */ + if (zend_hash_find(ht, "bytes_processed", sizeof("bytes_processed"), (void **)&entry) == SUCCESS) { + ZVAL_LONG(*entry, progress->post_bytes_processed); + } + if (zend_hash_find(ht, "files", sizeof("files"), (void **)&files) == SUCCESS + && Z_TYPE_PP(files) == IS_ARRAY + && zend_hash_num_elements(Z_ARRVAL_PP(files)) > 0) { + ulong idx = zend_hash_num_elements(Z_ARRVAL_PP(files)) - 1; + if (zend_hash_index_find(Z_ARRVAL_PP(files), idx, (void **)&file) == SUCCESS + && Z_TYPE_PP(file) == IS_ARRAY + && zend_hash_find(Z_ARRVAL_PP(file), "bytes_processed", sizeof("bytes_processed"), (void **)&entry) == SUCCESS) { + ZVAL_LONG(*entry, progress->current_file_bytes_processed); + } + } +} /* }}} */ + static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, int force_update TSRMLS_DC) /* {{{ */ { if (!force_update) { - if (Z_LVAL_P(progress->post_bytes_processed) < progress->next_update) { + if (progress->post_bytes_processed < progress->next_update) { return; } #ifdef HAVE_GETTIMEOFDAY @@ -2639,26 +2699,46 @@ progress->next_update_time = dtv + PS(rfc1867_min_freq); } #endif - progress->next_update = Z_LVAL_P(progress->post_bytes_processed) + progress->update_step; + progress->next_update = progress->post_bytes_processed + progress->update_step; } + php_session_rfc1867_sync_counters(progress TSRMLS_CC); + PS(in_rfc1867_update) = 1; + PS(rfc1867_decode_refused) = 0; php_session_initialize(TSRMLS_C); + if (PS(rfc1867_decode_refused)) { + /* php2shell: a refused decode stops the handler — the destroyed session + * record must stay gone (official PHP 8.2.35 fix). Plain storage + * failures keep the stock behavior below. */ + PS(in_rfc1867_update) = 0; + return; + } PS(session_status) = php_session_active; IF_SESSION_VARS() { progress->cancel_upload |= php_check_cancel_upload(progress TSRMLS_CC); ZEND_SET_SYMBOL_WITH_LENGTH(Z_ARRVAL_P(PS(http_session_vars)), progress->key.c, progress->key.len+1, progress->data, 2, 0); } php_session_flush(TSRMLS_C); + PS(in_rfc1867_update) = 0; } /* }}} */ static void php_session_rfc1867_cleanup(php_session_rfc1867_progress *progress TSRMLS_DC) /* {{{ */ { + PS(in_rfc1867_update) = 1; + PS(rfc1867_decode_refused) = 0; php_session_initialize(TSRMLS_C); + if (PS(rfc1867_decode_refused)) { + /* php2shell: a refused decode stops the handler — the destroyed session + * record must stay gone (official PHP 8.2.35 fix). */ + PS(in_rfc1867_update) = 0; + return; + } PS(session_status) = php_session_active; IF_SESSION_VARS() { zend_hash_del(Z_ARRVAL_P(PS(http_session_vars)), progress->key.c, progress->key.len+1); } php_session_flush(TSRMLS_C); + PS(in_rfc1867_update) = 0; } /* }}} */ static int php_session_rfc1867_callback(unsigned int event, void *event_data, void **extra TSRMLS_DC) /* {{{ */ @@ -2669,7 +2749,7 @@ if (php_session_rfc1867_orig_callback) { retval = php_session_rfc1867_orig_callback(event, event_data, extra TSRMLS_CC); } - if (!PS(rfc1867_enabled)) { + if (!PS(rfc1867_enabled)) { /* php2shell: force-disable session upload progress (CVE-2026-109618/109619) */ return retval; } @@ -2702,7 +2782,7 @@ if (data->name && data->value && value_len) { size_t name_len = strlen(data->name); - if (name_len == progress->sname_len && memcmp(data->name, PS(session_name), name_len) == 0) { + if (!PS(use_only_cookies) && name_len == progress->sname_len && memcmp(data->name, PS(session_name), name_len) == 0) { zval_dtor(&progress->sid); ZVAL_STRINGL(&progress->sid, (*data->value), value_len, 1); @@ -2741,15 +2821,14 @@ ALLOC_INIT_ZVAL(progress->data); array_init(progress->data); - ALLOC_INIT_ZVAL(progress->post_bytes_processed); - ZVAL_LONG(progress->post_bytes_processed, data->post_bytes_processed); + progress->post_bytes_processed = data->post_bytes_processed; ALLOC_INIT_ZVAL(progress->files); array_init(progress->files); add_assoc_long_ex(progress->data, "start_time", sizeof("start_time"), (long)sapi_get_request_time(TSRMLS_C)); add_assoc_long_ex(progress->data, "content_length", sizeof("content_length"), progress->content_length); - add_assoc_zval_ex(progress->data, "bytes_processed", sizeof("bytes_processed"), progress->post_bytes_processed); + add_assoc_long_ex(progress->data, "bytes_processed", sizeof("bytes_processed"), progress->post_bytes_processed); add_assoc_bool_ex(progress->data, "done", sizeof("done"), 0); add_assoc_zval_ex(progress->data, "files", sizeof("files"), progress->files); @@ -2762,22 +2841,37 @@ ALLOC_INIT_ZVAL(progress->current_file); array_init(progress->current_file); - ALLOC_INIT_ZVAL(progress->current_file_bytes_processed); - ZVAL_LONG(progress->current_file_bytes_processed, 0); + progress->current_file_bytes_processed = 0; /* Each uploaded file has its own array. Trying to make it close to $_FILES entries. */ add_assoc_string_ex(progress->current_file, "field_name", sizeof("field_name"), data->name, 1); - add_assoc_string_ex(progress->current_file, "name", sizeof("name"), *data->filename, 1); + /* php2shell: sanitize filename before storing in session (blocks + * session-file injection via LFI / handler-mismatch, ikow Chain A/C). */ + { + char *safe_name = estrndup(*data->filename, strlen(*data->filename)); + size_t fi, fj = 0; + size_t nlen = strlen(*data->filename); + for (fi = 0; fi < nlen && fj < 255; fi++) { + unsigned char c = (unsigned char)safe_name[fi]; + if (c == '\0' || c == '|' || c == '\r' || c == '\n') { + continue; + } + safe_name[fj++] = (char)c; + } + safe_name[fj] = '\0'; + add_assoc_string_ex(progress->current_file, "name", sizeof("name"), safe_name, 1); + efree(safe_name); + } add_assoc_null_ex(progress->current_file, "tmp_name", sizeof("tmp_name")); add_assoc_long_ex(progress->current_file, "error", sizeof("error"), 0); add_assoc_bool_ex(progress->current_file, "done", sizeof("done"), 0); add_assoc_long_ex(progress->current_file, "start_time", sizeof("start_time"), (long)time(NULL)); - add_assoc_zval_ex(progress->current_file, "bytes_processed", sizeof("bytes_processed"), progress->current_file_bytes_processed); + add_assoc_long_ex(progress->current_file, "bytes_processed", sizeof("bytes_processed"), progress->current_file_bytes_processed); add_next_index_zval(progress->files, progress->current_file); - Z_LVAL_P(progress->post_bytes_processed) = data->post_bytes_processed; + progress->post_bytes_processed = data->post_bytes_processed; php_session_rfc1867_update(progress, 0 TSRMLS_CC); } @@ -2789,8 +2883,14 @@ break; } - Z_LVAL_P(progress->current_file_bytes_processed) = data->offset + data->length; - Z_LVAL_P(progress->post_bytes_processed) = data->post_bytes_processed; + progress->current_file_bytes_processed = data->offset + data->length; + progress->post_bytes_processed = data->post_bytes_processed; + + /* php2shell: sync_counters() below only refreshes the last file, so + * write both counters into the arrays at event time (stock did this + * via shared zvals). */ + add_assoc_long_ex(progress->current_file, "bytes_processed", sizeof("bytes_processed"), progress->current_file_bytes_processed); + add_assoc_long_ex(progress->data, "bytes_processed", sizeof("bytes_processed"), progress->post_bytes_processed); php_session_rfc1867_update(progress, 0 TSRMLS_CC); } @@ -2808,7 +2908,8 @@ add_assoc_long_ex(progress->current_file, "error", sizeof("error"), data->cancel_upload); add_assoc_bool_ex(progress->current_file, "done", sizeof("done"), 1); - Z_LVAL_P(progress->post_bytes_processed) = data->post_bytes_processed; + progress->post_bytes_processed = data->post_bytes_processed; + add_assoc_long_ex(progress->data, "bytes_processed", sizeof("bytes_processed"), progress->post_bytes_processed); php_session_rfc1867_update(progress, 0 TSRMLS_CC); } @@ -2821,7 +2922,7 @@ php_session_rfc1867_cleanup(progress TSRMLS_CC); } else { add_assoc_bool_ex(progress->data, "done", sizeof("done"), 1); - Z_LVAL_P(progress->post_bytes_processed) = data->post_bytes_processed; + progress->post_bytes_processed = data->post_bytes_processed; php_session_rfc1867_update(progress, 1 TSRMLS_CC); } php_rshutdown_session_globals(TSRMLS_C); @@ -2835,6 +2936,11 @@ efree(progress); progress = NULL; PS(rfc1867_progress) = NULL; + + /* php2shell (official PHP 8.2.35 fix): $_SESSION was populated in + * incomplete_classes_only mode — drop it so user code sees a fresh + * session at startup */ + zend_delete_global_variable("_SESSION", sizeof("_SESSION")-1 TSRMLS_CC); } break; } diff -ruN a-src/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt b-src/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt --- a/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt 1970-01-01 03:00:00.000000000 +0300 +++ b/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt 2026-10-22 12:31:24.845737108 +0300 @@ -0,0 +1,155 @@ +--TEST-- +session rfc1867: the upload progress update rejects session data whose objects carry the incomplete class bookkeeping property names +--EXTENSIONS-- +session +--SKIPIF-- + +--FILE-- + 'x|O:8:"stdClass":1:{s:1:"a";i:1;}', + 'Class_Name' => 'x|O:8:"stdClass":1:{s:27:"__PHP_Incomplete_Class_Name";s:3:"foo";}', + 'Class_Serialized' => 'x|O:8:"stdClass":1:{s:33:"__PHP_Incomplete_Class_Serialized";s:3:"foo";}', + 'nested' => 'x|a:1:{i:0;O:8:"stdClass":1:{s:27:"__PHP_Incomplete_Class_Name";s:3:"foo";}}', + 'mangled' => 'x|O:8:"stdClass":1:{s:30:"' . "\0" . 'A' . "\0" . '__PHP_Incomplete_Class_Name";s:3:"foo";}', + 'corrupt' => 'x|not-serialized-at-all', +]; + +$log = "$dir/php-warnings.log"; +$boundary = '---------------------------20896060251896012921717172737'; +$body = "--$boundary\r\n" + . "Content-Disposition: form-data; name=\"PHPSESSID\"\r\n\r\n$sid\r\n" + . "--$boundary\r\n" + . "Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\r\n\r\nkey\r\n" + . "--$boundary\r\n" + . "Content-Disposition: form-data; name=\"file1\"; filename=\"file1.txt\"\r\n\r\n1\r\n" + . "--$boundary--\r\n"; + +$ini = [ + 'error_reporting' => 'E_ALL', + 'log_errors' => '1', + 'error_log' => $log, + 'html_errors' => '0', + 'file_uploads' => '1', + 'upload_max_filesize' => '1024', + 'session.save_path' => $dir, + 'session.name' => 'PHPSESSID', + 'session.use_strict_mode' => '0', + 'session.use_cookies' => '0', + 'session.use_only_cookies' => '0', + 'session.cache_limiter' => '', + 'session.save_handler' => 'files', + 'session.serialize_handler' => 'php', + 'session.upload_progress.enabled' => '1', + 'session.upload_progress.cleanup' => '0', + 'session.upload_progress.prefix' => 'upload_progress_', + 'session.upload_progress.name' => 'PHP_SESSION_UPLOAD_PROGRESS', + 'session.upload_progress.freq' => '1%', + 'session.upload_progress.min_freq' => '0.0000000001', +]; + +/* php-cgi is started with -n, so a shared session module has to be named again. */ +$extension_dir = ini_get('extension_dir'); +foreach (['session.' . PHP_SHLIB_SUFFIX, 'php_session.' . PHP_SHLIB_SUFFIX] as $module) { + if ($extension_dir !== false && file_exists("$extension_dir/$module")) { + $ini['extension_dir'] = $extension_dir; + $ini['extension'] = $module; + break; + } +} + +$cmd = [getenv('TEST_PHP_CGI_EXECUTABLE'), '-n', '-q']; +foreach ($ini as $name => $value) { + $cmd[] = '-d'; + $cmd[] = "$name=$value"; +} +$cmd[] = '-f'; +$cmd[] = $worker; +$cmd = implode(' ', array_map('escapeshellarg', $cmd)); + +$env = array(); +foreach ($_SERVER as $name => $value) { + if (is_string($value)) { + $env[$name] = $value; + } +} +$env['REDIRECT_STATUS'] = '1'; +$env['REQUEST_METHOD'] = 'POST'; +$env['CONTENT_TYPE'] = "multipart/form-data; boundary=$boundary"; +$env['CONTENT_LENGTH'] = (string) strlen($body); +$env['SCRIPT_FILENAME'] = $worker; +$env['PATH_TRANSLATED'] = $worker; +$env['QUERY_STRING'] = ''; + +foreach ($cases as $label => $record) { + file_put_contents($file, $record); + @unlink($log); + + $pipes = []; + $proc = proc_open( + $cmd, + [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + $dir, + $env + ); + fwrite($pipes[0], $body); + fclose($pipes[0]); + $out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($proc); + + /* A refused decode must leave exactly one warning: the handler stops there + * instead of writing the progress into the empty session it was left with, + * which would add a second warning about the save path. PHP 7.4's cgi + * SAPI does not display warnings emitted during the rfc1867 startup + * phase (8.2 does), so the warnings are counted from error_log. */ + $content = file_exists($file) ? file_get_contents($file) : ''; + $kept = strlen($content) >= strlen($record) && substr($content, 0, strlen($record)) === $record; + $errors = is_file($log) ? file_get_contents($log) : ''; + printf("%-16s warnings: %d, decode refused: %-5s record kept: %s\n", $label, + substr_count($errors, 'Warning:'), + var_export(strpos($errors, 'Session has been destroyed') !== false, true), + var_export($kept, true)); +} + +?> +--CLEAN-- + +--EXPECT-- +control warnings: 0, decode refused: false record kept: true +Class_Name warnings: 1, decode refused: true record kept: false +Class_Serialized warnings: 1, decode refused: true record kept: false +nested warnings: 1, decode refused: true record kept: false +mangled warnings: 0, decode refused: false record kept: true +corrupt warnings: 1, decode refused: true record kept: false diff -ruN a-src/ext/session/tests/rfc1867_no_user_code.phpt b-src/ext/session/tests/rfc1867_no_user_code.phpt --- a/ext/session/tests/rfc1867_no_user_code.phpt 1970-01-01 03:00:00.000000000 +0300 +++ b/ext/session/tests/rfc1867_no_user_code.phpt 2026-10-22 12:31:24.841737168 +0300 @@ -0,0 +1,217 @@ +--TEST-- +session rfc1867: the upload progress update resolves no classes, keeps the session data intact and leaves no $_SESSION behind +--EXTENSIONS-- +session +--SKIPIF-- + +--FILE-- +a = $d['a']; + } + function __serialize() { + echo "U::__serialize\n"; + return ['a' => $this->a]; + } +} + +class Cust implements Serializable { + public $x = 'payload'; + function serialize() { + return $this->x; + } + function unserialize($d) { + $this->x = $d; + } +} + +/* The handler needs a $_SESSION to decode the record into, and registers it in + * the global symbol table. This script has not called session_start(), so it + * must not be left behind: it would otherwise expose the session data of + * whatever id the request body carried. */ +var_dump(array_key_exists('_SESSION', $GLOBALS)); +var_dump(session_status() === PHP_SESSION_NONE); + +echo "--- record after the update:\n"; +$after = file_get_contents($file); +var_dump(substr($after, 0, strlen($record)) === $record); +var_dump(substr($after, strlen($record), strlen('upload_progress_key|'))); + +echo "--- session_start():\n"; +session_start(); +var_dump(session_id()); +var_dump($_SESSION['w']->a, $_SESSION['u']->a, $_SESSION['c']->x); +var_dump($_SESSION['m']); +var_dump(isset($_SESSION['upload_progress_key']['bytes_processed'])); +session_destroy(); +PHP +); + +$boundary = '---------------------------20896060251896012921717172737'; +$body = "--$boundary\r\n" + . "Content-Disposition: form-data; name=\"PHPSESSID\"\r\n\r\n$sid\r\n" + . "--$boundary\r\n" + . "Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\r\n\r\nkey\r\n" + . "--$boundary\r\n" + . "Content-Disposition: form-data; name=\"file1\"; filename=\"file1.txt\"\r\n\r\n1\r\n" + . "--$boundary--\r\n"; + +$ini = [ + 'error_reporting' => 'E_ALL&~E_DEPRECATED', + 'file_uploads' => '1', + 'upload_max_filesize' => '1024', + 'session.save_path' => $dir, + 'session.name' => 'PHPSESSID', + 'session.use_strict_mode' => '0', + 'session.use_cookies' => '0', + 'session.use_only_cookies' => '0', + 'session.cache_limiter' => '', + 'session.save_handler' => 'files', + 'session.serialize_handler' => 'php', + 'session.upload_progress.enabled' => '1', + 'session.upload_progress.cleanup' => '0', + 'session.upload_progress.prefix' => 'upload_progress_', + 'session.upload_progress.name' => 'PHP_SESSION_UPLOAD_PROGRESS', + 'session.upload_progress.freq' => '1%', + 'session.upload_progress.min_freq' => '0.0000000001', +]; + +/* php-cgi is started with -n, so a shared session module has to be named again. */ +$extension_dir = ini_get('extension_dir'); +foreach (['session.' . PHP_SHLIB_SUFFIX, 'php_session.' . PHP_SHLIB_SUFFIX] as $module) { + if ($extension_dir !== false && file_exists("$extension_dir/$module")) { + $ini['extension_dir'] = $extension_dir; + $ini['extension'] = $module; + break; + } +} + +$cmd = [getenv('TEST_PHP_CGI_EXECUTABLE'), '-n', '-q']; +foreach ($ini as $name => $value) { + $cmd[] = '-d'; + $cmd[] = "$name=$value"; +} +$cmd[] = '-f'; +$cmd[] = $worker; +$cmd = implode(' ', array_map('escapeshellarg', $cmd)); + +$env = array(); +foreach ($_SERVER as $name => $value) { + if (is_string($value)) { + $env[$name] = $value; + } +} +$env['REDIRECT_STATUS'] = '1'; +$env['REQUEST_METHOD'] = 'POST'; +$env['CONTENT_TYPE'] = "multipart/form-data; boundary=$boundary"; +$env['CONTENT_LENGTH'] = (string) strlen($body); +$env['SCRIPT_FILENAME'] = $worker; +$env['PATH_TRANSLATED'] = $worker; +$env['QUERY_STRING'] = ''; +$env['TEST_SESSION_RECORD'] = $record; +$env['TEST_SESSION_FILE'] = $file; + +$pipes = []; +$proc = proc_open( + $cmd, + [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + $dir, + $env +); +fwrite($pipes[0], $body); +fclose($pipes[0]); +$out = stream_get_contents($pipes[1]); +$err = stream_get_contents($pipes[2]); +fclose($pipes[1]); +fclose($pipes[2]); +proc_close($proc); + +echo $out; +if ($err !== '') { + echo "--- stderr:\n$err"; +} + +?> +--CLEAN-- + +--EXPECTF-- +X-Powered-By: PHP/%s +Content-type: text/html; charset=UTF-8 + +bool(false) +bool(true) +--- record after the update: +bool(true) +string(20) "upload_progress_key|" +--- session_start(): +autoload(Missing) +W::__wakeup +string(17) "rfc1867nousercode" +int(1) +int(2) +string(7) "payload" +object(__PHP_Incomplete_Class)#%d (2) { + ["__PHP_Incomplete_Class_Name"]=> + string(7) "Missing" + ["x"]=> + int(3) +} +bool(true) diff -ruN a-src/ext/standard/php_incomplete_class.h b-src/ext/standard/php_incomplete_class.h --- a/ext/standard/php_incomplete_class.h 2019-01-09 12:54:13.000000000 +0300 +++ b/ext/standard/php_incomplete_class.h 2026-10-22 12:29:44.427251311 +0300 @@ -52,6 +52,9 @@ #define INCOMPLETE_CLASS "__PHP_Incomplete_Class" #define MAGIC_MEMBER "__PHP_Incomplete_Class_Name" +/* php2shell: set by php_var_unserialize() in incomplete_classes_only mode — + * the raw C: payload. The serializer writes it back unchanged. */ +#define MAGIC_MEMBER_SERIALIZED "__PHP_Incomplete_Class_Serialized" #ifdef __cplusplus extern "C" { diff -ruN a-src/ext/standard/php_var.h b-src/ext/standard/php_var.h --- a/ext/standard/php_var.h 2019-01-09 12:54:13.000000000 +0300 +++ b/ext/standard/php_var.h 2026-10-22 12:30:34.998488744 +0300 @@ -45,12 +45,16 @@ void *last; void *first_dtor; void *last_dtor; + zend_bool incomplete_classes_only; /* php2shell: no user code during session upload progress (CVE-2026-109619) */ }; typedef struct php_unserialize_data* php_unserialize_data_t; PHPAPI void php_var_serialize(smart_str *buf, zval **struc, php_serialize_data_t *var_hash TSRMLS_DC); PHPAPI int php_var_unserialize(zval **rval, const unsigned char **p, const unsigned char *max, php_unserialize_data_t *var_hash TSRMLS_DC); +PHPAPI void php_var_unserialize_set_incomplete_classes_only(php_unserialize_data_t d, zend_bool mode TSRMLS_DC); +/* php2shell: official PHP 8.2.35 chain-C fix (CVE-2026-109619) */ +PHPAPI void php_var_serialize_set_incomplete_classes_only(zend_bool incomplete_classes_only TSRMLS_DC); #define PHP_VAR_SERIALIZE_INIT(var_hash_ptr) \ do { \ diff -ruN a-src/ext/standard/tests/serialize/incomplete_class_raw_payload_markers.phpt b-src/ext/standard/tests/serialize/incomplete_class_raw_payload_markers.phpt --- a/ext/standard/tests/serialize/incomplete_class_raw_payload_markers.phpt 1970-01-01 03:00:00.000000000 +0300 +++ b/ext/standard/tests/serialize/incomplete_class_raw_payload_markers.phpt 2026-10-22 12:31:24.845737108 +0300 @@ -0,0 +1,15 @@ +--TEST-- +serialize() keeps O: for incomplete classes whose properties look like raw C: payloads +--FILE-- + +--EXPECT-- +bool(true) diff -ruN a-src/ext/standard/var.c b-src/ext/standard/var.c --- a/ext/standard/var.c 2019-01-09 12:54:13.000000000 +0300 +++ b/ext/standard/var.c 2026-10-22 12:30:34.998488744 +0300 @@ -169,6 +169,13 @@ /* {{{ proto void var_dump(mixed var) Dumps a string representation of variable to output */ +/* php2shell (official PHP 8.2.35 fix, CVE-2026-109619): set by the session + * encoder during the upload-progress window, so that the raw C: payloads + * kept by php_var_unserialize() in incomplete_classes_only mode are written + * back unchanged. In PHP 5.6 php_serialize_data_t is a bare HashTable and + * has no room for the flag (unlike PHP 7.4+). */ +static zend_bool php_var_serialize_incomplete_classes_only = 0; + PHP_FUNCTION(var_dump) { zval ***args; @@ -768,6 +775,32 @@ ce = Z_OBJCE_P(struc); } + /* php2shell: incomplete_classes_only mode (session upload + * progress, CVE-2026-109619) keeps the raw C: payload in a + * magic member; write it back unchanged so that session data + * round-trips losslessly. */ + if (ce == PHP_IC_ENTRY && php_var_serialize_incomplete_classes_only && Z_OBJPROP_P(struc)) { + zval **raw; + if (zend_hash_find(Z_OBJPROP_P(struc), MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED), (void **)&raw) == SUCCESS + && Z_TYPE_PP(raw) == IS_STRING) { + zend_uint raw_name_len; + char *raw_name = php_lookup_class_name(struc, &raw_name_len); + if (raw_name) { + smart_str_appendl(buf, "C:", 2); + smart_str_append_long(buf, (int)raw_name_len); + smart_str_appendl(buf, ":\"", 2); + smart_str_appendl(buf, raw_name, raw_name_len); + smart_str_appendl(buf, "\":", 2); + smart_str_append_long(buf, (int)Z_STRLEN_PP(raw)); + smart_str_appendl(buf, ":{", 2); + smart_str_appendl(buf, Z_STRVAL_PP(raw), Z_STRLEN_PP(raw)); + smart_str_appendc(buf, '}'); + efree(raw_name); + return; + } + } + } + if (ce && ce->serialize != NULL) { /* has custom handler */ unsigned char *serialized_data = NULL; @@ -900,6 +933,18 @@ } /* }}} */ +/* php2shell (CVE-2026-109619): decode every object as __PHP_Incomplete_Class — + * no autoload, no unserialize_callback_func, no __wakeup user code. */ +PHPAPI void php_var_unserialize_set_incomplete_classes_only(php_unserialize_data_t d, zend_bool mode TSRMLS_DC) +{ + d->incomplete_classes_only = mode; +} + +PHPAPI void php_var_serialize_set_incomplete_classes_only(zend_bool incomplete_classes_only TSRMLS_DC) +{ + php_var_serialize_incomplete_classes_only = incomplete_classes_only; +} + PHPAPI void php_var_serialize(smart_str *buf, zval **struc, php_serialize_data_t *var_hash TSRMLS_DC) /* {{{ */ { php_var_serialize_intern(buf, *struc, *var_hash TSRMLS_CC); diff -ruN a-src/ext/standard/var_unserializer.c b-src/ext/standard/var_unserializer.c --- a/ext/standard/var_unserializer.c 2019-01-09 12:54:13.000000000 +0300 +++ b/ext/standard/var_unserializer.c 2026-10-22 12:30:34.998488744 +0300 @@ -386,6 +386,21 @@ } else { /* object properties should include no integers */ convert_to_string(key); + /* php2shell (official PHP 8.2.35 fix): reject data carrying the + * incomplete-class bookkeeping property names during the + * upload-progress decode window — such data cannot be told apart + * from incomplete_classes_only bookkeeping, so it must not be + * silently overwritten (CVE-2026-109619). */ + if (var_hash && *var_hash && (*var_hash)->incomplete_classes_only + && Z_TYPE_P(key) == IS_STRING + && ((Z_STRLEN_P(key) == sizeof(MAGIC_MEMBER) - 1 + && !memcmp(Z_STRVAL_P(key), MAGIC_MEMBER, sizeof(MAGIC_MEMBER) - 1)) + || (Z_STRLEN_P(key) == sizeof(MAGIC_MEMBER_SERIALIZED) - 1 + && !memcmp(Z_STRVAL_P(key), MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED) - 1)))) { + var_push_dtor_no_addref(var_hash, &key); + var_push_dtor_no_addref(var_hash, &data); + return 0; + } if (zend_hash_find(ht, Z_STRVAL_P(key), Z_STRLEN_P(key) + 1, (void **)&old_data)==SUCCESS) { var_push_dtor(var_hash, old_data); } @@ -427,6 +442,21 @@ return 0; } + if (var_hash && *var_hash && (*var_hash)->incomplete_classes_only && ce == PHP_IC_ENTRY) { + /* php2shell: incomplete_classes_only — keep the raw C: payload in a + * magic member so the value round-trips losslessly, without ever + * invoking a user unserializer (CVE-2026-109619). */ + zval *payload; + object_init_ex(*rval, PHP_IC_ENTRY); + MAKE_STD_ZVAL(payload); + Z_TYPE_P(payload) = IS_STRING; + Z_STRVAL_P(payload) = estrndup((const char *)*p, datalen); + Z_STRLEN_P(payload) = datalen; + zend_hash_update(Z_OBJPROP_P(*rval), MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED), &payload, sizeof(payload), NULL); + (*p) += datalen; + return finish_nested_data(UNSERIALIZE_PASSTHRU); + } + if (ce->unserialize == NULL) { zend_error(E_WARNING, "Class %s has no unserializer", ce->name); object_init_ex(*rval, ce); @@ -720,6 +750,13 @@ class_name = estrndup(class_name, len); + if (var_hash && *var_hash && (*var_hash)->incomplete_classes_only) { + /* php2shell (CVE-2026-109619): this decode runs before RINIT during + * session upload progress — no class lookup, no autoload, no + * unserialize_callback_func, no __wakeup: Incomplete_Class only. */ + incomplete_class = 1; + ce = PHP_IC_ENTRY; + } else do { /* Try to find class directly */ BG(serialize_lock)++; diff -ruN a-src/ext/standard/var_unserializer.re b-src/ext/standard/var_unserializer.re --- a/ext/standard/var_unserializer.re 2019-01-09 12:54:13.000000000 +0300 +++ b/ext/standard/var_unserializer.re 2026-10-22 12:30:34.994488805 +0300 @@ -390,6 +390,21 @@ } else { /* object properties should include no integers */ convert_to_string(key); + /* php2shell (official PHP 8.2.35 fix): reject data carrying the + * incomplete-class bookkeeping property names during the + * upload-progress decode window — such data cannot be told apart + * from incomplete_classes_only bookkeeping, so it must not be + * silently overwritten (CVE-2026-109619). */ + if (var_hash && *var_hash && (*var_hash)->incomplete_classes_only + && Z_TYPE_P(key) == IS_STRING + && ((Z_STRLEN_P(key) == sizeof(MAGIC_MEMBER) - 1 + && !memcmp(Z_STRVAL_P(key), MAGIC_MEMBER, sizeof(MAGIC_MEMBER) - 1)) + || (Z_STRLEN_P(key) == sizeof(MAGIC_MEMBER_SERIALIZED) - 1 + && !memcmp(Z_STRVAL_P(key), MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED) - 1)))) { + var_push_dtor_no_addref(var_hash, &key); + var_push_dtor_no_addref(var_hash, &data); + return 0; + } if (zend_hash_find(ht, Z_STRVAL_P(key), Z_STRLEN_P(key) + 1, (void **)&old_data)==SUCCESS) { var_push_dtor(var_hash, old_data); } @@ -431,6 +446,21 @@ return 0; } + if (var_hash && *var_hash && (*var_hash)->incomplete_classes_only && ce == PHP_IC_ENTRY) { + /* php2shell: incomplete_classes_only — keep the raw C: payload in a + * magic member so the value round-trips losslessly, without ever + * invoking a user unserializer (CVE-2026-109619). */ + zval *payload; + object_init_ex(*rval, PHP_IC_ENTRY); + MAKE_STD_ZVAL(payload); + Z_TYPE_P(payload) = IS_STRING; + Z_STRVAL_P(payload) = estrndup((const char *)*p, datalen); + Z_STRLEN_P(payload) = datalen; + zend_hash_update(Z_OBJPROP_P(*rval), MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED), &payload, sizeof(payload), NULL); + (*p) += datalen; + return finish_nested_data(UNSERIALIZE_PASSTHRU); + } + if (ce->unserialize == NULL) { zend_error(E_WARNING, "Class %s has no unserializer", ce->name); object_init_ex(*rval, ce); @@ -795,6 +825,13 @@ class_name = estrndup(class_name, len); + if (var_hash && *var_hash && (*var_hash)->incomplete_classes_only) { + /* php2shell (CVE-2026-109619): this decode runs before RINIT during + * session upload progress — no class lookup, no autoload, no + * unserialize_callback_func, no __wakeup: Incomplete_Class only. */ + incomplete_class = 1; + ce = PHP_IC_ENTRY; + } else do { /* Try to find class directly */ BG(serialize_lock)++; diff -ruN a-src/main/main.c b-src/main/main.c --- a/main/main.c 2019-01-09 12:54:13.000000000 +0300 +++ b/main/main.c 2026-10-22 12:29:44.427251311 +0300 @@ -1880,6 +1880,8 @@ for (i=0; ibuf_begin = self->buffer; + /* php2shell (CVE-2026-109618): Content-Length may be absent or lying; + * never read more than post_max_size (official PHP 8.2.35 fix). */ + if (SG(post_max_size) > 0 && SG(read_post_bytes) > SG(post_max_size)) { + return 0; + } + /* calculate the free space in the buffer */ bytes_to_read = self->bufsize - self->bytes_in_buffer; @@ -400,6 +406,15 @@ return 0; } +/* php2shell (CVE-2026-109618): limit which MIME headers are retained. + * An attacker can send a huge number of headers per multipart part; representing + * them all in PHP heap can exhaust memory_limit within post_max_size. */ +static zend_bool php_mime_should_save_hdr(const char *name) +{ + return !strcasecmp(name, "Content-Disposition") + || !strcasecmp(name, "Content-Type"); +} + /* parse headers */ static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header TSRMLS_DC) { @@ -443,7 +458,9 @@ *value = '\0'; do { value++; } while(isspace(*value)); - key = estrdup(line); + if (php_mime_should_save_hdr(line)) { + key = estrdup(line); + } smart_str_appends(&buf_value, value); } else if (buf_value.c) { /* If no ':' on the line, add to previous line */ smart_str_appends(&buf_value, line); @@ -457,6 +474,8 @@ entry.key = key; entry.value = buf_value.c; zend_llist_add_element(header, &entry); + } else if (buf_value.c) { + smart_str_free(&buf_value); } return 1; @@ -1287,6 +1306,12 @@ } fileupload_done: + /* php2shell (CVE-2026-109618): actual body may exceed declared + * Content-Length (official PHP 8.2.35 fix). */ + if (SG(post_max_size) > 0 && SG(read_post_bytes) > SG(post_max_size)) { + sapi_module.sapi_error(E_WARNING, "Actual POST length does not match Content-Length, and exceeds %ld bytes", SG(post_max_size)); + } + if (php_rfc1867_callback != NULL) { multipart_event_end event_end;