[packages/php/PHP_5_6] Rel 43. 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:11:55 CEST 2026


commit a1ff72725c1934a1a36ad78a3c3faa82fd41109f
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date:   Thu Oct 22 15:09:36 2026 +0200

    Rel 43. 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 | 1948 +++++++++++++++++++++++++++++++++++++++++++--
 php.spec                  |    5 +-
 2 files changed, 1886 insertions(+), 67 deletions(-)
---
diff --git a/php.spec b/php.spec
index 6985ea8..9932112 100644
--- a/php.spec
+++ b/php.spec
@@ -155,7 +155,7 @@ ERROR: You need to select at least one Apache SAPI to build shared modules.
 %undefine	with_filter
 %endif
 
-%define		rel	42
+%define		rel	43
 %define		orgname	php
 %define		ver_suffix 56
 %define		php_suffix %{!?with_default_php:%{ver_suffix}}
@@ -340,8 +340,9 @@ Patch308:	php-cve-2025-1219.patch
 Patch309:	php-cve-2025-1734.patch
 Patch310:	php-cve-2025-1736.patch
 Patch311:	php-cve-2025-1861.patch
-# CVE-2026-109618, CVE-2026-109619: upstream minimal mitigation for EOL branches, 5.6 port from
+# CVE-2026-109618, CVE-2026-109619: 5.6 port from
 # https://github.com/shivammathur/php-src-backports/commit/b18ab39d35ca71365193143283fa621d45628156
+# with the soft memory limit and session upload progress parts of upstream 3584a98bda0f ported (see PLD: lines)
 Patch400:	php-cve-2026-109618.patch
 # Patch410+: 5.6 ports of upstream fixes no 5.6 tree carries; each names its upstream commit
 Patch410:	php-cve-2025-1220.patch
diff --git a/php-cve-2026-109618.patch b/php-cve-2026-109618.patch
index 6a3fd72..44d7dbb 100644
--- a/php-cve-2026-109618.patch
+++ b/php-cve-2026-109618.patch
@@ -17,24 +17,34 @@ Co-authored-by: Arnaud Le Blanc <arnaud.lb at gmail.com>
 Co-authored-by: Ilija Tovilo <ilija.tovilo at me.com>
 
 (cherry picked from commit e00e84b0af5312566648d241701d70f120cb014d)
+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: session upload progress fix ported from upstream 3584a98bda0f (GHSA-6wmf-4rv5-p3rp), as in PHP_5_5, instead of the branch's "|| 1", which switched the feature off; with its tests. incomplete_classes_only is a BG() flag rather than a var_hash member: PHP_VAR_*_INIT() expand in extensions, so the var_hash layout is ABI and must not change
+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
 ---
- Zend/zend_API.c         |   9 ++++
- Zend/zend_API.h         |   2 +
- Zend/zend_alloc.c       | 102 +++++++++++++++++-----------------------
- Zend/zend_alloc.h       |  12 ++++-
- Zend/zend_execute_API.c |  11 +++++
- Zend/zend_llist.c       |   2 +
- Zend/zend_operators.c   |  33 +++++++------
- Zend/zend_strtod.c      |   4 +-
- ext/session/session.c   |   2 +-
- main/SAPI.c             |   5 ++
- main/main.c             |   7 +++
- main/php_variables.c    |   6 +--
- main/rfc1867.c          |  27 ++++++++---
- 13 files changed, 131 insertions(+), 91 deletions(-)
-
+diff --git a/Zend/zend.c b/Zend/zend.c
+index d41b163a..a1deb9f1 100644
+--- a/Zend/zend.c
++++ b/Zend/zend.c
+@@ -577,6 +577,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)));
+ }
+ /* }}} */
+ 
+@@ -1339,6 +1340,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
-index 0757cc9261..2b3bb43d37 100644
+index 0757cc92..2b3bb43d 100644
 --- a/Zend/zend_API.c
 +++ b/Zend/zend_API.c
 @@ -1016,6 +1016,15 @@ ZEND_API int _array_init(zval *arg, uint size ZEND_FILE_LINE_DC) /* {{{ */
