[packages/php-pecl-igbinary] Up to 3.2.17RC1; enable session serializer; CVE-2026-109618 counterpart

arekm arekm at pld-linux.org
Thu Oct 22 11:28:41 CEST 2026


commit 4c1e9fcb84857cb94cbd660127c5d0424d3cc657
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date:   Thu Oct 22 11:28:07 2026 +0200

    Up to 3.2.17RC1; enable session serializer; CVE-2026-109618 counterpart
    
    php's fix covers only its own serializers; igbinary still resolved
    classes and ran user code while decoding sessions for upload progress.

 php-pecl-igbinary.spec         |  45 ++--
 session-upload-progress.patch  | 591 +++++++++++++++++++++++++++++++++++++++++
 shared-session.patch           |  76 ++++++
 unserialize-long64-32bit.patch |  20 ++
 unserialize-truncated.patch    |  24 ++
 5 files changed, 731 insertions(+), 25 deletions(-)
---
diff --git a/php-pecl-igbinary.spec b/php-pecl-igbinary.spec
index 0cc64fe..fa5f902 100644
--- a/php-pecl-igbinary.spec
+++ b/php-pecl-igbinary.spec
@@ -4,21 +4,27 @@
 
 %define		php_name	php%{?php_suffix}
 %define		modname	igbinary
+%define		subver	RC1
+%define		rel	1
 Summary:	Replacement for the standard PHP serializer
 Name:		%{php_name}-pecl-%{modname}
-Version:	3.1.2
-Release:	1
+Version:	3.2.17
+Release:	0.%{subver}.%{rel}
 License:	BSD
 Group:		Development/Languages/PHP
-Source0:	https://pecl.php.net/get/%{modname}-%{version}.tgz
-# Source0-md5:	34280e1609ef8e23f67fe3e690405787
+Source0:	https://pecl.php.net/get/%{modname}-%{version}%{subver}.tgz
+# Source0-md5:	84d49a68917b5ad5cf087ee7286e91ba
 Source2:	%{modname}.ini
+Patch0:		shared-session.patch
+Patch1:		session-upload-progress.patch
+Patch2:		unserialize-truncated.patch
+Patch3:		unserialize-long64-32bit.patch
 URL:		https://pecl.php.net/package/igbinary
+%{?with_tests:BuildRequires:	%{php_name}-cgi}
 %{?with_tests:BuildRequires:	%{php_name}-cli}
 BuildRequires:	%{php_name}-devel >= 4:7.0
 %{?with_tests:BuildRequires:	%{php_name}-pcre}
 %{?with_tests:BuildRequires:	%{php_name}-session}
-%{?with_tests:BuildRequires:	%{php_name}-simplexml}
 %{?with_tests:BuildRequires:	%{php_name}-spl}
 BuildRequires:	rpmbuild(macros) >= 1.666
 %{?requires_php_extension}
