[packages/php/PHP_5_5] Rel 51. CVE-2026-109618 as soft memory limit (upstream delayed OOM) instead of _exit; harden session
arekm
arekm at pld-linux.org
Thu Oct 22 15:12:00 CEST 2026
commit d300f6158eaa600009b30c7cac6d895be7d1a2c1
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date: Thu Oct 22 15:09:36 2026 +0200
Rel 51. CVE-2026-109618 as soft memory limit (upstream delayed OOM) instead of _exit; harden session upload progress: safe serializers only, refuse back-references after kept C: payloads, keep the session on decode failure.
php-cve-2026-109618.patch | 1164 ++++++++++++++++++++++++++++++++++++++++++---
php.spec | 2 +-
2 files changed, 1090 insertions(+), 76 deletions(-)
---
diff --git a/php.spec b/php.spec
index 67946fd..756be51 100644
--- a/php.spec
+++ b/php.spec
@@ -140,7 +140,7 @@ ERROR: You need to select at least one Apache SAPI to build shared modules.
%undefine with_filter
%endif
-%define rel 50
+%define rel 51
%define orgname php
%define ver_suffix 55
%define php_suffix %{!?with_default_php:%{ver_suffix}}
diff --git a/php-cve-2026-109618.patch b/php-cve-2026-109618.patch
index a623ab0..36dbc16 100644
--- a/php-cve-2026-109618.patch
+++ b/php-cve-2026-109618.patch
@@ -7,7 +7,30 @@ Upstream: https://github.com/php/php-src/commit/fa146c84c1043662c7252496a50b9ff8
5.5 backport: https://github.com/shivammathur/php-src-backports/commit/ce192aa883e9fe2e41090c791bc50d80f147ebf2
PLD: session upload progress fix ported from upstream 3584a98bda0f (GHSA-6wmf-4rv5-p3rp) instead of the branch's "|| 1", which switched the feature off; with its tests
PLD: incomplete_classes_only is a BG() flag set by php_var_set_incomplete_classes_only() rather than a var_hash member: PHP_VAR_*_INIT() expand in extensions, so the var_hash layout is ABI
+PLD: memory_limit records the error and raises it at the next safe point (between opcodes, around zend_call_function(), after the main script and POST handling), as upstream 3584a98bda0f does, instead of _exit(1) at every memory_limit hit; only the hard limit (twice memory_limit, or memory_limit + 1G above 1G) exits. memory_limit cannot be set below the current usage.
+PLD: before RINIT the update runs only with the php, php_binary and php_serialize serializers (wddx and PECL ones may autoload or call __wakeup()), and a session it cannot decode that way (a back-reference after a kept C: payload, whose nested slots are not counted; reserved member names) is left untouched instead of being destroyed
---
+diff --git a/Zend/zend.c b/Zend/zend.c
+--- a/Zend/zend.c
++++ b/Zend/zend.c
+@@ -576,6 +576,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals TSRMLS
+ #endif
+ EG(saved_fpu_cw_ptr) = NULL;
+ EG(active) = 0;
++ memset(&EG(delayed_oom), 0, sizeof(EG(delayed_oom)));
+ }
+ /* }}} */
+
+@@ -1325,6 +1326,9 @@ ZEND_API int zend_execute_scripts(int type TSRMLS_DC, zval **retval, int file_co
+ if (EG(active_op_array)) {
+ EG(return_value_ptr_ptr) = retval ? retval : NULL;
+ zend_execute(EG(active_op_array) TSRMLS_CC);
++ if (ZEND_HAS_OOM()) {
++ zend_handle_delayed_oom(TSRMLS_C);
++ }
+ zend_exception_restore(TSRMLS_C);
+ if (EG(exception)) {
+ if (EG(user_exception_handler)) {
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -101,7 +124,7 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
static void zend_mm_safe_error(zend_mm_heap *heap,
const char *format,
size_t limit,
-@@ -1749,59 +1780,12 @@ static void zend_mm_safe_error(zend_mm_heap *heap,
+@@ -1749,58 +1780,64 @@ static void zend_mm_safe_error(zend_mm_heap *heap,
#endif
size_t size)
{
@@ -131,7 +154,9 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
- zend_error_noreturn(E_ERROR,
- format,
- limit,
--#if ZEND_DEBUG
++ (void) heap;
++ zend_mm_safe_error_ex(format, limit,
+ #if ZEND_DEBUG
- filename,
- lineno,
-#endif
@@ -142,9 +167,7 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
- fprintf(stderr,
- format,
- limit,
-+ (void) heap;
-+ zend_mm_safe_error_ex(format, limit,
- #if ZEND_DEBUG
+-#if ZEND_DEBUG
- filename,
- lineno,
+ filename, lineno,
@@ -155,17 +178,121 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
-/* See http://support.microsoft.com/kb/190351 */
-#ifdef PHP_WIN32
- fflush(stderr);
--#endif
++ size);
++}
++
++/* Twice the limit, or the limit plus 1G for limits above 1G, as upstream */
++static size_t zend_mm_hard_limit(size_t limit)
++{
++ size_t offset = (size_t) 1 << 30;
++
++ if (limit <= offset) {
++ return limit * 2;
++ }
++ return limit > (size_t) -1 - offset ? (size_t) -1 : limit + offset;
++}
++
++/* Bailing out of the allocator may leave whatever its caller was updating
++ * half done, and request shutdown would then use it. Let the allocation
++ * succeed and raise the error at the next safe point in
++ * zend_handle_delayed_oom() instead; only the hard limit stops the process
++ * right away. */
++static void zend_mm_limit_exceeded(zend_mm_heap *heap, size_t add_size, size_t size ZEND_FILE_LINE_DC)
++{
++ size_t hard_limit = zend_mm_hard_limit(heap->limit);
++ TSRMLS_FETCH();
++
++ if (add_size > hard_limit || heap->real_size > hard_limit - add_size) {
++ zend_mm_safe_error_ex("Hard memory limit of %lu bytes exhausted (tried to allocate %lu bytes)", (unsigned long) hard_limit, (unsigned long) size);
++ }
++ if (EG(delayed_oom).state == ZEND_OOM_STATE_NONE) {
++ EG(delayed_oom).state = ZEND_OOM_STATE_PENDING;
++ EG(delayed_oom).limit = heap->limit;
++ EG(delayed_oom).size = size;
++#if ZEND_DEBUG
++ EG(delayed_oom).filename = __zend_filename;
++ EG(delayed_oom).lineno = __zend_lineno;
+ #endif
- } zend_end_try();
- } else {
- heap->overflow = 2;
-- }
-- zend_bailout();
-+ size);
+ }
++}
++
++ZEND_API void zend_handle_delayed_oom(TSRMLS_D)
++{
++ zend_oom_info *info = &EG(delayed_oom);
++
++ if (info->state != ZEND_OOM_STATE_PENDING) {
++ return;
++ }
++ /* error reporting itself may now go over the limit, up to the hard one */
++ info->state = ZEND_OOM_STATE_HANDLED;
++#if ZEND_DEBUG
++ zend_error(E_ERROR, "Allowed memory size of %ld bytes exhausted at %s:%d (tried to allocate %lu bytes)", (long) info->limit, info->filename, info->lineno, (unsigned long) info->size);
++#else
++ zend_error(E_ERROR, "Allowed memory size of %ld bytes exhausted (tried to allocate %lu bytes)", (long) info->limit, (unsigned long) info->size);
++#endif
++ /* php_error_cb() bails out only once the modules are initialized */
+ zend_bailout();
}
- static zend_mm_free_block *zend_mm_search_large_block(zend_mm_heap *heap, size_t true_size)
-@@ -2472,7 +2456,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
+@@ -1965,18 +2002,17 @@ static void *_zend_mm_alloc_int(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_D
+ segment_size = heap->block_size;
+ }
+
+- if (segment_size < true_size ||
+- heap->real_size + segment_size > heap->limit) {
++ if (segment_size < true_size) {
++ goto out_of_memory;
++ }
++ if (heap->real_size + segment_size > heap->limit) {
+ /* Memory limit overflow */
+ #if ZEND_MM_CACHE
+ zend_mm_free_cache(heap);
+ #endif
+- HANDLE_UNBLOCK_INTERRUPTIONS();
+-#if ZEND_DEBUG
+- zend_mm_safe_error(heap, "Allowed memory size of %ld bytes exhausted at %s:%d (tried to allocate %lu bytes)", heap->limit, __zend_filename, __zend_lineno, size);
+-#else
+- zend_mm_safe_error(heap, "Allowed memory size of %ld bytes exhausted (tried to allocate %lu bytes)", heap->limit, size);
+-#endif
++ if (heap->real_size + segment_size > heap->limit) {
++ zend_mm_limit_exceeded(heap, segment_size, size ZEND_FILE_LINE_RELAY_CC);
++ }
+ }
+
+ segment = (zend_mm_segment *) ZEND_MM_STORAGE_ALLOC(segment_size);
+@@ -2276,21 +2312,16 @@ realloc_segment:
+ }
+
+ segment_copy = (zend_mm_segment *) ((char *)mm_block - ZEND_MM_ALIGNED_SEGMENT_SIZE);
+- if (segment_size < true_size ||
+- heap->real_size + segment_size - segment_copy->size > heap->limit) {
++ if (segment_size < true_size) {
+ if (ZEND_MM_IS_FREE_BLOCK(next_block)) {
+ zend_mm_add_to_free_list(heap, (zend_mm_free_block *) next_block);
+ }
+-#if ZEND_MM_CACHE
+- zend_mm_free_cache(heap);
+-#endif
+- HANDLE_UNBLOCK_INTERRUPTIONS();
+-#if ZEND_DEBUG
+- zend_mm_safe_error(heap, "Allowed memory size of %ld bytes exhausted at %s:%d (tried to allocate %ld bytes)", heap->limit, __zend_filename, __zend_lineno, size);
+-#else
+- zend_mm_safe_error(heap, "Allowed memory size of %ld bytes exhausted (tried to allocate %ld bytes)", heap->limit, size);
+-#endif
+- return NULL;
++ goto out_of_memory;
++ }
++ if (heap->real_size + segment_size - segment_copy->size > heap->limit) {
++ /* next_block is out of the free list here, so the cache cannot be
++ * merged back to make room as in _zend_mm_alloc_int() */
++ zend_mm_limit_exceeded(heap, segment_size > segment_copy->size ? segment_size - segment_copy->size : 0, size ZEND_FILE_LINE_RELAY_CC);
+ }
+
+ segment = ZEND_MM_STORAGE_REALLOC(segment_copy, segment_size);
+@@ -2472,7 +2503,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
"rm"(offset));
if (UNEXPECTED(overflow)) {
@@ -174,7 +301,7 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
return 0;
}
return res;
-@@ -2501,7 +2485,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
+@@ -2501,7 +2532,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
#undef LP_SUFF
if (UNEXPECTED(overflow)) {
@@ -183,7 +310,7 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
return 0;
}
return res;
-@@ -2522,7 +2506,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
+@@ -2522,7 +2553,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
"1"(0));
if (UNEXPECTED(overflow)) {
@@ -192,7 +319,7 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
return 0;
}
return res;
-@@ -2542,7 +2526,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
+@@ -2542,7 +2573,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
"r"(offset));
if (UNEXPECTED(overflow)) {
@@ -201,7 +328,7 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
return 0;
}
return res;
-@@ -2555,7 +2539,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
+@@ -2555,7 +2586,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
zend_ulong64 res = (zend_ulong64)nmemb * (zend_ulong64)size + (zend_ulong64)offset;
if (UNEXPECTED(res > (zend_ulong64)0xFFFFFFFFL)) {
@@ -210,7 +337,7 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
return 0;
}
return (size_t) res;
-@@ -2570,7 +2554,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
+@@ -2570,7 +2601,7 @@ static inline size_t safe_address(size_t nmemb, size_t size, size_t offset)
double _delta = (double)res - _d;
if (UNEXPECTED((_d + _delta ) != _d)) {
@@ -219,6 +346,31 @@ diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
return 0;
}
return res;
+@@ -2684,9 +2715,23 @@ ZEND_API char *zend_strndup(const char *s, uint length)
+
+ ZEND_API int zend_set_memory_limit(size_t memory_limit)
+ {
++ zend_mm_heap *heap;
+ TSRMLS_FETCH();
+
+- AG(mm_heap)->limit = (memory_limit >= AG(mm_heap)->block_size) ? memory_limit : AG(mm_heap)->block_size;
++ heap = AG(mm_heap);
++ if (memory_limit < heap->block_size) {
++ memory_limit = heap->block_size;
++ }
++ /* the hard limit is derived from this one and must stay above the memory in use */
++ if (memory_limit < heap->real_size) {
++#if ZEND_MM_CACHE
++ zend_mm_free_cache(heap);
++#endif
++ if (memory_limit < heap->real_size) {
++ return FAILURE;
++ }
++ }
++ heap->limit = memory_limit;
+
+ return SUCCESS;
+ }
diff --git a/Zend/zend_alloc.h b/Zend/zend_alloc.h
--- a/Zend/zend_alloc.h
+++ b/Zend/zend_alloc.h
@@ -253,11 +405,30 @@ diff --git a/Zend/zend_alloc.h b/Zend/zend_alloc.h
}
-@@ -159,6 +165,8 @@ void zend_debug_alloc_output(char *format, ...);
+@@ -159,6 +165,27 @@ void zend_debug_alloc_output(char *format, ...);
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, ...);
++
++typedef enum _zend_oom_state {
++ ZEND_OOM_STATE_NONE = 0,
++ ZEND_OOM_STATE_PENDING,
++ ZEND_OOM_STATE_HANDLED
++} zend_oom_state;
++
++/* memory_limit exceeded, raised later by zend_handle_delayed_oom() */
++typedef struct _zend_oom_info {
++ zend_oom_state state;
++ size_t limit;
++ size_t size;
++ const char *filename;
++ uint lineno;
++} zend_oom_info;
++
++#define ZEND_HAS_OOM() UNEXPECTED(EG(delayed_oom).state != ZEND_OOM_STATE_NONE)
++
++ZEND_API void zend_handle_delayed_oom(TSRMLS_D);
+
END_EXTERN_C()
@@ -283,6 +454,42 @@ diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
}
/* }}} */
+@@ -744,6 +755,10 @@ int zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache TS
+ return FAILURE; /* executor is already inactive */
+ }
+
++ if (ZEND_HAS_OOM()) {
++ zend_handle_delayed_oom(TSRMLS_C);
++ }
++
+ if (EG(exception)) {
+ return FAILURE; /* we would result in an instable executor otherwise */
+ }
+@@ -998,6 +1013,11 @@ int zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache TS
+ EG(This) = current_this;
+ EG(current_execute_data) = EX(prev_execute_data);
+
++ /* internal functions do not reach the check in the executor loop */
++ if (ZEND_HAS_OOM()) {
++ zend_handle_delayed_oom(TSRMLS_C);
++ }
++
+ if (EG(exception)) {
+ zend_throw_exception_internal(NULL TSRMLS_CC);
+ }
+diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h
+--- a/Zend/zend_globals.h
++++ b/Zend/zend_globals.h
+@@ -258,6 +258,9 @@ struct _zend_executor_globals {
+ #endif
+
+ void *reserved[ZEND_MAX_RESERVED_RESOURCES];
++
++ /* last, so that the offsets used by extensions do not change */
++ zend_oom_info delayed_oom;
+ };
+
+ struct _zend_ini_scanner_globals {
diff --git a/Zend/zend_llist.c b/Zend/zend_llist.c
--- a/Zend/zend_llist.c
+++ b/Zend/zend_llist.c
@@ -401,19 +608,149 @@ diff --git a/Zend/zend_strtod.c b/Zend/zend_strtod.c
}
rv->k = k;
rv->maxwds = x;
+diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
+--- a/Zend/zend_vm_execute.h
++++ b/Zend/zend_vm_execute.h
+@@ -359,6 +359,10 @@ zend_vm_enter:
+ zend_timeout(0);
+ }
+ #endif
++ /* between two opcodes is a safe point for the memory limit error */
++ if (ZEND_HAS_OOM()) {
++ zend_handle_delayed_oom(TSRMLS_C);
++ }
+
+ if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) {
+ switch (ret) {
+diff --git a/Zend/zend_vm_execute.skl b/Zend/zend_vm_execute.skl
+--- a/Zend/zend_vm_execute.skl
++++ b/Zend/zend_vm_execute.skl
+@@ -27,6 +27,10 @@ zend_vm_enter:
+ zend_timeout(0);
+ }
+ #endif
++ /* between two opcodes is a safe point for the memory limit error */
++ if (ZEND_HAS_OOM()) {
++ zend_handle_delayed_oom(TSRMLS_C);
++ }
+
+ {%ZEND_VM_DISPATCH%} {
+ {%INTERNAL_EXECUTOR%}
+diff --git a/ext/session/php_session.h b/ext/session/php_session.h
+--- a/ext/session/php_session.h
++++ b/ext/session/php_session.h
+@@ -180,6 +180,10 @@ typedef struct _php_ps_globals {
+ double rfc1867_min_freq; /* session.upload_progress.min_freq */
+
+ zend_bool use_strict_mode; /* whether or not PHP accepts unknown session ids */
++
++ /* last, so that the offsets extensions use do not move */
++ zend_bool in_rfc1867_update; /* decoding before RINIT, see php_session_rfc1867_open() */
++ zend_bool rfc1867_decode_failed;
+ } php_ps_globals;
+
+ typedef php_ps_globals zend_ps_globals;
diff --git a/ext/session/session.c b/ext/session/session.c
--- a/ext/session/session.c
+++ b/ext/session/session.c
-@@ -2603,23 +2603,34 @@ static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, i
- progress->next_update = Z_LVAL_P(progress->post_bytes_processed) + progress->update_step;
+@@ -223,6 +223,11 @@ static int php_session_decode(const char *val, int vallen TSRMLS_DC) /* {{{ */
+ return FAILURE;
}
+ if (PS(serializer)->decode(val, vallen TSRMLS_CC) == FAILURE) {
++ if (PS(in_rfc1867_update)) {
++ /* not decodable without user code, which is not an invalid session */
++ PS(rfc1867_decode_failed) = 1;
++ return FAILURE;
++ }
+ php_session_destroy(TSRMLS_C);
+ php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to decode session object. Session has been destroyed");
+ return FAILURE;
+@@ -865,6 +870,11 @@ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */
+ ALLOC_INIT_ZVAL(session_vars);
+ if (php_var_unserialize(&session_vars, (const unsigned char**) &val, endptr, &var_hash TSRMLS_CC)) {
+ var_push_dtor(&var_hash, &session_vars);
++ } else if (PS(in_rfc1867_update)) {
++ /* a partly decoded session must not be written back, see php_session_rfc1867_open() */
++ var_push_dtor_no_addref(&var_hash, &session_vars);
++ PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
++ return FAILURE;
+ }
+
+ PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
+@@ -1646,6 +1656,18 @@ static void php_session_flush(TSRMLS_D) /* {{{ */
+ }
+ /* }}} */
-+ /* Runs before RINIT: no user code may be invoked, see php_var_set_incomplete_classes_only() */
++/* as in 5.6: close the session without writing it */
++static void php_session_abort(TSRMLS_D) /* {{{ */
++{
++ if (PS(session_status) == php_session_active) {
++ PS(session_status) = php_session_none;
++ if (PS(mod_data) || PS(mod_user_implemented)) {
++ PS(mod)->s_close(&PS(mod_data) TSRMLS_CC);
++ }
++ }
++}
++/* }}} */
++
+ PHPAPI void session_adapt_url(const char *url, size_t urllen, char **new, size_t *newlen TSRMLS_DC) /* {{{ */
+ {
+ if (PS(apply_trans_sid) && (PS(session_status) == php_session_active)) {
+@@ -2330,6 +2352,9 @@ static const zend_function_entry php_session_class_functions[] = {
+ static int php_rinit_session(zend_bool auto_start TSRMLS_DC) /* {{{ */
+ {
+ php_rinit_session_globals(TSRMLS_C);
++ /* the upload progress update may have bailed out in the previous request */
++ PS(in_rfc1867_update) = 0;
++ PS(rfc1867_decode_failed) = 0;
+
+ if (PS(mod) == NULL) {
+ char *value;
+@@ -2582,6 +2607,37 @@ static zend_bool php_check_cancel_upload(php_session_rfc1867_progress *progress
+ return Z_TYPE_PP(cancel_upload) == IS_BOOL && Z_LVAL_PP(cancel_upload);
+ } /* }}} */
+
++/* Starts the session for the upload progress handler, which runs before RINIT,
++ * when no user code may be invoked. Only the serializers of this extension
++ * decode without resolving classes (see php_var_set_incomplete_classes_only());
++ * wddx and PECL ones may autoload or call __wakeup(). On success, the caller
++ * flushes the session and calls php_var_set_incomplete_classes_only(0). */
++static int php_session_rfc1867_open(TSRMLS_D) /* {{{ */
++{
++ const char *name = PS(serializer) ? PS(serializer)->name : NULL;
++
++ if (!name || (strcmp(name, "php") && strcmp(name, "php_binary") && strcmp(name, "php_serialize"))) {
++ return FAILURE;
++ }
++
+ php_var_set_incomplete_classes_only(1 TSRMLS_CC);
- php_session_initialize(TSRMLS_C);
-- PS(session_status) = php_session_active;
++ PS(rfc1867_decode_failed) = 0;
++ PS(in_rfc1867_update) = 1;
++ php_session_initialize(TSRMLS_C);
++ PS(in_rfc1867_update) = 0;
+ if (PS(session_status) != php_session_active) {
+ php_var_set_incomplete_classes_only(0 TSRMLS_CC);
++ return FAILURE;
++ }
++ if (PS(rfc1867_decode_failed)) {
++ /* leave the stored session as it is */
++ php_session_abort(TSRMLS_C);
++ php_var_set_incomplete_classes_only(0 TSRMLS_CC);
++ return FAILURE;
++ }
++ return SUCCESS;
++} /* }}} */
++
+ static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, int force_update TSRMLS_DC) /* {{{ */
+ {
+ if (!force_update) {
+@@ -2603,23 +2659,27 @@ static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, i
+ progress->next_update = Z_LVAL_P(progress->post_bytes_processed) + progress->update_step;
+ }
+
+- php_session_initialize(TSRMLS_C);
+- PS(session_status) = php_session_active;
++ if (php_session_rfc1867_open(TSRMLS_C) == FAILURE) {
+ return;
+ }
IF_SESSION_VARS() {
@@ -426,11 +763,9 @@ diff --git a/ext/session/session.c b/ext/session/session.c
static void php_session_rfc1867_cleanup(php_session_rfc1867_progress *progress TSRMLS_DC) /* {{{ */
{
-+ php_var_set_incomplete_classes_only(1 TSRMLS_CC);
- php_session_initialize(TSRMLS_C);
+- php_session_initialize(TSRMLS_C);
- PS(session_status) = php_session_active;
-+ if (PS(session_status) != php_session_active) {
-+ php_var_set_incomplete_classes_only(0 TSRMLS_CC);
++ if (php_session_rfc1867_open(TSRMLS_C) == FAILURE) {
+ return;
+ }
IF_SESSION_VARS() {
@@ -441,7 +776,7 @@ diff --git a/ext/session/session.c b/ext/session/session.c
} /* }}} */
static int php_session_rfc1867_callback(unsigned int event, void *event_data, void **extra TSRMLS_DC) /* {{{ */
-@@ -2633,12 +2644,20 @@ static int php_session_rfc1867_callback(unsigned int event, void *event_data, vo
+@@ -2633,12 +2693,20 @@ static int php_session_rfc1867_callback(unsigned int event, void *event_data, vo
if (!PS(rfc1867_enabled)) {
return retval;
}
@@ -456,27 +791,369 @@ diff --git a/ext/session/session.c b/ext/session/session.c
case MULTIPART_EVENT_START: {
multipart_event_start *data = (multipart_event_start *) event_data;
+
-+ /* This runs before RINIT, so we need to clear these early */
-+ php_rinit_session_globals(TSRMLS_C);
++ /* This runs before RINIT, so we need to clear these early */
++ php_rinit_session_globals(TSRMLS_C);
++
+ progress = ecalloc(1, sizeof(php_session_rfc1867_progress));
+ progress->content_length = data->content_length;
+ progress->sname_len = strlen(PS(session_name));
+@@ -2798,6 +2866,9 @@ static int php_session_rfc1867_callback(unsigned int event, void *event_data, vo
+ efree(progress);
+ progress = NULL;
+ PS(rfc1867_progress) = NULL;
++
++ /* $_SESSION was populated in incomplete_classes_only mode */
++ zend_delete_global_variable("_SESSION", sizeof("_SESSION") - 1 TSRMLS_CC);
+ }
+ break;
+ }
+diff --git a/ext/session/tests/rfc1867_custom_backref.phpt b/ext/session/tests/rfc1867_custom_backref.phpt
+new file mode 100644
+--- /dev/null
++++ b/ext/session/tests/rfc1867_custom_backref.phpt
+@@ -0,0 +1,166 @@
++--TEST--
++session rfc1867: a back-reference after a Serializable payload stops the upload progress update without touching the session
++--SKIPIF--
++<?php
++include('skipif.inc');
++if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
++ die('skip CGI not available');
++}
++?>
++--FILE--
++<?php
++
++$dir = __DIR__ . '/rfc1867_custom_backref';
++ at mkdir($dir);
++$sid = 'rfc1867custombackref';
++$file = "$dir/sess_$sid";
++$worker = "$dir/worker.php";
++
++/* The C: payload is kept verbatim while the handler decodes the record, so the
++ * slots that Cust::unserialize() registers when the class is loaded are not
++ * counted, and r: after it may point elsewhere. The handler leaves such a
++ * record alone instead of writing back one with shifted references. */
++$record = 'c|C:4:"Cust":1:{a}'
++ . 'o|O:8:"stdClass":1:{s:1:"p";i:1;}'
++ . 'r|r:2;';
++file_put_contents($file, $record);
++
++file_put_contents($worker, <<<'PHP'
++<?php
++$record = getenv('TEST_SESSION_RECORD');
++$file = getenv('TEST_SESSION_FILE');
++
++class Cust implements Serializable {
++ public $x;
++ function serialize() {
++ return $this->x;
++ }
++ function unserialize($d) {
++ $this->x = $d;
++ }
++}
++
++var_dump(array_key_exists('_SESSION', $GLOBALS));
++echo "--- record after the update:\n";
++var_dump(file_get_contents($file) === $record);
++
++echo "--- session_start():\n";
++session_start();
++var_dump($_SESSION['c']->x, $_SESSION['r'] === $_SESSION['o']);
++var_dump(isset($_SESSION['upload_progress_key']));
++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 = array(
++ 'error_reporting' => 'E_ALL&~E_DEPRECATED',
++ /* the update runs during request startup; On by default since PHP 8.0 */
++ 'display_startup_errors' => '1',
++ '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.000000001',
++);
++
++/* php-cgi is started with -n, so shared modules have to be named again;
++ * session requires spl. */
++$extension_dir = ini_get('extension_dir');
++$modules = array();
++foreach (array('spl', 'session') as $ext) {
++ foreach (array("$ext.", "php_$ext.") as $prefix) {
++ if ($extension_dir !== false && file_exists("$extension_dir/$prefix" . PHP_SHLIB_SUFFIX)) {
++ $ini['extension_dir'] = $extension_dir;
++ $modules[] = $prefix . PHP_SHLIB_SUFFIX;
++ break;
++ }
++ }
++}
++
++$cmd = escapeshellarg(getenv('TEST_PHP_CGI_EXECUTABLE')) . ' -n -q';
++foreach ($ini as $name => $value) {
++ $cmd .= ' -d ' . escapeshellarg("$name=$value");
++}
++foreach ($modules as $module) {
++ $cmd .= ' -d ' . escapeshellarg("extension=$module");
++}
++$cmd .= ' -f ' . escapeshellarg($worker);
++
++$env = array();
++foreach (array('PATH', 'LD_LIBRARY_PATH', 'TMPDIR') as $name) {
++ if (getenv($name) !== false) {
++ $env[$name] = getenv($name);
++ }
++}
++$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 = array();
++$proc = proc_open(
++ $cmd,
++ array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('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--
++<?php
++$dir = __DIR__ . '/rfc1867_custom_backref';
++ at unlink("$dir/worker.php");
++foreach (glob("$dir/sess_*") as $f) {
++ unlink($f);
++}
++ at rmdir($dir);
++?>
++--EXPECTF--
++X-Powered-By: PHP/%s
++Content-type: text/html%A
++
++bool(false)
++--- record after the update:
++bool(true)
++--- session_start():
++string(1) "a"
++bool(true)
++bool(false)
+diff --git a/ext/session/tests/rfc1867_custom_backref_serialize.phpt b/ext/session/tests/rfc1867_custom_backref_serialize.phpt
+new file mode 100644
+--- /dev/null
++++ b/ext/session/tests/rfc1867_custom_backref_serialize.phpt
+@@ -0,0 +1,166 @@
++--TEST--
++session rfc1867: php_serialize handler: a back-reference after a Serializable payload stops the upload progress update without touching the session
++--SKIPIF--
++<?php
++include('skipif.inc');
++if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
++ die('skip CGI not available');
++}
++?>
++--FILE--
++<?php
++
++$dir = __DIR__ . '/rfc1867_custom_backref_serialize';
++ at mkdir($dir);
++$sid = 'rfc1867custombackrefser';
++$file = "$dir/sess_$sid";
++$worker = "$dir/worker.php";
++
++/* The C: payload is kept verbatim while the handler decodes the record, so the
++ * slots that Cust::unserialize() registers when the class is loaded are not
++ * counted, and r: after it may point elsewhere. The handler leaves such a
++ * record alone instead of writing back one with shifted references. */
++$record = 'a:3:{s:1:"c";C:4:"Cust":1:{a}'
++ . 's:1:"o";O:8:"stdClass":1:{s:1:"p";i:1;}'
++ . 's:1:"r";r:3;}';
++file_put_contents($file, $record);
++
++file_put_contents($worker, <<<'PHP'
++<?php
++$record = getenv('TEST_SESSION_RECORD');
++$file = getenv('TEST_SESSION_FILE');
++
++class Cust implements Serializable {
++ public $x;
++ function serialize() {
++ return $this->x;
++ }
++ function unserialize($d) {
++ $this->x = $d;
++ }
++}
++
++var_dump(array_key_exists('_SESSION', $GLOBALS));
++echo "--- record after the update:\n";
++var_dump(file_get_contents($file) === $record);
++
++echo "--- session_start():\n";
++session_start();
++var_dump($_SESSION['c']->x, $_SESSION['r'] === $_SESSION['o']);
++var_dump(isset($_SESSION['upload_progress_key']));
++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 = array(
++ 'error_reporting' => 'E_ALL&~E_DEPRECATED',
++ /* the update runs during request startup; On by default since PHP 8.0 */
++ 'display_startup_errors' => '1',
++ '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_serialize',
++ '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.000000001',
++);
++
++/* php-cgi is started with -n, so shared modules have to be named again;
++ * session requires spl. */
++$extension_dir = ini_get('extension_dir');
++$modules = array();
++foreach (array('spl', 'session') as $ext) {
++ foreach (array("$ext.", "php_$ext.") as $prefix) {
++ if ($extension_dir !== false && file_exists("$extension_dir/$prefix" . PHP_SHLIB_SUFFIX)) {
++ $ini['extension_dir'] = $extension_dir;
++ $modules[] = $prefix . PHP_SHLIB_SUFFIX;
++ break;
++ }
++ }
++}
++
++$cmd = escapeshellarg(getenv('TEST_PHP_CGI_EXECUTABLE')) . ' -n -q';
++foreach ($ini as $name => $value) {
++ $cmd .= ' -d ' . escapeshellarg("$name=$value");
++}
++foreach ($modules as $module) {
++ $cmd .= ' -d ' . escapeshellarg("extension=$module");
++}
++$cmd .= ' -f ' . escapeshellarg($worker);
++
++$env = array();
++foreach (array('PATH', 'LD_LIBRARY_PATH', 'TMPDIR') as $name) {
++ if (getenv($name) !== false) {
++ $env[$name] = getenv($name);
++ }
++}
++$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 = array();
++$proc = proc_open(
++ $cmd,
++ array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('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);
+
- progress = ecalloc(1, sizeof(php_session_rfc1867_progress));
- progress->content_length = data->content_length;
- progress->sname_len = strlen(PS(session_name));
-@@ -2798,6 +2817,9 @@ static int php_session_rfc1867_callback(unsigned int event, void *event_data, vo
- efree(progress);
- progress = NULL;
- PS(rfc1867_progress) = NULL;
++echo $out;
++if ($err !== '') {
++ echo "--- stderr:\n$err";
++}
+
-+ /* $_SESSION was populated in incomplete_classes_only mode */
-+ zend_delete_global_variable("_SESSION", sizeof("_SESSION") - 1 TSRMLS_CC);
- }
- break;
- }
++?>
++--CLEAN--
++<?php
++$dir = __DIR__ . '/rfc1867_custom_backref_serialize';
++ at unlink("$dir/worker.php");
++foreach (glob("$dir/sess_*") as $f) {
++ unlink($f);
++}
++ at rmdir($dir);
++?>
++--EXPECTF--
++X-Powered-By: PHP/%s
++Content-type: text/html%A
++
++bool(false)
++--- record after the update:
++bool(true)
++--- session_start():
++string(1) "a"
++bool(true)
++bool(false)
diff --git a/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt b/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt
new file mode 100644
--- /dev/null
+++ b/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt
-@@ -0,0 +1,147 @@
+@@ -0,0 +1,146 @@
+--TEST--
+session rfc1867: the upload progress update rejects session data whose objects carry the incomplete class bookkeeping property names
+--SKIPIF--
@@ -597,14 +1274,13 @@ new file mode 100644
+ 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. */
-+ $kept = file_exists($file) && strncmp(file_get_contents($file), $record, strlen($record)) === 0;
-+ printf("%-16s warnings: %d, decode refused: %-5s record kept: %s\n", $label,
++ /* A refused decode leaves the record alone, without a warning: the session
++ * is not invalid, the script's own session_start() decodes it normally. */
++ $after = file_exists($file) ? file_get_contents($file) : '';
++ printf("%-16s warnings: %d, progress written: %-5s record kept: %s\n", $label,
+ substr_count($out, 'Warning:'),
-+ var_export(strpos($out, 'Session has been destroyed') !== false, true),
-+ var_export($kept, true));
++ var_export(strpos($after, 'upload_progress_key|') !== false, true),
++ var_export(strncmp($after, $record, strlen($record)) === 0, true));
+}
+
+?>
@@ -618,12 +1294,12 @@ new file mode 100644
+ at rmdir($dir);
+?>
+--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
++control warnings: 0, progress written: true record kept: true
++Class_Name warnings: 0, progress written: false record kept: true
++Class_Serialized warnings: 0, progress written: false record kept: true
++nested warnings: 0, progress written: false record kept: true
++mangled warnings: 0, progress written: true record kept: true
++corrupt warnings: 0, progress written: false record kept: true
diff --git a/ext/session/tests/rfc1867_no_user_code.phpt b/ext/session/tests/rfc1867_no_user_code.phpt
new file mode 100644
--- /dev/null
@@ -832,18 +1508,181 @@ new file mode 100644
+ int(3)
+}
+bool(true)
+diff --git a/ext/session/tests/rfc1867_serializer_wddx.phpt b/ext/session/tests/rfc1867_serializer_wddx.phpt
+new file mode 100644
+--- /dev/null
++++ b/ext/session/tests/rfc1867_serializer_wddx.phpt
+@@ -0,0 +1,157 @@
++--TEST--
++session rfc1867: with the wddx serializer, which may call __wakeup(), the upload progress update leaves the session alone
++--SKIPIF--
++<?php
++include('skipif.inc');
++/* wddx registers its session serializer only when built with a static session */
++if (@ini_set('session.serialize_handler', 'wddx') === false) {
++ die('skip wddx session serializer not available');
++}
++if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
++ die('skip CGI not available');
++}
++?>
++--FILE--
++<?php
++
++$dir = __DIR__ . '/rfc1867_serializer_wddx';
++ at mkdir($dir);
++$sid = 'rfc1867serializerwddx';
++$file = "$dir/sess_$sid";
++$worker = "$dir/worker.php";
++
++/* Only the serializers of the session extension decode without resolving
++ * classes before RINIT; wddx looks classes up and calls __wakeup(). */
++$record = "<wddxPacket version='1.0'><header/><data><struct><var name='o'><struct>"
++ . "<var name='php_class_name'><string>stdClass</string></var>"
++ . "<var name='p'><number>1</number></var></struct></var></struct></data></wddxPacket>";
++file_put_contents($file, $record);
++
++file_put_contents($worker, <<<'PHP'
++<?php
++$record = getenv('TEST_SESSION_RECORD');
++$file = getenv('TEST_SESSION_FILE');
++
++var_dump(array_key_exists('_SESSION', $GLOBALS));
++echo "--- record after the update:\n";
++var_dump(file_get_contents($file) === $record);
++
++echo "--- session_start():\n";
++session_start();
++var_dump($_SESSION['o']->p);
++var_dump(isset($_SESSION['upload_progress_key']));
++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 = array(
++ 'error_reporting' => 'E_ALL&~E_DEPRECATED',
++ /* the update runs during request startup; On by default since PHP 8.0 */
++ 'display_startup_errors' => '1',
++ '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' => 'wddx',
++ '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.000000001',
++);
++
++/* php-cgi is started with -n, so shared modules have to be named again;
++ * session requires spl. */
++$extension_dir = ini_get('extension_dir');
++$modules = array();
++foreach (array('spl', 'session', 'xml', 'wddx') as $ext) {
++ foreach (array("$ext.", "php_$ext.") as $prefix) {
++ if ($extension_dir !== false && file_exists("$extension_dir/$prefix" . PHP_SHLIB_SUFFIX)) {
++ $ini['extension_dir'] = $extension_dir;
++ $modules[] = $prefix . PHP_SHLIB_SUFFIX;
++ break;
++ }
++ }
++}
++
++$cmd = escapeshellarg(getenv('TEST_PHP_CGI_EXECUTABLE')) . ' -n -q';
++foreach ($ini as $name => $value) {
++ $cmd .= ' -d ' . escapeshellarg("$name=$value");
++}
++foreach ($modules as $module) {
++ $cmd .= ' -d ' . escapeshellarg("extension=$module");
++}
++$cmd .= ' -f ' . escapeshellarg($worker);
++
++$env = array();
++foreach (array('PATH', 'LD_LIBRARY_PATH', 'TMPDIR') as $name) {
++ if (getenv($name) !== false) {
++ $env[$name] = getenv($name);
++ }
++}
++$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 = array();
++$proc = proc_open(
++ $cmd,
++ array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('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--
++<?php
++$dir = __DIR__ . '/rfc1867_serializer_wddx';
++ at unlink("$dir/worker.php");
++foreach (glob("$dir/sess_*") as $f) {
++ unlink($f);
++}
++ at rmdir($dir);
++?>
++--EXPECTF--
++X-Powered-By: PHP/%s
++Content-type: text/html%A
++
++bool(false)
++--- record after the update:
++bool(true)
++--- session_start():
++int(1)
++bool(false)
diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c
--- a/ext/standard/basic_functions.c
+++ b/ext/standard/basic_functions.c
-@@ -3744,6 +3744,7 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */
+@@ -3744,6 +3744,8 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */
BG(serialize_lock) = 0;
memset(&BG(serialize), 0, sizeof(BG(serialize)));
memset(&BG(unserialize), 0, sizeof(BG(unserialize)));
+ BG(serialize_incomplete_classes_only) = 0;
++ BG(serialize_incomplete_custom_seen) = 0;
BG(strtok_string) = NULL;
BG(strtok_zval) = NULL;
-@@ -3784,6 +3785,19 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */
+@@ -3784,6 +3786,20 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */
}
/* }}} */
@@ -857,6 +1696,7 @@ diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c
+ memset(&BG(serialize), 0, sizeof(BG(serialize)));
+ memset(&BG(unserialize), 0, sizeof(BG(unserialize)));
+ BG(serialize_incomplete_classes_only) = 0;
++ BG(serialize_incomplete_custom_seen) = 0;
+ php_url_scanner_early_rinit(TSRMLS_C);
+}
+
@@ -866,17 +1706,18 @@ diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c
diff --git a/ext/standard/basic_functions.h b/ext/standard/basic_functions.h
--- a/ext/standard/basic_functions.h
+++ b/ext/standard/basic_functions.h
-@@ -230,6 +230,9 @@ typedef struct _php_basic_globals {
+@@ -230,6 +230,10 @@ typedef struct _php_basic_globals {
#endif
int umask;
+
+ /* var.c, var_unserializer.re; last, so that the offsets extensions use do not move */
+ zend_bool serialize_incomplete_classes_only;
++ zend_bool serialize_incomplete_custom_seen;
} php_basic_globals;
#ifdef ZTS
-@@ -257,6 +260,7 @@ typedef struct _php_shutdown_function_entry {
+@@ -257,6 +261,7 @@ typedef struct _php_shutdown_function_entry {
int arg_count;
} php_shutdown_function_entry;
@@ -1004,7 +1845,7 @@ diff --git a/ext/standard/var.c b/ext/standard/var.c
if (ce && ce->serialize != NULL) {
/* has custom handler */
unsigned char *serialized_data = NULL;
-@@ -910,6 +946,16 @@ PHPAPI void php_var_serialize(smart_str *buf, zval **struc, php_serialize_data_t
+@@ -910,6 +946,17 @@ PHPAPI void php_var_serialize(smart_str *buf, zval **struc, php_serialize_data_t
}
/* }}} */
@@ -1015,6 +1856,7 @@ diff --git a/ext/standard/var.c b/ext/standard/var.c
+PHPAPI void php_var_set_incomplete_classes_only(zend_bool incomplete_classes_only TSRMLS_DC) /* {{{ */
+{
+ BG(serialize_incomplete_classes_only) = incomplete_classes_only;
++ BG(serialize_incomplete_custom_seen) = 0;
+}
+/* }}} */
+
@@ -1064,7 +1906,7 @@ diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
{
long datalen;
-@@ -425,7 +442,16 @@ static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
+@@ -425,7 +442,18 @@ static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
return 0;
}
@@ -1078,11 +1920,33 @@ diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
+ MAKE_STD_ZVAL(payload);
+ ZVAL_STRINGL(payload, (char *) *p, datalen, 1);
+ zend_hash_update(Z_OBJPROP_PP(rval), MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED), &payload, sizeof(payload), NULL);
++ /* Unserializing the payload may have registered back-reference slots */
++ BG(serialize_incomplete_custom_seen) = 1;
+ } else if (ce->unserialize == NULL) {
zend_error(E_WARNING, "Class %s has no unserializer", ce->name);
object_init_ex(*rval, ce);
} else if (ce->unserialize(rval, ce, (const unsigned char*)*p, datalen, (zend_unserialize_data *)var_hash TSRMLS_CC) != SUCCESS) {
-@@ -782,6 +808,13 @@ object ":" uiv ":" ["] {
+@@ -536,6 +564,9 @@ PHPAPI int php_var_unserialize(UNSERIALIZE_PARAMETER)
+ *p = YYCURSOR;
+ if (!var_hash) return 0;
+
++ /* a C: payload kept verbatim holds slots that were not counted */
++ if (UNEXPECTED(BG(serialize_incomplete_custom_seen))) return 0;
++
+ id = parse_iv(start + 2) - 1;
+ if (id == -1 || var_access(var_hash, id, &rval_ref) != SUCCESS) {
+ return 0;
+@@ -557,6 +588,9 @@ PHPAPI int php_var_unserialize(UNSERIALIZE_PARAMETER)
+ *p = YYCURSOR;
+ if (!var_hash) return 0;
+
++ /* a C: payload kept verbatim holds slots that were not counted */
++ if (UNEXPECTED(BG(serialize_incomplete_custom_seen))) return 0;
++
+ id = parse_iv(start + 2) - 1;
+ if (id == -1 || var_access(var_hash, id, &rval_ref) != SUCCESS) {
+ return 0;
+@@ -782,6 +816,13 @@ object ":" uiv ":" ["] {
class_name = estrndup(class_name, len);
do {
@@ -1096,7 +1960,7 @@ diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
/* Try to find class directly */
BG(serialize_lock)++;
if (zend_lookup_class(class_name, len2, &pce TSRMLS_CC) == SUCCESS) {
-@@ -859,7 +892,7 @@ object ":" uiv ":" ["] {
+@@ -859,7 +900,7 @@ object ":" uiv ":" ["] {
if (custom_object) {
int ret;
@@ -1108,7 +1972,18 @@ diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
diff --git a/main/SAPI.c b/main/SAPI.c
--- a/main/SAPI.c
+++ b/main/SAPI.c
-@@ -519,18 +519,23 @@ SAPI_API void sapi_deactivate(TSRMLS_D)
+@@ -193,6 +193,10 @@ SAPI_API void sapi_handle_post(void *arg TSRMLS_DC)
+ }
+ efree(SG(request_info).content_type_dup);
+ SG(request_info).content_type_dup = NULL;
++
++ if (ZEND_HAS_OOM()) {
++ zend_handle_delayed_oom(TSRMLS_C);
++ }
+ }
+ }
+
+@@ -519,18 +523,23 @@ SAPI_API void sapi_deactivate(TSRMLS_D)
}
if (SG(request_info).auth_user) {
efree(SG(request_info).auth_user);
@@ -1135,7 +2010,50 @@ diff --git a/main/SAPI.c b/main/SAPI.c
diff --git a/main/main.c b/main/main.c
--- a/main/main.c
+++ b/main/main.c
-@@ -1560,6 +1560,7 @@ int php_request_startup(TSRMLS_D)
+@@ -174,12 +174,23 @@ static PHP_INI_MH(OnSetPrecision)
+ */
+ static PHP_INI_MH(OnChangeMemoryLimit)
+ {
++ long value;
++
+ if (new_value) {
+- PG(memory_limit) = zend_atol(new_value, new_value_length);
++ value = zend_atol(new_value, new_value_length);
+ } else {
+- PG(memory_limit) = 1<<30; /* effectively, no limit */
++ value = 1<<30; /* effectively, no limit */
++ }
++ if (zend_set_memory_limit(value) == FAILURE) {
++ /* the request memory is still in use while ini entries are restored,
++ * php_request_shutdown() sets the limit again once it is released */
++ if (stage != ZEND_INI_STAGE_DEACTIVATE) {
++ zend_error(E_WARNING, "Failed to set memory limit to %ld bytes (Current memory usage is %ld bytes)", value, (long) zend_memory_usage(1 TSRMLS_CC));
++ return FAILURE;
++ }
+ }
+- return zend_set_memory_limit(PG(memory_limit));
++ PG(memory_limit) = value;
++ return SUCCESS;
+ }
+ /* }}} */
+
+@@ -713,6 +724,15 @@ PHPAPI void php_verror(const char *docref, const char *params, int type, const c
+ char *message;
+ int is_function = 0;
+
++ if (ZEND_HAS_OOM()) {
++ /* callers do not expect a fatal error to return: raise the pending one */
++ if (type & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR)) {
++ zend_handle_delayed_oom(TSRMLS_C);
++ } else {
++ return;
++ }
++ }
++
+ /* get error text into buffer and escape for html if necessary */
+ buffer_len = vspprintf(&buffer, 0, format, args);
+
+@@ -1560,6 +1580,7 @@ int php_request_startup(TSRMLS_D)
PG(in_user_include) = 0;
zend_activate(TSRMLS_C);
@@ -1143,7 +2061,7 @@ diff --git a/main/main.c b/main/main.c
sapi_activate(TSRMLS_C);
#ifdef ZEND_SIGNALS
-@@ -1699,6 +1700,8 @@ void php_request_shutdown_for_hook(void *dummy)
+@@ -1699,6 +1720,8 @@ void php_request_shutdown_for_hook(void *dummy)
for (i = 0; i < NUM_TRACK_VARS; i++) {
if (PG(http_globals)[i]) {
zval_ptr_dtor(&PG(http_globals)[i]);
@@ -1152,7 +2070,17 @@ diff --git a/main/main.c b/main/main.c
}
}
} zend_end_try();
-@@ -1795,6 +1798,8 @@ void php_request_shutdown(void *dummy)
+@@ -1745,6 +1768,9 @@ void php_request_shutdown(void *dummy)
+
+ php_deactivate_ticks(TSRMLS_C);
+
++ /* a memory limit error is reported again if shutdown code goes over it */
++ EG(delayed_oom).state = ZEND_OOM_STATE_NONE;
++
+ /* 1. Call all possible shutdown functions registered with register_shutdown_function() */
+ if (PG(modules_activated)) zend_try {
+ php_call_shutdown_functions(TSRMLS_C);
+@@ -1795,6 +1821,8 @@ void php_request_shutdown(void *dummy)
for (i=0; i<NUM_TRACK_VARS; i++) {
if (PG(http_globals)[i]) {
zval_ptr_dtor(&PG(http_globals)[i]);
@@ -1161,7 +2089,16 @@ diff --git a/main/main.c b/main/main.c
}
}
} zend_end_try();
-@@ -1849,6 +1854,9 @@ void php_request_shutdown(void *dummy)
+@@ -1832,6 +1860,8 @@ void php_request_shutdown(void *dummy)
+ zend_try {
+ shutdown_memory_manager(CG(unclean_shutdown) || !report_memleaks, 0 TSRMLS_CC);
+ } zend_end_try();
++ EG(delayed_oom).state = ZEND_OOM_STATE_NONE;
++ zend_set_memory_limit(PG(memory_limit));
+ zend_interned_strings_restore(TSRMLS_C);
+
+ /* 12. Reset max_execution_time */
+@@ -1849,6 +1879,9 @@ void php_request_shutdown(void *dummy)
#ifdef HAVE_DTRACE
DTRACE_REQUEST_SHUTDOWN(SAFE_FILENAME(SG(request_info).path_translated), SAFE_FILENAME(SG(request_info).request_uri), (char *)SAFE_FILENAME(SG(request_info).request_method));
#endif /* HAVE_DTRACE */
@@ -1174,7 +2111,21 @@ diff --git a/main/main.c b/main/main.c
diff --git a/main/php_variables.c b/main/php_variables.c
--- a/main/php_variables.c
+++ b/main/php_variables.c
-@@ -187,7 +187,7 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars
+@@ -161,6 +161,13 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars
+ return;
+ }
+
++ /* Each level retains an array, so stop once over the limit. */
++ if (ZEND_HAS_OOM()) {
++ zval_dtor(val);
++ free_alloca(var_orig, use_heap);
++ return;
++ }
++
+ ip++;
+ index_s = ip;
+ if (isspace(*ip)) {
+@@ -187,7 +194,7 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars
if (!index) {
MAKE_STD_ZVAL(gpc_element);
@@ -1183,7 +2134,7 @@ diff --git a/main/php_variables.c b/main/php_variables.c
if (zend_hash_next_index_insert(symtable1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p) == FAILURE) {
zval_ptr_dtor(&gpc_element);
zval_dtor(val);
-@@ -203,7 +203,7 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars
+@@ -203,7 +210,7 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars
if (zend_symtable_find(symtable1, index, index_len + 1, (void **) &gpc_element_p) == FAILURE
|| Z_TYPE_PP(gpc_element_p) != IS_ARRAY) {
MAKE_STD_ZVAL(gpc_element);
@@ -1192,7 +2143,36 @@ diff --git a/main/php_variables.c b/main/php_variables.c
zend_symtable_update(symtable1, index, index_len + 1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p);
}
}
-@@ -315,7 +315,7 @@ SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data)
+@@ -259,7 +266,8 @@ SAPI_API SAPI_POST_HANDLER_FUNC(php_std_post_handler)
+ zval *array_ptr = (zval *) arg;
+ long count = 0;
+
+- if (SG(request_info).post_data == NULL) {
++ /* reading the body may already have gone over the limit */
++ if (SG(request_info).post_data == NULL || ZEND_HAS_OOM()) {
+ return;
+ }
+
+@@ -281,10 +289,18 @@ last_value:
+ val++;
+ val_len = php_url_decode(val, (p - val));
+ val = estrndup(val, val_len);
++ /* registering copies the value once more */
++ if (ZEND_HAS_OOM()) {
++ efree(val);
++ return;
++ }
+ if (sapi_module.input_filter(PARSE_POST, var, &val, val_len, &new_val_len TSRMLS_CC)) {
+ php_register_variable_safe(var, val, new_val_len, array_ptr TSRMLS_CC);
+ }
+ efree(val);
++ if (ZEND_HAS_OOM()) {
++ return;
++ }
+ }
+ s = p + 1;
+ }
+@@ -315,7 +331,7 @@ SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data)
case PARSE_GET:
case PARSE_COOKIE:
ALLOC_ZVAL(array_ptr);
@@ -1204,7 +2184,19 @@ diff --git a/main/php_variables.c b/main/php_variables.c
diff --git a/main/rfc1867.c b/main/rfc1867.c
--- a/main/rfc1867.c
+++ b/main/rfc1867.c
-@@ -395,6 +395,14 @@ static int find_boundary(multipart_buffer *self, char *boundary TSRMLS_DC)
+@@ -237,6 +237,11 @@ static int fill_buffer(multipart_buffer *self TSRMLS_DC)
+ {
+ int bytes_to_read, total_read = 0, actual_read = 0;
+
++ /* a body without Content-Length passed the check in rfc1867_post_handler() */
++ if (SG(post_max_size) > 0 && SG(read_post_bytes) > SG(post_max_size)) {
++ return 0;
++ }
++
+ /* shift the existing data if necessary */
+ if (self->bytes_in_buffer > 0 && self->buf_begin != self->buffer) {
+ memmove(self->buffer, self->buf_begin, self->bytes_in_buffer);
+@@ -395,6 +400,14 @@ static int find_boundary(multipart_buffer *self, char *boundary TSRMLS_DC)
return 0;
}
@@ -1219,7 +2211,7 @@ diff --git a/main/rfc1867.c b/main/rfc1867.c
/* parse headers */
static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header TSRMLS_DC)
{
-@@ -436,10 +444,15 @@ static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header T
+@@ -436,10 +449,15 @@ static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header T
}
*value = '\0';
@@ -1239,7 +2231,7 @@ diff --git a/main/rfc1867.c b/main/rfc1867.c
} else if (buf_value.c) { /* If no ':' on the line, add to previous line */
smart_str_appends(&buf_value, line);
} else {
-@@ -512,11 +525,11 @@ static char *php_ap_getword(const zend_encoding *encoding, char **line, char sto
+@@ -512,11 +530,11 @@ static char *php_ap_getword(const zend_encoding *encoding, char **line, char sto
return res;
}
@@ -1253,7 +2245,7 @@ diff --git a/main/rfc1867.c b/main/rfc1867.c
for (i = 0; i < len && start[i] != quote; ++i) {
if (start[i] == '\\' && (start[i + 1] == '\\' || (quote && start[i + 1] == quote))) {
-@@ -551,7 +564,7 @@ static char *php_ap_getword_conf(const zend_encoding *encoding, char *str TSRMLS
+@@ -551,7 +569,7 @@ static char *php_ap_getword_conf(const zend_encoding *encoding, char *str TSRMLS
while (*strend && !isspace(*strend)) {
++strend;
}
@@ -1262,3 +2254,25 @@ diff --git a/main/rfc1867.c b/main/rfc1867.c
}
}
+@@ -803,6 +821,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
+
+ zend_llist_clean(&header);
+
++ if (ZEND_HAS_OOM()) {
++ /* A previous body part has reached the soft memory limit: Stop here */
++ goto fileupload_done;
++ }
++
+ if (!multipart_buffer_headers(mbuff, &header TSRMLS_CC)) {
+ goto fileupload_done;
+ }
+@@ -1280,6 +1303,9 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
+ }
+
+ fileupload_done:
++ 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;
+
================================================================
---- gitweb:
http://git.pld-linux.org/gitweb.cgi/packages/php.git/commitdiff/d300f6158eaa600009b30c7cac6d895be7d1a2c1
More information about the pld-cvs-commit
mailing list