@@ -54,7 +64,7 @@ index 0757cc9261..2b3bb43d37 100644
  {
  	/* which name should a numeric property have ? */
 diff --git a/Zend/zend_API.h b/Zend/zend_API.h
-index 41aa65f25b..80439622d2 100644
+index 41aa65f2..80439622 100644
 --- a/Zend/zend_API.h
 +++ b/Zend/zend_API.h
 @@ -362,11 +362,13 @@ ZEND_API char *zend_get_type_by_const(int type);
@@ -72,7 +82,7 @@ index 41aa65f25b..80439622d2 100644
  ZEND_API int _object_init_ex(zval *arg, zend_class_entry *ce ZEND_FILE_LINE_DC TSRMLS_DC);
  ZEND_API int _object_and_properties_init(zval *arg, zend_class_entry *ce, HashTable *properties ZEND_FILE_LINE_DC TSRMLS_DC);
 diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
-index c7babe12b7..6d2b4249b4 100644
+index c7babe12..3dcfcb47 100644
 --- a/Zend/zend_alloc.c
 +++ b/Zend/zend_alloc.c
 @@ -22,6 +22,7 @@
@@ -129,7 +139,7 @@ index c7babe12b7..6d2b4249b4 100644
  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)
  {
@@ -159,7 +169,9 @@ index c7babe12b7..6d2b4249b4 100644
 -			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
@@ -170,9 +182,7 @@ index c7babe12b7..6d2b4249b4 100644
 -				fprintf(stderr,
 -					format,
 -					limit,
-+	(void) heap;
-+	zend_mm_safe_error_ex(format, limit,
- #if ZEND_DEBUG
+-#if ZEND_DEBUG
 -					filename,
 -					lineno,
 +		filename, lineno,
@@ -183,17 +193,121 @@ index c7babe12b7..6d2b4249b4 100644
 -/* 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)) {
@@ -202,7 +316,7 @@ index c7babe12b7..6d2b4249b4 100644
  		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)) {
@@ -211,7 +325,7 @@ index c7babe12b7..6d2b4249b4 100644
                  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)) {
@@ -220,7 +334,7 @@ index c7babe12b7..6d2b4249b4 100644
                  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)) {
@@ -229,7 +343,7 @@ index c7babe12b7..6d2b4249b4 100644
                  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)) {
@@ -238,7 +352,7 @@ index c7babe12b7..6d2b4249b4 100644
  		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)) {
@@ -247,7 +361,7 @@ index c7babe12b7..6d2b4249b4 100644
  		return 0;
  	}
  	return res;
-@@ -2582,7 +2566,7 @@ ZEND_API void *_safe_emalloc_string(size_t nmemb, size_t size, size_t offset ZEN
+@@ -2582,7 +2613,7 @@ ZEND_API void *_safe_emalloc_string(size_t nmemb, size_t size, size_t offset ZEN
  {
  	size_t str_size = safe_address(nmemb, size, offset);
  	if (UNEXPECTED(str_size > INT_MAX)) {
@@ -256,8 +370,33 @@ index c7babe12b7..6d2b4249b4 100644
  	}
  	return emalloc_rel(str_size);
  }
+@@ -2693,9 +2724,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
-index 719f9c5cae..7cd2121b63 100644
+index 719f9c5c..01caeb9e 100644
 --- a/Zend/zend_alloc.h
 +++ b/Zend/zend_alloc.h
 @@ -27,6 +27,12 @@
@@ -291,17 +430,36 @@ index 719f9c5cae..7cd2121b63 100644
  }
  
  
-@@ -161,6 +167,8 @@ void zend_debug_alloc_output(char *format, ...);
+@@ -161,6 +167,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()
  
  /* fast cache for zval's */
 diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
-index 4c4ff017f6..aecdfbdff5 100644
+index bf754d25..408add37 100644
 --- a/Zend/zend_execute_API.c
 +++ b/Zend/zend_execute_API.c
 @@ -331,6 +331,17 @@ void shutdown_executor(TSRMLS_D) /* {{{ */
@@ -322,8 +480,45 @@ index 4c4ff017f6..aecdfbdff5 100644
  }
  /* }}} */
  
+@@ -639,6 +650,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 */
+ 	}
+@@ -896,6 +911,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
+index 9c55f8fd..15f18d19 100644
+--- a/Zend/zend_globals.h
++++ b/Zend/zend_globals.h
+@@ -265,6 +265,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
-index 3d597a3732..5be49408f0 100644
+index 3d597a37..5be49408 100644
 --- a/Zend/zend_llist.c
 +++ b/Zend/zend_llist.c
 @@ -115,6 +115,8 @@ ZEND_API void zend_llist_destroy(zend_llist *l)
@@ -336,7 +531,7 @@ index 3d597a3732..5be49408f0 100644
  }
  
 diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c
-index 450153ffec..620722c418 100644
+index 450153ff..620722c4 100644
 --- a/Zend/zend_operators.c
 +++ b/Zend/zend_operators.c
 @@ -1396,14 +1396,16 @@ ZEND_API int shift_right_function(zval *result, zval *op1, zval *op2 TSRMLS_DC)
@@ -412,7 +607,7 @@ index 450153ffec..620722c418 100644
  
  		memcpy(buf, Z_STRVAL_P(op1), Z_STRLEN_P(op1));
 diff --git a/Zend/zend_strtod.c b/Zend/zend_strtod.c
-index e74cf0eb82..3a55545f89 100644
+index e74cf0eb..3a55545f 100644
 --- a/Zend/zend_strtod.c
 +++ b/Zend/zend_strtod.c
 @@ -479,7 +479,7 @@ static Bigint * Balloc(int k)
@@ -433,24 +628,1489 @@ index e74cf0eb82..3a55545f89 100644
  		}
  		rv->k = k;
  		rv->maxwds = x;
+diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
+index f09d05b7..a24ad99f 100644
+--- 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
+index 13fd88e2..602fd3cb 100644
+--- 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
+index 31cd908c..f000723c 100644
+--- a/ext/session/php_session.h
++++ b/ext/session/php_session.h
+@@ -181,6 +181,10 @@ typedef struct _php_ps_globals {
+ 
+ 	zend_bool use_strict_mode; /* whether or not PHP accepts unknown session ids */
+ 	unsigned char session_data_hash[16]; /* binary MD5 hash length */
++
++	/* 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
-index d759fcabbf..6d535f9261 100644
+index fd73d61a..b04b6640 100644
 --- a/ext/session/session.c
 +++ b/ext/session/session.c
-@@ -2669,7 +2669,7 @@ static int php_session_rfc1867_callback(unsigned int event, void *event_data, vo
- 	if (php_session_rfc1867_orig_callback) {
- 		retval = php_session_rfc1867_orig_callback(event, event_data, extra TSRMLS_CC);
+@@ -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;
+@@ -866,6 +871,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);
+@@ -2369,6 +2379,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;
+@@ -2621,6 +2634,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);
++	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) {
+@@ -2642,23 +2686,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;
  	}
--	if (!PS(rfc1867_enabled)) {
-+	if (!PS(rfc1867_enabled) || 1) {
+ 
+-	php_session_initialize(TSRMLS_C);
+-	PS(session_status) = php_session_active;
++	if (php_session_rfc1867_open(TSRMLS_C) == FAILURE) {
++		return;
++	}
+ 	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);
++	php_var_set_incomplete_classes_only(0 TSRMLS_CC);
+ } /* }}} */
+ 
+ static void php_session_rfc1867_cleanup(php_session_rfc1867_progress *progress TSRMLS_DC) /* {{{ */
+ {
+-	php_session_initialize(TSRMLS_C);
+-	PS(session_status) = php_session_active;
++	if (php_session_rfc1867_open(TSRMLS_C) == FAILURE) {
++		return;
++	}
+ 	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);
++	php_var_set_incomplete_classes_only(0 TSRMLS_CC);
+ } /* }}} */
+ 
+ static int php_session_rfc1867_callback(unsigned int event, void *event_data, void **extra TSRMLS_DC) /* {{{ */
+@@ -2672,12 +2720,20 @@ static int php_session_rfc1867_callback(unsigned int event, void *event_data, vo
+ 	if (!PS(rfc1867_enabled)) {
  		return retval;
  	}
++	if (PG(modules_activated)) {
++		/* APACHE_HOOKS parses the body after RINIT. Do not run here as it corrupts the session. */
++		return retval;
++	}
+ 
+ 	progress = PS(rfc1867_progress);
+ 
+ 	switch(event) {
+ 		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);
++
+ 			progress = ecalloc(1, sizeof(php_session_rfc1867_progress));
+ 			progress->content_length = data->content_length;
+ 			progress->sname_len  = strlen(PS(session_name));
+@@ -2837,6 +2893,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
+index 00000000..66b15f06
+--- /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
+index 00000000..f13246f3
+--- /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);
++
++echo $out;
++if ($err !== '') {
++    echo "--- stderr:\n$err";
++}
++
++?>
++--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
+index 00000000..26a0026b
+--- /dev/null
++++ b/ext/session/tests/rfc1867_incomplete_class_reserved_props.phpt
+@@ -0,0 +1,146 @@
++--TEST--
++session rfc1867: the upload progress update rejects session data whose objects carry the incomplete class bookkeeping property names
++--SKIPIF--
++<?php
++include('skipif.inc');
++if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
++    die('skip CGI not available');
++}
++?>
++--FILE--
++<?php
++
++$dir = __DIR__ . '/rfc1867_incomplete_class_reserved_props';
++ at mkdir($dir);
++$sid = 'rfc1867reservedprops';
++$file = "$dir/sess_$sid";
++$worker = "$dir/worker.php";
++file_put_contents($worker, '<?php');
++
++/* The handler decodes the record with every object kept as
++ * __PHP_Incomplete_Class, which stores the class name of each object and the
++ * payload of C: entries under these two property names. Data that already
++ * carries one of them cannot be told apart from that bookkeeping, so the decode
++ * has to fail rather than let the two meanings be confused. */
++$cases = array(
++    'control'          => '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',
++);
++
++$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',
++    /* the update runs during request startup; On by default since PHP 8.0 */
++    'display_startup_errors' => '1',
++    '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.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'] = '';
++
++foreach ($cases as $label => $record) {
++    file_put_contents($file, $record);
++
++    $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]) . stream_get_contents($pipes[2]);
++    fclose($pipes[1]);
++    fclose($pipes[2]);
++    proc_close($proc);
++
++    /* 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($after, 'upload_progress_key|') !== false, true),
++        var_export(strncmp($after, $record, strlen($record)) === 0, true));
++}
++
++?>
++--CLEAN--
++<?php
++$dir = __DIR__ . '/rfc1867_incomplete_class_reserved_props';
++ at unlink("$dir/worker.php");
++foreach (glob("$dir/sess_*") as $f) {
++    unlink($f);
++}
++ at rmdir($dir);
++?>
++--EXPECT--
++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
+index 00000000..4ac14553
+--- /dev/null
++++ b/ext/session/tests/rfc1867_no_user_code.phpt
+@@ -0,0 +1,203 @@
++--TEST--
++session rfc1867: the upload progress update resolves no classes, keeps the session data intact and leaves no $_SESSION behind
++--SKIPIF--
++<?php
++include('skipif.inc');
++if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
++    die('skip CGI not available');
++}
++?>
++--FILE--
++<?php
++
++$dir = __DIR__ . '/rfc1867_no_user_code';
++ at mkdir($dir);
++$sid = 'rfc1867nousercode';
++$file = "$dir/sess_$sid";
++$worker = "$dir/worker.php";
++
++/* A hand-written record, as session_encode() produces it for the `php` handler:
++ * an object with __wakeup(), a Serializable payload (C:) and a class that does
++ * not exist. Writing it directly leaves the session module untouched until the
++ * upload progress handler runs, as in a real request. */
++$record = 'w|O:1:"W":1:{s:1:"a";i:1;}'
++        . 'c|C:4:"Cust":7:{payload}'
++        . 'm|O:7:"Missing":1:{s:1:"x";i:3;}';
++file_put_contents($file, $record);
++
++/* None of these classes exist while the handler decodes the record: the request
++ * script is not even compiled yet. Resolving them, C:4:"Cust" would lose its
++ * payload. The handler keeps every object as __PHP_Incomplete_Class instead and
++ * writes it back unchanged. */
++file_put_contents($worker, <<<'PHP'
++<?php
++$record = getenv('TEST_SESSION_RECORD');
++$file = getenv('TEST_SESSION_FILE');
++
++spl_autoload_register(function ($class) { echo "autoload($class)\n"; });
++
++class W {
++    public $a = 1;
++    function __wakeup() {
++        echo "W::__wakeup\n";
++    }
++    function __sleep() {
++        echo "W::__sleep\n"; return array('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['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 = 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_no_user_code';
++ 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)
++bool(true)
++--- record after the update:
++bool(true)
++string(20) "upload_progress_key|"
++--- session_start():
++autoload(Missing)
++W::__wakeup
++string(17) "rfc1867nousercode"
++int(1)
++string(7) "payload"
++object(__PHP_Incomplete_Class)#%d (2) {
++  ["__PHP_Incomplete_Class_Name"]=>
++  string(7) "Missing"
++  ["x"]=>
++  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
+index 00000000..6c2dc9e5
+--- /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
+index 1ea19066..30664734 100644
+--- a/ext/standard/basic_functions.c
++++ b/ext/standard/basic_functions.c
+@@ -3718,6 +3718,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;
+@@ -3758,6 +3760,20 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */
+ }
+ /* }}} */
+ 
++/* Called from php_request_startup(), before any request data is processed. */
++PHPAPI void php_basic_early_rinit(TSRMLS_D)
++{
++	/* The session upload progress handler (un)serializes the session and
++	 * registers the session id with the URL rewriter from
++	 * php_hash_environment(), before RINIT. */
++	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;
++	php_url_scanner_early_rinit(TSRMLS_C);
++}
++
+ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */
+ {
+ 	if (BG(strtok_zval)) {
+diff --git a/ext/standard/basic_functions.h b/ext/standard/basic_functions.h
+index d900bb02..26701d42 100644
+--- a/ext/standard/basic_functions.h
++++ b/ext/standard/basic_functions.h
+@@ -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 +261,7 @@ typedef struct _php_shutdown_function_entry {
+ 	int arg_count;
+ } php_shutdown_function_entry;
+ 
++PHPAPI void php_basic_early_rinit(TSRMLS_D);
+ PHPAPI extern zend_bool register_user_shutdown_function(char *function_name, size_t function_len, php_shutdown_function_entry *shutdown_function_entry TSRMLS_DC);
+ PHPAPI extern zend_bool remove_user_shutdown_function(char *function_name, size_t function_len TSRMLS_DC);
+ PHPAPI extern zend_bool append_user_shutdown_function(php_shutdown_function_entry shutdown_function_entry TSRMLS_DC);
+diff --git a/ext/standard/php_incomplete_class.h b/ext/standard/php_incomplete_class.h
+index efcbbf8d..7efa41ea 100644
+--- a/ext/standard/php_incomplete_class.h
++++ b/ext/standard/php_incomplete_class.h
+@@ -52,6 +52,9 @@
+ 
+ #define INCOMPLETE_CLASS "__PHP_Incomplete_Class"
+ #define MAGIC_MEMBER "__PHP_Incomplete_Class_Name"
++/* Set by php_var_unserialize() in incomplete_classes_only mode: the raw payload
++ * of a C: value. The serializer writes it back unchanged. */
++#define MAGIC_MEMBER_SERIALIZED "__PHP_Incomplete_Class_Serialized"
+ 
+ #ifdef __cplusplus
+ extern "C" {
+diff --git a/ext/standard/php_var.h b/ext/standard/php_var.h
+index 95af4837..7cd35514 100644
+--- a/ext/standard/php_var.h
++++ b/ext/standard/php_var.h
+@@ -51,6 +51,7 @@ 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_set_incomplete_classes_only(zend_bool incomplete_classes_only TSRMLS_DC);
+ 
+ #define PHP_VAR_SERIALIZE_INIT(var_hash_ptr) \
+ do  { \
+diff --git a/ext/standard/url_scanner_ex.c b/ext/standard/url_scanner_ex.c
+index 0719dae7..ae84dbfd 100644
+--- a/ext/standard/url_scanner_ex.c
++++ b/ext/standard/url_scanner_ex.c
+@@ -948,6 +948,12 @@ static int php_url_scanner_ex_deactivate(TSRMLS_D)
+ 	return SUCCESS;
+ }
+ 
++void php_url_scanner_early_rinit(TSRMLS_D)
++{
++	/* See php_basic_early_rinit() */
++	php_url_scanner_ex_activate(TSRMLS_C);
++}
++
+ static void php_url_scanner_output_handler(char *output, uint output_len, char **handled_output, uint *handled_output_len, int mode TSRMLS_DC)
+ {
+ 	size_t len;
+diff --git a/ext/standard/url_scanner_ex.h b/ext/standard/url_scanner_ex.h
+index fb64c673..e96cd050 100644
+--- a/ext/standard/url_scanner_ex.h
++++ b/ext/standard/url_scanner_ex.h
+@@ -26,6 +26,7 @@ PHP_MSHUTDOWN_FUNCTION(url_scanner_ex);
+ 
+ PHP_RINIT_FUNCTION(url_scanner_ex);
+ PHP_RSHUTDOWN_FUNCTION(url_scanner_ex);
++void php_url_scanner_early_rinit(TSRMLS_D);
+ 
+ PHPAPI char *php_url_scanner_adapt_single_url(const char *url, size_t urllen, const char *name, const char *value, size_t *newlen TSRMLS_DC);
+ PHPAPI int php_url_scanner_add_var(char *name, int name_len, char *value, int value_len, int urlencode TSRMLS_DC);
+diff --git a/ext/standard/url_scanner_ex.re b/ext/standard/url_scanner_ex.re
+index 5ee7063c..b56e94f1 100644
+--- a/ext/standard/url_scanner_ex.re
++++ b/ext/standard/url_scanner_ex.re
+@@ -456,6 +456,12 @@ static int php_url_scanner_ex_deactivate(TSRMLS_D)
+ 	return SUCCESS;
+ }
+ 
++void php_url_scanner_early_rinit(TSRMLS_D)
++{
++	/* See php_basic_early_rinit() */
++	php_url_scanner_ex_activate(TSRMLS_C);
++}
++
+ static void php_url_scanner_output_handler(char *output, uint output_len, char **handled_output, uint *handled_output_len, int mode TSRMLS_DC)
+ {
+ 	size_t len;
+diff --git a/ext/standard/var.c b/ext/standard/var.c
+index 137e794f..ae2ef077 100644
+--- a/ext/standard/var.c
++++ b/ext/standard/var.c
+@@ -615,6 +615,38 @@ static inline zend_bool php_var_serialize_class_name(smart_str *buf, zval *struc
+ }
+ /* }}} */
+ 
++/* Write back the C: payload kept by php_var_unserialize() in incomplete_classes_only mode */
++static zend_bool php_var_serialize_incomplete_raw(smart_str *buf, zval *struc TSRMLS_DC) /* {{{ */
++{
++	zval **member;
++	char *class_name;
++	zend_uint name_len;
++
++	if (zend_hash_find(Z_OBJPROP_P(struc), MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED), (void **) &member) == FAILURE
++			|| Z_TYPE_PP(member) != IS_STRING) {
++		/* Other incomplete classes are handled using the normal path */
++		return 0;
++	}
++
++	class_name = php_lookup_class_name(struc, &name_len);
++	if (!class_name) {
++		return 0;
++	}
++
++	smart_str_appendl(buf, "C:", 2);
++	smart_str_append_long(buf, (long) name_len);
++	smart_str_appendl(buf, ":\"", 2);
++	smart_str_appendl(buf, class_name, name_len);
++	smart_str_appendl(buf, "\":", 2);
++	smart_str_append_long(buf, (long) Z_STRLEN_PP(member));
++	smart_str_appendl(buf, ":{", 2);
++	smart_str_appendl(buf, Z_STRVAL_PP(member), Z_STRLEN_PP(member));
++	smart_str_appendc(buf, '}');
++	efree(class_name);
++	return 1;
++}
++/* }}} */
++
+ static void php_var_serialize_class(smart_str *buf, zval *struc, zval *retval_ptr, HashTable *var_hash TSRMLS_DC) /* {{{ */
+ {
+ 	int count;
+@@ -768,6 +800,10 @@ static void php_var_serialize_intern(smart_str *buf, zval *struc, HashTable *var
+ 					ce = Z_OBJCE_P(struc);
+ 				}
+ 
++				if (ce == PHP_IC_ENTRY && BG(serialize_incomplete_classes_only) && php_var_serialize_incomplete_raw(buf, struc TSRMLS_CC)) {
++					return;
++				}
++
+ 				if (ce && ce->serialize != NULL) {
+ 					/* has custom handler */
+ 					unsigned char *serialized_data = NULL;
+@@ -907,6 +943,17 @@ PHPAPI void php_var_serialize(smart_str *buf, zval **struc, php_serialize_data_t
+ }
+ /* }}} */
+ 
++/* Represent every object as __PHP_Incomplete_Class, without resolving its class:
++ * no autoloading, no unserialize_callback_func, no __wakeup(). O: and C: values
++ * serialize back to the same bytes. A global rather than a var_hash member:
++ * extensions allocate var_hash through the PHP_VAR_*_INIT() macros. */
++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;
++}
++/* }}} */
++
+ /* {{{ proto string serialize(mixed variable)
+    Returns a string representation of variable (which can later be unserialized) */
+ PHP_FUNCTION(serialize)
+diff --git a/ext/standard/var_unserializer.c b/ext/standard/var_unserializer.c
+index bc4fb34c..5aeee099 100644
+--- a/ext/standard/var_unserializer.c
++++ b/ext/standard/var_unserializer.c
+@@ -341,6 +341,16 @@ static inline size_t parse_uiv(const unsigned char *p)
+ #define UNSERIALIZE_PARAMETER zval **rval, const unsigned char **p, const unsigned char *max, php_unserialize_data_t *var_hash TSRMLS_DC
+ #define UNSERIALIZE_PASSTHRU rval, p, max, var_hash TSRMLS_CC
+ 
++/* The members that incomplete_classes_only mode writes itself: the class name of
++ * every object, and the payload kept for C: entries. Data that already carries
++ * one of them cannot be told apart from that bookkeeping, so it is rejected
++ * rather than silently overwritten. */
++static inline int is_incomplete_class_reserved_member(const char *name, int name_len)
++{
++	return (name_len == sizeof(MAGIC_MEMBER) - 1 && !memcmp(name, MAGIC_MEMBER, name_len))
++		|| (name_len == sizeof(MAGIC_MEMBER_SERIALIZED) - 1 && !memcmp(name, MAGIC_MEMBER_SERIALIZED, name_len));
++}
++
+ static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long elements, int objprops)
+ {
+ 	while (elements-- > 0) {
+@@ -358,6 +368,13 @@ static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long
+ 			return 0;
+ 		}
+ 
++		if (objprops && UNEXPECTED(BG(serialize_incomplete_classes_only))
++				&& Z_TYPE_P(key) == IS_STRING
++				&& UNEXPECTED(is_incomplete_class_reserved_member(Z_STRVAL_P(key), Z_STRLEN_P(key)))) {
++			var_push_dtor_no_addref(var_hash, &key);
++			return 0;
++		}
++
+ 		ALLOC_INIT_ZVAL(data);
+ 
+ 		if (!php_var_unserialize(&data, p, max, var_hash TSRMLS_CC)) {
+@@ -412,7 +429,7 @@ static inline int finish_nested_data(UNSERIALIZE_PARAMETER)
+ 	return 1;
+ }
+ 
+-static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
++static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce, zend_bool keep_payload)
+ {
+ 	long datalen;
+ 
+@@ -425,7 +442,18 @@ static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
+ 		return 0;
+ 	}
+ 
+-	if (ce->unserialize == NULL) {
++	if (keep_payload) {
++		/* The class was not resolved (see php_var_set_incomplete_classes_only()):
++		 * keep the payload verbatim, so that the object serializes back to the same bytes. */
++		zval *payload;
++
++		object_init_ex(*rval, ce);
++		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) {
+@@ -713,6 +741,13 @@ yy20:
+ 	class_name = estrndup(class_name, len);
+ 
+ 	do {
++		if (BG(serialize_incomplete_classes_only)) {
++			/* Do not look the class up at all, so that no user code can run. */
++			incomplete_class = 1;
++			ce = PHP_IC_ENTRY;
++			break;
++		}
++
+ 		/* Try to find class directly */
+ 		BG(serialize_lock)++;
+ 		if (zend_lookup_class(class_name, len2, &pce TSRMLS_CC) == SUCCESS) {
+@@ -792,7 +827,7 @@ yy20:
+ 	if (custom_object) {
+ 		int ret;
+ 
+-		ret = object_custom(UNSERIALIZE_PASSTHRU, ce);
++		ret = object_custom(UNSERIALIZE_PASSTHRU, ce, BG(serialize_incomplete_classes_only));
  
+ 		if (ret && incomplete_class) {
+ 			php_store_class_name(*rval, class_name, len2);
+@@ -1266,6 +1301,9 @@ yy91:
+  	*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;
+@@ -1310,6 +1348,9 @@ yy97:
+  	*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;
+diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
+index 1707ca5c..4569effe 100644
+--- a/ext/standard/var_unserializer.re
++++ b/ext/standard/var_unserializer.re
+@@ -347,6 +347,16 @@ static inline size_t parse_uiv(const unsigned char *p)
+ #define UNSERIALIZE_PARAMETER zval **rval, const unsigned char **p, const unsigned char *max, php_unserialize_data_t *var_hash TSRMLS_DC
+ #define UNSERIALIZE_PASSTHRU rval, p, max, var_hash TSRMLS_CC
+ 
++/* The members that incomplete_classes_only mode writes itself: the class name of
++ * every object, and the payload kept for C: entries. Data that already carries
++ * one of them cannot be told apart from that bookkeeping, so it is rejected
++ * rather than silently overwritten. */
++static inline int is_incomplete_class_reserved_member(const char *name, int name_len)
++{
++	return (name_len == sizeof(MAGIC_MEMBER) - 1 && !memcmp(name, MAGIC_MEMBER, name_len))
++		|| (name_len == sizeof(MAGIC_MEMBER_SERIALIZED) - 1 && !memcmp(name, MAGIC_MEMBER_SERIALIZED, name_len));
++}
++
+ static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long elements, int objprops)
+ {
+ 	while (elements-- > 0) {
+@@ -364,6 +374,13 @@ static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long
+ 			return 0;
+ 		}
+ 
++		if (objprops && UNEXPECTED(BG(serialize_incomplete_classes_only))
++				&& Z_TYPE_P(key) == IS_STRING
++				&& UNEXPECTED(is_incomplete_class_reserved_member(Z_STRVAL_P(key), Z_STRLEN_P(key)))) {
++			var_push_dtor_no_addref(var_hash, &key);
++			return 0;
++		}
++
+ 		ALLOC_INIT_ZVAL(data);
+ 
+ 		if (!php_var_unserialize(&data, p, max, var_hash TSRMLS_CC)) {
+@@ -418,7 +435,7 @@ static inline int finish_nested_data(UNSERIALIZE_PARAMETER)
+ 	return 1;
+ }
+ 
+-static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
++static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce, zend_bool keep_payload)
+ {
+ 	long datalen;
+ 
+@@ -431,7 +448,18 @@ static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
+ 		return 0;
+ 	}
+ 
+-	if (ce->unserialize == NULL) {
++	if (keep_payload) {
++		/* The class was not resolved (see php_var_set_incomplete_classes_only()):
++		 * keep the payload verbatim, so that the object serializes back to the same bytes. */
++		zval *payload;
++
++		object_init_ex(*rval, ce);
++		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) {
+@@ -539,6 +567,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;
+@@ -560,6 +591,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;
+@@ -796,6 +830,13 @@ object ":" uiv ":" ["]	{
+ 	class_name = estrndup(class_name, len);
+ 
+ 	do {
++		if (BG(serialize_incomplete_classes_only)) {
++			/* Do not look the class up at all, so that no user code can run. */
++			incomplete_class = 1;
++			ce = PHP_IC_ENTRY;
++			break;
++		}
++
+ 		/* Try to find class directly */
+ 		BG(serialize_lock)++;
+ 		if (zend_lookup_class(class_name, len2, &pce TSRMLS_CC) == SUCCESS) {
+@@ -875,7 +916,7 @@ object ":" uiv ":" ["]	{
+ 	if (custom_object) {
+ 		int ret;
+ 
+-		ret = object_custom(UNSERIALIZE_PASSTHRU, ce);
++		ret = object_custom(UNSERIALIZE_PASSTHRU, ce, BG(serialize_incomplete_classes_only));
+ 
+ 		if (ret && incomplete_class) {
+ 			php_store_class_name(*rval, class_name, len2);
 diff --git a/main/SAPI.c b/main/SAPI.c
-index 49ffc1c9a4..a60ebb1362 100644
+index 49ffc1c9..49463b9a 100644
 --- a/main/SAPI.c
 +++ b/main/SAPI.c
-@@ -528,18 +528,23 @@ SAPI_API void sapi_deactivate(TSRMLS_D)
+@@ -185,6 +185,10 @@ SAPI_API void sapi_handle_post(void *arg TSRMLS_DC)
+ 		SG(request_info).post_entry->post_handler(SG(request_info).content_type_dup, arg TSRMLS_CC);
+ 		efree(SG(request_info).content_type_dup);
+ 		SG(request_info).content_type_dup = NULL;
++
++		if (ZEND_HAS_OOM()) {
++			zend_handle_delayed_oom(TSRMLS_C);
++		}
+ 	}
+ }
+ 
+@@ -528,18 +532,23 @@ SAPI_API void sapi_deactivate(TSRMLS_D)
  	}
  	if (SG(request_info).auth_user) {
  		efree(SG(request_info).auth_user);
@@ -475,10 +2135,61 @@ index 49ffc1c9a4..a60ebb1362 100644
  	if (sapi_module.deactivate) {
  		sapi_module.deactivate(TSRMLS_C);
 diff --git a/main/main.c b/main/main.c
-index 66da1881d4..4097e8821d 100644
+index 66da1881..7d26ac10 100644
 --- a/main/main.c
 +++ b/main/main.c
-@@ -1785,6 +1785,8 @@ void php_request_shutdown_for_hook(void *dummy)
+@@ -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;
+ }
+ /* }}} */
+ 
+@@ -783,6 +794,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);
+ 
+@@ -1646,6 +1666,7 @@ int php_request_startup(TSRMLS_D)
+ 		PG(in_user_include) = 0;
+ 
+ 		zend_activate(TSRMLS_C);
++		php_basic_early_rinit(TSRMLS_C);
+ 		sapi_activate(TSRMLS_C);
+ 
+ #ifdef ZEND_SIGNALS
+@@ -1785,6 +1806,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]);
@@ -487,7 +2198,17 @@ index 66da1881d4..4097e8821d 100644
  			}
  		}
  	} zend_end_try();
-@@ -1881,6 +1883,8 @@ void php_request_shutdown(void *dummy)
+@@ -1831,6 +1854,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);
+@@ -1881,6 +1907,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]);
@@ -496,7 +2217,16 @@ index 66da1881d4..4097e8821d 100644
  			}
  		}
  	} zend_end_try();
-@@ -1938,6 +1942,9 @@ void php_request_shutdown(void *dummy)
+@@ -1921,6 +1949,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 */
+@@ -1938,6 +1968,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 */
@@ -507,10 +2237,24 @@ index 66da1881d4..4097e8821d 100644
  /* }}} */
  
 diff --git a/main/php_variables.c b/main/php_variables.c
-index fbd9562e8d..606a4b03d6 100644
+index fbd9562e..ecc611d8 100644
 --- a/main/php_variables.c
 +++ b/main/php_variables.c
-@@ -191,7 +191,7 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars
+@@ -165,6 +165,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)) {
+@@ -191,7 +198,7 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars
  
  			if (!index) {
  				MAKE_STD_ZVAL(gpc_element);
@@ -519,7 +2263,7 @@ index fbd9562e8d..606a4b03d6 100644
  				if (zend_hash_next_index_insert(symtable1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p) == FAILURE) {
  					zval_ptr_dtor(&gpc_element);
  					zval_dtor(val);
-@@ -208,7 +208,7 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars
+@@ -208,7 +215,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);
@@ -528,7 +2272,50 @@ index fbd9562e8d..606a4b03d6 100644
  					zend_symtable_update(symtable1, index, index_len + 1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p);
  				}
  			}
-@@ -406,7 +406,7 @@ SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data)
+@@ -307,6 +314,10 @@ static zend_bool add_post_var(zval *arr, post_var_data_t *var, zend_bool eof TSR
+ 	php_url_decode(var->ptr, klen);
+ 
+ 	val = estrndup(ksep, vlen);
++	if (ZEND_HAS_OOM()) {
++		efree(val);
++		return 0;
++	}
+ 	if (vlen) {
+ 		vlen = php_url_decode(val, vlen);
+ 	}
+@@ -315,6 +326,9 @@ static zend_bool add_post_var(zval *arr, post_var_data_t *var, zend_bool eof TSR
+ 		php_register_variable_safe(var->ptr, val, new_vlen, arr TSRMLS_CC);
+ 	}
+ 	efree(val);
++	if (ZEND_HAS_OOM()) {
++		return 0;
++	}
+ 
+ 	var->ptr = vsep + (vsep != var->end);
+ 	var->already_scanned = 0;
+@@ -335,6 +349,12 @@ static inline int add_post_vars(zval *arr, post_var_data_t *vars, zend_bool eof
+ 					max_vars);
+ 			return FAILURE;
+ 		}
++		if (ZEND_HAS_OOM()) {
++			return FAILURE;
++		}
++	}
++	if (ZEND_HAS_OOM()) {
++		return FAILURE;
+ 	}
+ 
+ 	if (!eof && vars->str.c != vars->ptr) {
+@@ -364,7 +384,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(php_std_post_handler)
+ 			if (len && len != (size_t) -1) {
+ 				smart_str_appendl(&post_data.str, buf, len);
+ 
+-				if (SUCCESS != add_post_vars(arr, &post_data, 0 TSRMLS_CC)) {
++				if (ZEND_HAS_OOM() || SUCCESS != add_post_vars(arr, &post_data, 0 TSRMLS_CC)) {
+ 					if (post_data.str.c) {
+ 						efree(post_data.str.c);
+ 					}
+@@ -406,7 +426,7 @@ SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data)
  		case PARSE_GET:
  		case PARSE_COOKIE:
  			ALLOC_ZVAL(array_ptr);
@@ -538,10 +2325,22 @@ index fbd9562e8d..606a4b03d6 100644
  			switch (arg) {
  				case PARSE_POST:
 diff --git a/main/rfc1867.c b/main/rfc1867.c
-index 1efd28298e..1b59ca3206 100644
+index dd710fab..431be048 100644
 --- a/main/rfc1867.c
 +++ b/main/rfc1867.c
-@@ -400,6 +400,14 @@ static int find_boundary(multipart_buffer *self, char *boundary TSRMLS_DC)
+@@ -242,6 +242,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);
+@@ -400,6 +405,14 @@ static int find_boundary(multipart_buffer *self, char *boundary TSRMLS_DC)
  	return 0;
  }
  
@@ -556,7 +2355,7 @@ index 1efd28298e..1b59ca3206 100644
  /* parse headers */
  static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header TSRMLS_DC)
  {
-@@ -441,10 +449,15 @@ static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header T
+@@ -441,10 +454,15 @@ static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header T
  			}
  
  			*value = '\0';
@@ -576,7 +2375,7 @@ index 1efd28298e..1b59ca3206 100644
  		} else if (buf_value.c) { /* If no ':' on the line, add to previous line */
  			smart_str_appends(&buf_value, line);
  		} else {
-@@ -517,11 +530,11 @@ static char *php_ap_getword(const zend_encoding *encoding, char **line, char sto
+@@ -517,11 +535,11 @@ static char *php_ap_getword(const zend_encoding *encoding, char **line, char sto
  	return res;
  }
  
@@ -590,7 +2389,7 @@ index 1efd28298e..1b59ca3206 100644
  
  	for (i = 0; i < len && start[i] != quote; ++i) {
  		if (start[i] == '\\' && (start[i + 1] == '\\' || (quote && start[i + 1] == quote))) {
-@@ -556,7 +569,7 @@ static char *php_ap_getword_conf(const zend_encoding *encoding, char *str TSRMLS
+@@ -556,7 +574,7 @@ static char *php_ap_getword_conf(const zend_encoding *encoding, char *str TSRMLS
  		while (*strend && !isspace(*strend)) {
  			++strend;
  		}
@@ -599,6 +2398,25 @@ index 1efd28298e..1b59ca3206 100644
  	}
  }
  
--- 
-2.56.0
-
+@@ -807,6 +825,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;
+ 		}
+@@ -1309,6 +1332,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/a1ff72725c1934a1a36ad78a3c3faa82fd41109f



More information about the pld-cvs-commit mailing list