@@ -47,38 +53,26 @@ These are the files needed to compile programs using Igbinary
 %prep
 %setup -qc
 mv %{modname}-*/* .
+%patch -P0 -p1
+%patch -P1 -p1
+%patch -P2 -p1
+%patch -P3 -p1
 
 cat <<'EOF' > run-tests.sh
 #!/bin/sh
 export NO_INTERACTION=1 REPORT_EXIT_STATUS=1 MALLOC_CHECK_=2
+export TEST_PHP_CGI_EXECUTABLE=%{__php}.cgi
 exec %{__make} test \
 	PHP_EXECUTABLE=%{__php} \
 %if "%php_major_version.%php_minor_version" >= "7.4"
-	PHP_TEST_SHARED_SYSTEM_EXTENSIONS="simplexml session" \
+	PHP_TEST_SHARED_SYSTEM_EXTENSIONS="session" \
 %else
-	PHP_TEST_SHARED_SYSTEM_EXTENSIONS="pcre spl simplexml session" \
+	PHP_TEST_SHARED_SYSTEM_EXTENSIONS="pcre spl session" \
 %endif
 	RUN_TESTS_SETTINGS="-q $*"
 EOF
 chmod +x run-tests.sh
 
-xfail() {
-	local t=$1
-	test -f $t
-	cat >> $t <<-EOF
-
-	--XFAIL--
-	Skip
-	EOF
-}
-
-while read line; do
-	t=${line##*\[}; t=${t%\]}
-	xfail $t
-done << 'EOF'
-Test serializing multiple reference groups to the same empty array [tests/igbinary_067.phpt]
-EOF
-
 %build
 phpize
 %configure
@@ -87,9 +81,10 @@ phpize
 # simple module load test
 %{__php} -n -q \
 	-dextension_dir=modules \
+%if "%php_major_version.%php_minor_version" < "7.4"
 	-dextension=%{php_extensiondir}/pcre.so \
 	-dextension=%{php_extensiondir}/spl.so \
-	-dextension=%{php_extensiondir}/simplexml.so \
+%endif
 	-dextension=%{php_extensiondir}/session.so \
 	-dextension=%{modname}.so \
 	-m > modules.log
diff --git a/session-upload-progress.patch b/session-upload-progress.patch
new file mode 100644
index 0000000..5d8d129
--- /dev/null
+++ b/session-upload-progress.patch
@@ -0,0 +1,591 @@
+Keep session objects as __PHP_Incomplete_Class during session upload progress.
+php_session_rfc1867_update() decodes and re-encodes the session before RINIT,
+when no user code may run; php-src keeps every object incomplete there, igbinary
+still autoloaded classes, called unserialize_callback_func, __wakeup(),
+__unserialize() and Serializable::unserialize(), and destroyed sessions holding
+Serializable objects of not yet declared classes.
+
+Bug: https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
+See-also: https://github.com/php/php-src/commit/3584a98bda0f
+
+diff --git a/src/php7/igbinary.c b/src/php7/igbinary.c
+index 9a1b72e..6747818 100644
+--- a/src/php7/igbinary.c
++++ b/src/php7/igbinary.c
+@@ -34,6 +34,15 @@
+ 
+ #include "ext/standard/php_incomplete_class.h"
+ 
++/* Members of __PHP_Incomplete_Class holding a Serializable payload or an enum case name
++ * in incomplete_classes_only mode, named as in php-src releases that have this mode. */
++#ifndef MAGIC_MEMBER_SERIALIZED
++# define MAGIC_MEMBER_SERIALIZED "__PHP_Incomplete_Class_Serialized"
++#endif
++#ifndef MAGIC_MEMBER_ENUM_CASE
++# define MAGIC_MEMBER_ENUM_CASE "__PHP_Incomplete_Enum_Case"
++#endif
++
+ #if PHP_VERSION_ID < 70400
+ #define zend_get_properties_for(struc, purpose) Z_OBJPROP_P((struc))
+ 
+@@ -201,6 +210,7 @@ struct igbinary_serialize_data {
+ 	struct hash_si_ptr references; /**< Hash of already serialized potential references. (non-NULL uintptr_t => int32_t) */
+ 	uint32_t references_id;        /**< Number of things that the unserializer might think are references. >= length of references */
+ 	uint32_t string_count;         /**< Serialized string count, used for back referencing */
++	bool incomplete_classes_only;  /**< Write back the payloads kept by incomplete_classes_only unserialization. */
+ 
+ 	struct deferred_dtor_tracker deferred_dtor_tracker;  /**< refcounted objects and arrays to call dtor on after serializing. See i_zval_ptr_dtor */
+ };
+@@ -273,6 +283,10 @@ struct igbinary_unserialize_data {
+ #if PHP_VERSION_ID >= 70400
+ 	HashTable *ref_props; /**< objects&data for calls to __unserialize/__wakeup */
+ #endif
++	/** Keep every object as __PHP_Incomplete_Class without resolving its class: no autoloading,
++	 * no unserialize_callback_func, no __wakeup(), __unserialize() or Serializable::unserialize().
++	 * The objects serialize back to the same data. */
++	zend_bool incomplete_classes_only;
+ };
+ 
+ #define IGB_REF_VAL_2(igsd, n)	((igsd)->references[(n)])
+@@ -856,6 +870,17 @@ PHP_FUNCTION(igbinary_serialize) {
+ }
+ /* }}} */
+ #if HAVE_PHP_SESSION
++/* {{{ igbinary_session_in_upload_progress */
++/**
++ * php_session_rfc1867_update() decodes and re-encodes the session while the request body
++ * is parsed, before RINIT, when no user code may run (GHSA-w8hw-w9fx-j79w).
++ * Like the php-src serializers then (see php_session_unserialize_init() in ext/session/session.c),
++ * igbinary keeps every object as __PHP_Incomplete_Class.
++ */
++static zend_always_inline bool igbinary_session_in_upload_progress(void) {
++	return PS(rfc1867_progress) != NULL && !PG(modules_activated);
++}
++/* }}} */
+ /* {{{ Serializer encode function */
+ /**
+  * This provides a serializer encode function for PHP's session module (using igbinary),
+@@ -879,6 +904,7 @@ PS_SERIALIZER_ENCODE_FUNC(igbinary)
+ 		zend_error(E_WARNING, "igbinary_serialize: cannot init igsd");
+ 		return ZSTR_EMPTY_ALLOC();
+ 	}
++	igsd.incomplete_classes_only = igbinary_session_in_upload_progress();
+ 
+ 	igbinary_serialize_header(&igsd);
+ 
+@@ -925,6 +951,7 @@ PS_SERIALIZER_DECODE_FUNC(igbinary) {
+ 	if (igbinary_unserialize_data_init(&igsd) != 0) {
+ 		return FAILURE;
+ 	}
++	igsd.incomplete_classes_only = igbinary_session_in_upload_progress();
+ 
+ 	igsd.buffer = (const uint8_t *)val;
+ 	igsd.buffer_ptr = igsd.buffer;
+@@ -1022,6 +1049,7 @@ inline static int igbinary_serialize_data_init(struct igbinary_serialize_data *i
+ 	igsd->string_count = 0;
+ 	igsd->buffer = buffer;
+ 	igsd->scalar = scalar;
++	igsd->incomplete_classes_only = false;
+ 	if (scalar) {
+ 		igsd->compact_strings = 0;
+ 	} else {
+@@ -1407,6 +1435,7 @@ inline static int igbinary_serialize_array(struct igbinary_serialize_data *igsd,
+ 
+ 	zend_string *key;
+ 	zend_long key_index;
++	zend_ulong numeric_key;
+ 
+ 	z_original = z;
+ 	ZVAL_DEREF(z);
+@@ -1473,6 +1502,11 @@ inline static int igbinary_serialize_array(struct igbinary_serialize_data *igsd,
+ 		if (key == NULL) {
+ 			/* Key is numeric */
+ 			RETURN_1_IF_NON_ZERO(igbinary_serialize_long(igsd, key_index));
++		} else if (UNEXPECTED(incomplete_class && igsd->incomplete_classes_only)
++		 && ZEND_HANDLE_NUMERIC_STR(ZSTR_VAL(key), ZSTR_LEN(key), numeric_key)) {
++			/* The data may have been the array returned by __serialize(), whose integer keys became
++			 * property names: igbinary_unserialize_array() would not turn "0" back into 0 for __unserialize(). */
++			RETURN_1_IF_NON_ZERO(igbinary_serialize_long(igsd, (zend_long)numeric_key));
+ 		} else {
+ 			/* Key is string */
+ 			RETURN_1_IF_NON_ZERO(igbinary_serialize_string(igsd, key));
+@@ -1731,6 +1765,31 @@ inline static int igbinary_serialize_object_name(struct igbinary_serialize_data
+ 	return 0;
+ }
+ /* }}} */
++/* igbinary_serialize_object_ser_data {{{ */
++/** Serializes the Serializable::serialize() data of an object, after its class name. */
++static int igbinary_serialize_object_ser_data(struct igbinary_serialize_data *igsd, const unsigned char *serialized_data, size_t serialized_len) {
++	if (serialized_len <= 0xff) {
++		RETURN_1_IF_NON_ZERO(igbinary_serialize8_and_8(igsd, igbinary_type_object_ser8, (uint8_t)serialized_len));
++	} else if (serialized_len <= 0xffff) {
++		RETURN_1_IF_NON_ZERO(igbinary_serialize8_and_16(igsd, (uint8_t)igbinary_type_object_ser16, (uint16_t)serialized_len));
++	} else {
++#if SIZEOF_ZEND_LONG > 4
++		if (UNEXPECTED(serialized_len > 0xffffffff)) {
++			zend_error(E_WARNING, "igbinary_serialize_object_old_serializer_class: serialize() data 4GB or larger is not supported");
++			return 1;
++		}
++#endif
++		RETURN_1_IF_NON_ZERO(igbinary_serialize8_and_32(igsd, (uint8_t)igbinary_type_object_ser32, (uint32_t)serialized_len));
++	}
++
++	RETURN_1_IF_NON_ZERO(igbinary_serialize_resize(igsd, serialized_len));
++
++	memcpy(igsd->buffer + igsd->buffer_size, serialized_data, serialized_len);
++	igsd->buffer_size += serialized_len;
++
++	return 0;
++}
++/* }}} */
+ /* igbinary_serialize_object_old_serializer_class {{{ */
+ static ZEND_COLD int igbinary_serialize_object_old_serializer_class(struct igbinary_serialize_data* igsd, zval *z, zend_class_entry *ce) {
+ 	unsigned char *serialized_data = NULL;
+@@ -1738,36 +1797,10 @@ static ZEND_COLD int igbinary_serialize_object_old_serializer_class(struct igbin
+ 	int r = 0;
+ 
+ 	if (ce->serialize(z, &serialized_data, &serialized_len, (zend_serialize_data *)NULL) == SUCCESS && !EG(exception)) {
+-		if (UNEXPECTED(igbinary_serialize_object_name(igsd, ce->name) != 0)) {
+-			goto failure;
+-		}
+-
+-		if (serialized_len <= 0xff) {
+-			if (UNEXPECTED(igbinary_serialize8_and_8(igsd, igbinary_type_object_ser8, (uint8_t)serialized_len) != 0)) {
+-				goto failure;
+-			}
+-		} else if (serialized_len <= 0xffff) {
+-			if (UNEXPECTED(igbinary_serialize8_and_16(igsd, (uint8_t)igbinary_type_object_ser16, (uint16_t)serialized_len) != 0)) {
+-				goto failure;
+-			}
+-		} else {
+-#if SIZEOF_ZEND_LONG > 4
+-			if (UNEXPECTED(serialized_len > 0xffffffff)) {
+-				zend_error(E_WARNING, "igbinary_serialize_object_old_serializer_class: serialize() data 4GB or larger is not supported");
+-				goto failure;
+-			}
+-#endif
+-			if (UNEXPECTED(igbinary_serialize8_and_32(igsd, (uint8_t)igbinary_type_object_ser32, (uint32_t)serialized_len) != 0)) {
+-				goto failure;
+-			}
+-		}
+-
+-		if (UNEXPECTED(igbinary_serialize_resize(igsd, serialized_len))) {
++		if (UNEXPECTED(igbinary_serialize_object_name(igsd, ce->name) != 0)
++		 || UNEXPECTED(igbinary_serialize_object_ser_data(igsd, serialized_data, serialized_len) != 0)) {
+ 			goto failure;
+ 		}
+-
+-		memcpy(igsd->buffer + igsd->buffer_size, serialized_data, serialized_len);
+-		igsd->buffer_size += serialized_len;
+ 	} else if (EG(exception)) {
+ 		/* exception, return failure */
+ failure:
+@@ -1874,6 +1907,44 @@ inline static int igbinary_serialize_object_enum_case(struct igbinary_serialize_
+ }
+ #endif
+ /* }}} */
++/* {{{ igbinary_serialize_incomplete_class_raw */
++/** Writes back the Serializable payload or the enum case name kept by incomplete_classes_only unserialization.
++ * Returns 2 when the object keeps neither, so that it is serialized like any other incomplete class. */
++static int igbinary_serialize_incomplete_class_raw(struct igbinary_serialize_data *igsd, zval *z) {
++	PHP_CLASS_ATTRIBUTES;
++	HashTable *props = Z_OBJ_P(z)->properties;
++	zval *payload;
++	zval *case_name = NULL;
++	int r;
++
++	if (!props) {
++		return 2;
++	}
++	payload = zend_hash_str_find(props, MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED) - 1);
++	if (!payload || Z_TYPE_P(payload) != IS_STRING) {
++		payload = NULL;
++#if PHP_VERSION_ID >= 80100
++		case_name = zend_hash_str_find(props, MAGIC_MEMBER_ENUM_CASE, sizeof(MAGIC_MEMBER_ENUM_CASE) - 1);
++		if (!case_name || Z_TYPE_P(case_name) != IS_STRING) {
++			return 2;
++		}
++#else
++		return 2;
++#endif
++	}
++
++	PHP_SET_CLASS_ATTRIBUTES(z);
++	r = igbinary_serialize_object_name(igsd, class_name);
++	PHP_CLEANUP_CLASS_ATTRIBUTES();
++	RETURN_1_IF_NON_ZERO(r);
++
++	if (payload) {
++		return igbinary_serialize_object_ser_data(igsd, (const unsigned char *)Z_STRVAL_P(payload), Z_STRLEN_P(payload));
++	}
++	RETURN_1_IF_NON_ZERO(igbinary_serialize8(igsd, (uint8_t)igbinary_type_enum_case));
++	return igbinary_serialize_string(igsd, Z_STR_P(case_name));
++}
++/* }}} */
+ /* {{{ igbinary_serialize_object */
+ /** Serialize object.
+  * @see ext/standard/var.c
+@@ -1892,6 +1963,13 @@ inline static int igbinary_serialize_object(struct igbinary_serialize_data *igsd
+ 
+ 	ce = Z_OBJCE_P(z);
+ 
++	if (UNEXPECTED(igsd->incomplete_classes_only) && ce == PHP_IC_ENTRY) {
++		r = igbinary_serialize_incomplete_class_raw(igsd, z);
++		if (r != 2) {
++			return r;
++		}
++	}
++
+ 	if (IGBINARY_IS_NOT_SERIALIZABLE(ce)) {
+ 		zend_throw_exception_ex(NULL, 0, "Serialization of '%s' is not allowed", ZSTR_VAL(ce->name));
+ 		return 1;
+@@ -2081,6 +2159,7 @@ inline static int igbinary_unserialize_data_init(struct igbinary_unserialize_dat
+ #if PHP_VERSION_ID >= 70400
+ 	igsd->ref_props = NULL;
+ #endif
++	igsd->incomplete_classes_only = 0;
+ 
+ 	return 0;
+ }
+@@ -2823,6 +2902,17 @@ inline static int igbinary_unserialize_object_properties(struct igbinary_unseria
+ 				return 1;
+ 		}
+ 
++		/* The members incomplete_classes_only mode writes itself: data already carrying
++		 * one of them cannot be told apart from that bookkeeping, so it is rejected. */
++		if (UNEXPECTED(igsd->incomplete_classes_only)
++		 && (zend_string_equals_literal(key_str, MAGIC_MEMBER)
++		  || zend_string_equals_literal(key_str, MAGIC_MEMBER_SERIALIZED)
++		  || zend_string_equals_literal(key_str, MAGIC_MEMBER_ENUM_CASE))) {
++			zend_error(E_WARNING, "igbinary_unserialize_object_properties: property name %s is reserved for incomplete classes", ZSTR_VAL(key_str));
++			zend_string_release(key_str);
++			return 1;
++		}
++
+ 		/* first add key into array so references can properly and not stack allocated zvals */
+ 		/* Use NULL because inserting UNDEF into array does not add a new element */
+ 		ZVAL_NULL(&v);
+@@ -2943,15 +3033,17 @@ static ZEND_COLD int igbinary_unserialize_object_ser(struct igbinary_unserialize
+ 	int ret;
+ 	php_unserialize_data_t var_hash;
+ 
+-	if (ce->unserialize == NULL) {
+-		/* Should be impossible. */
+-		zend_error(E_WARNING, "Class %s has no unserializer", ZSTR_VAL(ce->name));
+-		return 1;
+-	}
++	if (!igsd->incomplete_classes_only) {
++		if (ce->unserialize == NULL) {
++			/* Should be impossible. */
++			zend_error(E_WARNING, "Class %s has no unserializer", ZSTR_VAL(ce->name));
++			return 1;
++		}
+ 
+-	if (IGBINARY_IS_NOT_UNSERIALIZABLE(ce)) {
+-		zend_throw_exception_ex(NULL, 0, "Unserialization of '%s' is not allowed", ZSTR_VAL(ce->name));
+-		return 1;
++		if (IGBINARY_IS_NOT_UNSERIALIZABLE(ce)) {
++			zend_throw_exception_ex(NULL, 0, "Unserialization of '%s' is not allowed", ZSTR_VAL(ce->name));
++			return 1;
++		}
+ 	}
+ 
+ 	if (t == igbinary_type_object_ser8) {
+@@ -2982,6 +3074,18 @@ static ZEND_COLD int igbinary_unserialize_object_ser(struct igbinary_unserialize
+ 		return 1;
+ 	}
+ 
++	if (UNEXPECTED(igsd->incomplete_classes_only)) {
++		/* The class was not resolved: keep the payload verbatim, so that the object serializes back to the same data. */
++		zval payload;
++
++		ZEND_ASSERT(ce == PHP_IC_ENTRY);
++		object_init_ex(z, ce);
++		ZVAL_STRINGL(&payload, (const char *)igsd->buffer_ptr, n);
++		zend_hash_str_update(Z_OBJPROP_P(z), MAGIC_MEMBER_SERIALIZED, sizeof(MAGIC_MEMBER_SERIALIZED) - 1, &payload);
++		igsd->buffer_ptr += n;
++		return 0;
++	}
++
+ 	PHP_VAR_UNSERIALIZE_INIT(var_hash);
+ 	ret = ce->unserialize(
+ 		z,
+@@ -3001,32 +3105,56 @@ static ZEND_COLD int igbinary_unserialize_object_ser(struct igbinary_unserialize
+ 	return 0;
+ }
+ /* }}} */
+-/* {{{ igbinary_unserialize_object_enum_case */
++/* {{{ igbinary_unserialize_enum_case_name */
+ #if PHP_VERSION_ID >= 80100
+-/** Unserializes object's property array. This is used to serialize objects implementing Serializable -interface. */
+-static int igbinary_unserialize_object_enum_case(struct igbinary_unserialize_data *igsd, zval *const z, zend_class_entry *ce) {
+-	if (UNEXPECTED(!(ce->ce_flags & ZEND_ACC_ENUM))) {
+-		zend_error(E_WARNING, "igbinary_unserialize_object_enum_case: Class '%s' is not an enum", ZSTR_VAL(ce->name));
+-		return 1;
+-	}
++static zend_string *igbinary_unserialize_enum_case_name(struct igbinary_unserialize_data *igsd) {
+ 	if (IGB_NEEDS_MORE_DATA(igsd, 1)) {
+ 		zend_error(E_WARNING, "igbinary_unserialize_object_enum_case: end-of-data");
+-		return 1;
++		return NULL;
+ 	}
+ 
+ 	enum igbinary_type t = (enum igbinary_type)igbinary_unserialize8(igsd);
+-	zend_string *case_name;
+ 	switch (t) {
+ 		case igbinary_type_string8:
+ 		case igbinary_type_string16:
+ 		case igbinary_type_string32:
+ 		case igbinary_type_string64:
+-			case_name = igbinary_unserialize_chararray(igsd, t, 1);
+-			break;
++			return igbinary_unserialize_chararray(igsd, t, 1);
+ 		default:
+-			case_name = igbinary_unserialize_string(igsd, t);
+-			break;
++			return igbinary_unserialize_string(igsd, t);
++	}
++}
++#endif
++/* }}} */
++/* {{{ igbinary_unserialize_incomplete_enum_case */
++#if PHP_VERSION_ID >= 80100
++/** Keeps an enum case as __PHP_Incomplete_Class in incomplete_classes_only mode, without loading the enum. */
++static int igbinary_unserialize_incomplete_enum_case(struct igbinary_unserialize_data *igsd, zval *const z, zend_string *class_name) {
++	zval case_zv;
++	zend_string *case_name = igbinary_unserialize_enum_case_name(igsd);
++	if (UNEXPECTED(!case_name)) {
++		return 1;
++	}
++
++	object_init_ex(z, PHP_IC_ENTRY);
++	php_store_class_name(z, class_name);
++	ZVAL_STR(&case_zv, case_name);
++	zend_hash_str_update(Z_OBJPROP_P(z), MAGIC_MEMBER_ENUM_CASE, sizeof(MAGIC_MEMBER_ENUM_CASE) - 1, &case_zv);
++
++	return 0;
++}
++#endif
++/* }}} */
++/* {{{ igbinary_unserialize_object_enum_case */
++#if PHP_VERSION_ID >= 80100
++/** Unserializes object's property array. This is used to serialize objects implementing Serializable -interface. */
++static int igbinary_unserialize_object_enum_case(struct igbinary_unserialize_data *igsd, zval *const z, zend_class_entry *ce) {
++	if (UNEXPECTED(!(ce->ce_flags & ZEND_ACC_ENUM))) {
++		zend_error(E_WARNING, "igbinary_unserialize_object_enum_case: Class '%s' is not an enum", ZSTR_VAL(ce->name));
++		return 1;
+ 	}
++
++	zend_string *case_name = igbinary_unserialize_enum_case_name(igsd);
+ 	if (UNEXPECTED(!case_name)) {
+ 		return 1;
+ 	}
+@@ -3095,6 +3223,13 @@ zend_always_inline static int igbinary_unserialize_object(struct igbinary_unseri
+ 		zval args[1];
+ 		const char* user_func_name;
+ 
++		if (UNEXPECTED(igsd->incomplete_classes_only)) {
++			/* Do not look the class up at all, so that no user code can run. */
++			incomplete_class = true;
++			ce = PHP_IC_ENTRY;
++			break;
++		}
++
+ 		/* Try to find class directly */
+ 		if (EXPECTED((ce = zend_lookup_class(class_name)) != NULL)) {
+ 			/* FIXME: lookup class may cause exception in load callback */
+@@ -3252,13 +3387,15 @@ zend_always_inline static int igbinary_unserialize_object(struct igbinary_unseri
+ 		}
+ 		case igbinary_type_enum_case:
+ #if PHP_VERSION_ID >= 80100
+-			if (UNEXPECTED(incomplete_class)) {
++			if (UNEXPECTED(igsd->incomplete_classes_only)) {
++				r = igbinary_unserialize_incomplete_enum_case(igsd, z, class_name);
++			} else if (UNEXPECTED(incomplete_class)) {
+ 				zend_error(E_WARNING, "igbinary_unserialize_object_enum_case: Class '%s' does not exist", ZSTR_VAL(class_name));
+-				zend_string_release(class_name);
+-				return 1;
++				r = 1;
++			} else {
++				r = igbinary_unserialize_object_enum_case(igsd, z, ce);
+ 			}
+ 			zend_string_release(class_name);
+-			r = igbinary_unserialize_object_enum_case(igsd, z, ce);
+ 			if (r) {
+ 				return r;
+ 			}
+diff --git a/tests/igbinary_session_upload_progress.phpt b/tests/igbinary_session_upload_progress.phpt
+new file mode 100644
+index 0000000..5aa6d71
+--- /dev/null
++++ b/tests/igbinary_session_upload_progress.phpt
+@@ -0,0 +1,169 @@
++--TEST--
++Session upload progress keeps igbinary session objects as __PHP_Incomplete_Class and runs no user code
++--SKIPIF--
++<?php
++if (!extension_loaded('session')) die('skip session extension not loaded');
++if (PHP_VERSION_ID < 80200) die('skip needs PHP 8.2 or later');
++if (!getenv('TEST_PHP_CGI_EXECUTABLE')) die('skip CGI not available');
++if (@ini_set('session.serialize_handler', 'igbinary') === false) die('skip igbinary session serializer not available');
++?>
++--INI--
++error_reporting=E_ALL & ~E_DEPRECATED
++--FILE--
++<?php
++/* php_session_rfc1867_update() decodes and writes back the session before RINIT
++ * (GHSA-w8hw-w9fx-j79w). No class may be looked up then: unserialize_callback_func
++ * would run, Serializable payloads could not be kept, and data carrying the
++ * incomplete class bookkeeping member could change class. */
++ob_start();
++
++class W { public $a = 1; public $woke = 0; function __wakeup() { $this->woke++; } }
++class Cust implements Serializable {
++    public $x = 'payload';
++    function serialize() { return $this->x; }
++    function unserialize($d) { $this->x = $d; }
++}
++class L {
++    public $p, $q;
++    function __construct($p = null, $q = null) { $this->p = $p; $this->q = $q; }
++    function __serialize(): array { return [$this->p, $this->q]; }
++    function __unserialize(array $d): void { [$this->p, $this->q] = [$d[0], $d[1]]; }
++}
++enum Suit: string { case Hearts = 'H'; }
++
++$dir = __DIR__ . '/igbinary_session_upload_progress';
++ at mkdir($dir);
++$worker = "$dir/worker.php";
++file_put_contents($worker, '<?php');
++
++ini_set('session.save_path', $dir);
++ini_set('session.use_cookies', '0');
++ini_set('session.cache_limiter', '');
++ini_set('session.serialize_handler', 'igbinary');
++
++function read_record($sid) {
++    session_id($sid);
++    session_start();
++    unset($_SESSION['upload_progress_key']);
++    $s = serialize($_SESSION);
++    session_abort();
++    return $s;
++}
++
++$shared = new W;
++$self = new stdClass;
++$self->self = $self;
++$self->{'0'} = 'zero';
++$storage = new SplObjectStorage;
++$storage[$shared] = 'data';
++$array = ['k' => 1];
++session_id('mixed');
++session_start();
++$_SESSION = [
++    'w' => $shared, 'w2' => $shared,
++    'c' => new Cust, 'l' => new L(10, [20, 30]),
++    'ao' => new ArrayObject([5 => 'five', 'k' => 'v']),
++    'dt' => new DateTime('2026-10-22 12:00:00', new DateTimeZone('UTC')),
++    'e1' => Suit::Hearts, 'e2' => Suit::Hearts, 'st' => $storage, 'self' => $self,
++];
++$_SESSION['r1'] = &$array;
++$_SESSION['r2'] = &$array;
++session_write_close();
++
++$reserved = new stdClass;
++$reserved->{'__PHP_Incomplete_Class_Name'} = 'W';
++session_id('reserved');
++session_start();
++$_SESSION['x'] = $reserved;
++session_write_close();
++
++$before = read_record('mixed');
++
++/* php-cgi is started with -n, so the shared session and igbinary modules have to be named again. */
++$cmd = [getenv('TEST_PHP_CGI_EXECUTABLE'), '-n', '-q'];
++foreach (['session', 'igbinary'] as $ext) {
++    foreach ([ini_get('extension_dir'), PHP_EXTENSION_DIR] as $extdir) {
++        if (file_exists("$extdir/$ext." . PHP_SHLIB_SUFFIX)) {
++            array_push($cmd, '-d', "extension=$extdir/$ext." . PHP_SHLIB_SUFFIX);
++            break;
++        }
++    }
++}
++$ini = [
++    'error_reporting' => 'E_ALL', 'display_errors' => '1', 'html_errors' => '0',
++    'unserialize_callback_func' => 'var_dump',
++    'session.save_path' => $dir, 'session.use_strict_mode' => '0', 'session.use_cookies' => '0',
++    'session.cache_limiter' => '', 'session.serialize_handler' => 'igbinary',
++    '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',
++];
++foreach ($ini as $name => $value) {
++    array_push($cmd, '-d', "$name=$value");
++}
++array_push($cmd, '-f', $worker);
++
++function upload($cmd, $dir, $worker, $sid) {
++    $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";
++    $env = getenv();
++    $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'] = '';
++    $proc = proc_open($cmd, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $dir, $env);
++    fwrite($pipes[0], $body);
++    fclose($pipes[0]);
++    $out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
++    fclose($pipes[1]);
++    fclose($pipes[2]);
++    proc_close($proc);
++    return $out;
++}
++
++$out = upload($cmd, $dir, $worker, 'mixed');
++$file = file_exists("$dir/sess_mixed") ? file_get_contents("$dir/sess_mixed") : '';
++$results = [
++    'user code ran' => strpos($out, 'string(') !== false,
++    'warnings' => substr_count($out, 'Warning:'),
++    'progress recorded' => strpos($file, 'upload_progress_key') !== false,
++    'data unchanged' => read_record('mixed') === $before,
++];
++$out = upload($cmd, $dir, $worker, 'reserved');
++$results['reserved member refused'] = strpos($out, 'Session has been destroyed') !== false;
++
++echo ob_get_clean();
++var_dump($results);
++?>
++--CLEAN--
++<?php
++$dir = __DIR__ . '/igbinary_session_upload_progress';
++ at unlink("$dir/worker.php");
++foreach (glob("$dir/sess_*") as $f) {
++    unlink($f);
++}
++ at rmdir($dir);
++?>
++--EXPECT--
++array(5) {
++  ["user code ran"]=>
++  bool(false)
++  ["warnings"]=>
++  int(0)
++  ["progress recorded"]=>
++  bool(true)
++  ["data unchanged"]=>
++  bool(true)
++  ["reserved member refused"]=>
++  bool(true)
++}
diff --git a/shared-session.patch b/shared-session.patch
new file mode 100644
index 0000000..e484f93
--- /dev/null
+++ b/shared-session.patch
@@ -0,0 +1,76 @@
+Register the igbinary session serializer when session is a shared extension.
+Without it session.serialize_handler=igbinary is "cannot be found": PLD builds
+session shared, so php_config.h defines COMPILE_DL_SESSION. session.so exports
+the serializer API and is loaded first (ZEND_MOD_REQUIRED, 00_session.ini).
+
+diff --git a/src/php7/igbinary.c b/src/php7/igbinary.c
+index 17eee3c..9a1b72e 100644
+--- a/src/php7/igbinary.c
++++ b/src/php7/igbinary.c
+@@ -28,7 +28,7 @@
+ #include "Zend/zend_enum.h"
+ #endif
+ 
+-#if HAVE_PHP_SESSION && !defined(COMPILE_DL_SESSION)
++#if HAVE_PHP_SESSION
+ # include "ext/session/php_session.h"
+ #endif
+ 
+@@ -69,10 +69,10 @@
+ #include "zend_alloc.h"
+ #include "igbinary_zend_hash.h"
+ 
+-#if HAVE_PHP_SESSION && !defined(COMPILE_DL_SESSION)
++#if HAVE_PHP_SESSION
+ /** Session serializer function prototypes. */
+ PS_SERIALIZER_FUNCS(igbinary);
+-#endif /* HAVE_PHP_SESSION && !defined(COMPILE_DL_SESSION) */
++#endif /* HAVE_PHP_SESSION */
+ 
+ #if defined(HAVE_APCU_SUPPORT)
+ /** Apc serializer function prototypes */
+@@ -353,7 +353,7 @@ zend_function_entry igbinary_functions[] = {
+ /* {{{ igbinary dependencies */
+ static const zend_module_dep igbinary_module_deps[] = {
+ 	ZEND_MOD_REQUIRED("standard")
+-#if HAVE_PHP_SESSION && !defined(COMPILE_DL_SESSION)
++#if HAVE_PHP_SESSION
+ 	ZEND_MOD_REQUIRED("session")
+ #endif
+ #if defined(HAVE_APCU_SUPPORT)
+@@ -409,7 +409,7 @@ PHP_MINIT_FUNCTION(igbinary) {
+ 	(void)module_number;
+ 	ZEND_INIT_MODULE_GLOBALS(igbinary, php_igbinary_init_globals, NULL);
+ 
+-#if HAVE_PHP_SESSION && !defined(COMPILE_DL_SESSION)
++#if HAVE_PHP_SESSION
+ 	php_session_register_serializer("igbinary",
+ 		PS_SERIALIZER_ENCODE_NAME(igbinary),
+ 		PS_SERIALIZER_DECODE_NAME(igbinary));
+@@ -460,7 +460,7 @@ PHP_MINFO_FUNCTION(igbinary) {
+ #else
+ 	php_info_print_table_row(2, "igbinary APCu serializer ABI", "no");
+ #endif
+-#if HAVE_PHP_SESSION && !defined(COMPILE_DL_SESSION)
++#if HAVE_PHP_SESSION
+ 	php_info_print_table_row(2, "igbinary session support", "yes");
+ #else
+ 	php_info_print_table_row(2, "igbinary session support", "no");
+@@ -855,7 +855,7 @@ PHP_FUNCTION(igbinary_serialize) {
+ 	efree(string);
+ }
+ /* }}} */
+-#if HAVE_PHP_SESSION && !defined(COMPILE_DL_SESSION)
++#if HAVE_PHP_SESSION
+ /* {{{ Serializer encode function */
+ /**
+  * This provides a serializer encode function for PHP's session module (using igbinary),
+@@ -974,7 +974,7 @@ deinit:
+ 	return SUCCESS;
+ }
+ /* }}} */
+-#endif /* HAVE_PHP_SESSION && !defined(COMPILE_DL_SESSION) */
++#endif /* HAVE_PHP_SESSION */
+ 
+ #if defined(HAVE_APCU_SUPPORT)
+ /* {{{ apc_serialize function */
diff --git a/unserialize-long64-32bit.patch b/unserialize-long64-32bit.patch
new file mode 100644
index 0000000..8eac7ed
--- /dev/null
+++ b/unserialize-long64-32bit.patch
@@ -0,0 +1,20 @@
+On 32-bit zend_long a 64-bit integer was read without checking that 8 bytes remain.
+
+diff --git a/src/php7/igbinary.c b/src/php7/igbinary.c
+index 23c0fe4..664da40 100644
+--- a/src/php7/igbinary.c
++++ b/src/php7/igbinary.c
+@@ -2352,12 +2352,11 @@ inline static int igbinary_unserialize_long(struct igbinary_unserialize_data *ig
+ 		*ret = (zend_long)(t == igbinary_type_long32n ? -1 : 1) * tmp32;
+ 	} else {
+ 		ZEND_ASSERT(t == igbinary_type_long64p || t == igbinary_type_long64n);
+-#if SIZEOF_ZEND_LONG == 8
+ 		if (IGB_NEEDS_MORE_DATA(igsd, 8)) {
+ 			zend_error(E_WARNING, "igbinary_unserialize_long: end-of-data");
+ 			return 1;
+ 		}
+-
++#if SIZEOF_ZEND_LONG == 8
+ 		/* check for boundaries (perform only one comparison in common case) */
+ 		tmp64 = igbinary_unserialize64(igsd);
+ 		if (UNEXPECTED(tmp64 >= 0x8000000000000000 && (tmp64 > 0x8000000000000000 || t == igbinary_type_long64p))) {
diff --git a/unserialize-truncated.patch b/unserialize-truncated.patch
new file mode 100644
index 0000000..c9d27be
--- /dev/null
+++ b/unserialize-truncated.patch
@@ -0,0 +1,24 @@
+Truncated data after the class name of an object with __unserialize() left the
+__unserialize() argument uninitialized, and deinit called zval_ptr_dtor() on it.
+
+diff --git a/src/php7/igbinary.c b/src/php7/igbinary.c
+index 6747818..23c0fe4 100644
+--- a/src/php7/igbinary.c
++++ b/src/php7/igbinary.c
+@@ -3335,9 +3335,16 @@ zend_always_inline static int igbinary_unserialize_object(struct igbinary_unseri
+ 					zval param;
+ 					int result;
+ 					zend_string_release(class_name);
++					ZVAL_UNDEF(&param);
+ 					result = igbinary_unserialize_array(igsd, t, &param, 0, false);
+ 					ZVAL_DEREF(z);
+ 					ZEND_ASSERT(Z_TYPE_P(z) == IS_OBJECT);
++					if (UNEXPECTED(Z_TYPE(param) != IS_ARRAY)) {
++						/* Failed before creating the array: there is nothing to pass to __unserialize(),
++						 * which will not be called, so the destructor must not run either. */
++						GC_ADD_FLAGS(Z_OBJ_P(z), IS_OBJ_DESTRUCTOR_CALLED);
++						return 1;
++					}
+ 					igsd_defer_unserialize(igsd, Z_OBJ_P(z), param);
+ 					return result;
+ 				}
================================================================

---- gitweb:

http://git.pld-linux.org/gitweb.cgi/packages/php-pecl-igbinary.git/commitdiff/4c1e9fcb84857cb94cbd660127c5d0424d3cc657



More information about the pld-cvs-commit mailing list