Fixed PHP SEGV when constructing messages from a destructor.

This also fixes the keep_descriptor_pool_after_request option, which
was not working properly.
diff --git a/kokoro/linux/php_all/build.sh b/kokoro/linux/php_all/build.sh
index 1ee79ad..cfcab00 100755
--- a/kokoro/linux/php_all/build.sh
+++ b/kokoro/linux/php_all/build.sh
@@ -15,6 +15,9 @@
 docker run $(test -t 0 && echo "-it") -v$PWD:/workspace gcr.io/protobuf-build/php/linux:7.4.18-dbg-14a06550010c0649bf69b6c9b803c1ca609bbb6d "composer test && composer test_c"
 docker run $(test -t 0 && echo "-it") -v$PWD:/workspace gcr.io/protobuf-build/php/linux:8.0.5-dbg-14a06550010c0649bf69b6c9b803c1ca609bbb6d "composer test && composer test_c"
 
+# Run specialized memory leak & multirequest tests.
+docker run $(test -t 0 && echo "-it") -v$PWD:/workspace gcr.io/protobuf-build/php/linux:8.0.5-dbg-14a06550010c0649bf69b6c9b803c1ca609bbb6d "composer test_c && tests/multirequest.sh && tests/memory_leak_test.sh"
+
 # Most of our tests use a debug build of PHP, but we do one build against an opt
 # php just in case that surfaces anything unexpected.
 docker run $(test -t 0 && echo "-it") -v$PWD:/workspace gcr.io/protobuf-build/php/linux:8.0.5-14a06550010c0649bf69b6c9b803c1ca609bbb6d "composer test && composer test_c"
diff --git a/php/ext/google/protobuf/def.c b/php/ext/google/protobuf/def.c
index 88c201f..6662383 100644
--- a/php/ext/google/protobuf/def.c
+++ b/php/ext/google/protobuf/def.c
@@ -731,44 +731,25 @@
 }
 
 /**
- * Object handler to create an DescriptorPool.
- */
-static zend_object* DescriptorPool_create(zend_class_entry *class_type) {
-  DescriptorPool *intern = emalloc(sizeof(DescriptorPool));
-  zend_object_std_init(&intern->std, class_type);
-  intern->std.handlers = &DescriptorPool_object_handlers;
-  intern->symtab = upb_symtab_new();
-  // Skip object_properties_init(), we don't allow derived classes.
-  return &intern->std;
-}
-
-/**
  * Object handler to free an DescriptorPool.
  */
 static void DescriptorPool_destructor(zend_object* obj) {
   DescriptorPool* intern = (DescriptorPool*)obj;
-  if (intern->symtab) {
-    upb_symtab_free(intern->symtab);
-  }
-  intern->symtab = NULL;
+
+  // We can't free our underlying symtab here, because user code may create
+  // messages from destructors that will refer to it. The symtab will be freed
+  // by our RSHUTDOWN() handler in protobuf.c
+
   zend_object_std_dtor(&intern->std);
 }
 
 void DescriptorPool_CreateWithSymbolTable(zval *zv, upb_symtab *symtab) {
-  ZVAL_OBJ(zv, DescriptorPool_create(DescriptorPool_class_entry));
+  DescriptorPool *intern = emalloc(sizeof(DescriptorPool));
+  zend_object_std_init(&intern->std, DescriptorPool_class_entry);
+  intern->std.handlers = &DescriptorPool_object_handlers;
+  intern->symtab = symtab;
 
-  if (symtab) {
-    DescriptorPool *intern = GetPool(zv);
-    upb_symtab_free(intern->symtab);
-    intern->symtab = symtab;
-  }
-}
-
-upb_symtab *DescriptorPool_Steal(zval *zv) {
-  DescriptorPool *intern = GetPool(zv);
-  upb_symtab *ret = intern->symtab;
-  intern->symtab = NULL;
-  return ret;
+  ZVAL_OBJ(zv, &intern->std);
 }
 
 upb_symtab *DescriptorPool_GetSymbolTable() {
@@ -1120,7 +1101,7 @@
                    DescriptorPool_methods);
   DescriptorPool_class_entry = zend_register_internal_class(&tmp_ce);
   DescriptorPool_class_entry->ce_flags |= ZEND_ACC_FINAL;
-  DescriptorPool_class_entry->create_object = DescriptorPool_create;
+  DescriptorPool_class_entry->create_object = CreateHandler_ReturnNull;
   h = &DescriptorPool_object_handlers;
   memcpy(h, &std_object_handlers, sizeof(zend_object_handlers));
   h->dtor_obj = DescriptorPool_destructor;
diff --git a/php/ext/google/protobuf/def.h b/php/ext/google/protobuf/def.h
index e705642..ed944ab 100644
--- a/php/ext/google/protobuf/def.h
+++ b/php/ext/google/protobuf/def.h
@@ -38,15 +38,10 @@
 // Initializes the Def module, which defines all of the descriptor classes.
 void Def_ModuleInit();
 
-// Creates a new DescriptorPool to wrap the given symtab. The DescriptorPool
-// takes ownership of the given symtab. If symtab is NULL, the DescriptorPool
-// will create an empty symtab instead.
+// Creates a new DescriptorPool to wrap the given symtab, which must not be
+// NULL.
 void DescriptorPool_CreateWithSymbolTable(zval *zv, upb_symtab *symtab);
 
-// Given a zval representing a DescriptorPool, steals and returns its symtab,
-// which is now owned by the caller.
-upb_symtab *DescriptorPool_Steal(zval *zv);
-
 upb_symtab *DescriptorPool_GetSymbolTable();
 
 // Returns true if the global descriptor pool already has the given filename.
diff --git a/php/ext/google/protobuf/protobuf.c b/php/ext/google/protobuf/protobuf.c
index 888b434..625008f 100644
--- a/php/ext/google/protobuf/protobuf.c
+++ b/php/ext/google/protobuf/protobuf.c
@@ -66,7 +66,7 @@
   // to rebuild it from scratch. When keep_descriptor_pool_after_request==true,
   // we steal the upb_symtab from the global DescriptorPool object just before
   // destroying it.
-  upb_symtab *saved_symtab;
+  upb_symtab *global_symtab;
 
   // Object cache (see interface in protobuf.h).
   HashTable object_cache;
@@ -82,6 +82,13 @@
   HashTable descriptors;
 ZEND_END_MODULE_GLOBALS(protobuf)
 
+void free_protobuf_globals(zend_protobuf_globals *globals) {
+  zend_hash_destroy(&globals->name_msg_cache);
+  zend_hash_destroy(&globals->name_enum_cache);
+  upb_symtab_free(globals->global_symtab);
+  globals->global_symtab = NULL;
+}
+
 ZEND_DECLARE_MODULE_GLOBALS(protobuf)
 
 const zval *get_generated_pool() {
@@ -146,14 +153,14 @@
 //     discouraged by the documentation: https://serverfault.com/a/231660
 
 static PHP_GSHUTDOWN_FUNCTION(protobuf) {
-  if (protobuf_globals->saved_symtab) {
-    upb_symtab_free(protobuf_globals->saved_symtab);
+  if (protobuf_globals->global_symtab) {
+    free_protobuf_globals(protobuf_globals);
   }
 }
 
 static PHP_GINIT_FUNCTION(protobuf) {
   ZVAL_NULL(&protobuf_globals->generated_pool);
-  protobuf_globals->saved_symtab = NULL;
+  protobuf_globals->global_symtab = NULL;
 }
 
 /**
@@ -164,12 +171,15 @@
 static PHP_RINIT_FUNCTION(protobuf) {
   // Create the global generated pool.
   // Reuse the symtab (if any) left to us by the last request.
-  upb_symtab *symtab = PROTOBUF_G(saved_symtab);
+  upb_symtab *symtab = PROTOBUF_G(global_symtab);
+  if (!symtab) {
+    PROTOBUF_G(global_symtab) = symtab = upb_symtab_new();
+    zend_hash_init(&PROTOBUF_G(name_msg_cache), 64, NULL, NULL, 0);
+    zend_hash_init(&PROTOBUF_G(name_enum_cache), 64, NULL, NULL, 0);
+  }
   DescriptorPool_CreateWithSymbolTable(&PROTOBUF_G(generated_pool), symtab);
 
   zend_hash_init(&PROTOBUF_G(object_cache), 64, NULL, NULL, 0);
-  zend_hash_init(&PROTOBUF_G(name_msg_cache), 64, NULL, NULL, 0);
-  zend_hash_init(&PROTOBUF_G(name_enum_cache), 64, NULL, NULL, 0);
   zend_hash_init(&PROTOBUF_G(descriptors), 64, NULL, ZVAL_PTR_DTOR, 0);
 
   return SUCCESS;
@@ -182,15 +192,12 @@
  */
 static PHP_RSHUTDOWN_FUNCTION(protobuf) {
   // Preserve the symtab if requested.
-  if (PROTOBUF_G(keep_descriptor_pool_after_request)) {
-    zval *zv = &PROTOBUF_G(generated_pool);
-    PROTOBUF_G(saved_symtab) = DescriptorPool_Steal(zv);
+  if (!PROTOBUF_G(keep_descriptor_pool_after_request)) {
+    free_protobuf_globals(ZEND_MODULE_GLOBALS_BULK(protobuf));
   }
 
   zval_dtor(&PROTOBUF_G(generated_pool));
   zend_hash_destroy(&PROTOBUF_G(object_cache));
-  zend_hash_destroy(&PROTOBUF_G(name_msg_cache));
-  zend_hash_destroy(&PROTOBUF_G(name_enum_cache));
   zend_hash_destroy(&PROTOBUF_G(descriptors));
 
   return SUCCESS;
@@ -296,7 +303,7 @@
 
 PHP_INI_BEGIN()
 STD_PHP_INI_ENTRY("protobuf.keep_descriptor_pool_after_request", "0",
-                  PHP_INI_SYSTEM, OnUpdateBool,
+                  PHP_INI_ALL, OnUpdateBool,
                   keep_descriptor_pool_after_request, zend_protobuf_globals,
                   protobuf_globals)
 PHP_INI_END()
diff --git a/php/tests/memory_leak_test.php b/php/tests/memory_leak_test.php
index 1f2d095..ddb8491 100644
--- a/php/tests/memory_leak_test.php
+++ b/php/tests/memory_leak_test.php
@@ -2,9 +2,22 @@
 
 # phpunit has memory leak by itself. Thus, it cannot be used to test memory leak.
 
+class HasDestructor
+{
+  function __construct() {
+    $this->foo = $this;
+  }
+
+  function __destruct() {
+    new Foo\TestMessage();
+  }
+}
+
 require_once('../vendor/autoload.php');
 require_once('test_util.php');
 
+$has_destructor = new HasDestructor();
+
 use Google\Protobuf\Internal\RepeatedField;
 use Google\Protobuf\Internal\GPBType;
 use Foo\TestAny;
diff --git a/php/tests/memory_leak_test.sh b/php/tests/memory_leak_test.sh
new file mode 100755
index 0000000..0b66957
--- /dev/null
+++ b/php/tests/memory_leak_test.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+cd $(dirname $0)
+
+set -ex
+
+PORT=12345
+TIMEOUT=10
+
+./compile_extension.sh
+
+run_test() {
+  echo
+  echo "Running memory leak test, args: $@"
+
+  EXTRA_ARGS=""
+  ARGS="-d xdebug.profiler_enable=0 -d display_errors=on -dextension=../ext/google/protobuf/modules/protobuf.so"
+
+  for i in "$@"; do
+    case $i in
+      --keep_descriptors)
+        EXTRA_ARGS=-dprotobuf.keep_descriptor_pool_after_request=1
+        shift
+        ;;
+    esac
+  done
+
+  export ZEND_DONT_UNLOAD_MODULES=1
+  export USE_ZEND_ALLOC=0
+
+  if valgrind --error-exitcode=1 --leak-check=full --show-leak-kinds=all --errors-for-leak-kinds=all --suppressions=valgrind.supp --num-callers=100 php $ARGS $EXTRA_ARGS memory_leak_test.php; then
+    echo "Memory leak test SUCCEEDED"
+  else
+    echo "Memory leak test FAILED"
+    exit 1
+  fi
+}
+
+run_test
+run_test --keep_descriptors
diff --git a/php/tests/multirequest.sh b/php/tests/multirequest.sh
index ec4a1ae..24db507 100755
--- a/php/tests/multirequest.sh
+++ b/php/tests/multirequest.sh
@@ -5,28 +5,58 @@
 set -e
 
 PORT=12345
+TIMEOUT=10
 
 ./compile_extension.sh
 
-nohup php -d protobuf.keep_descriptor_pool_after_request=1 -dextension=../ext/google/protobuf/modules/protobuf.so -S localhost:$PORT multirequest.php 2>&1 &
+run_test() {
+  echo
+  echo "Running multirequest test, args: $@"
 
-sleep 1
+  RUN_UNDER=""
+  EXTRA_ARGS=""
+  ARGS="-d xdebug.profiler_enable=0 -d display_errors=on -dextension=../ext/google/protobuf/modules/protobuf.so"
 
-wget http://localhost:$PORT/multirequest.result -O multirequest.result
-wget http://localhost:$PORT/multirequest.result -O multirequest.result
+  for i in "$@"; do
+    case $i in
+      --valgrind)
+        RUN_UNDER="valgrind --error-exitcode=1"
+        shift
+        ;;
+      --keep_descriptors)
+        EXTRA_ARGS=-dprotobuf.keep_descriptor_pool_after_request=1
+        shift
+        ;;
+    esac
+  done
 
-pushd ../ext/google/protobuf
-phpize --clean
-popd
+  export ZEND_DONT_UNLOAD_MODULES=1
+  export USE_ZEND_ALLOC=0
+  rm nohup.out
+  nohup $RUN_UNDER php $ARGS $EXTRA_ARGS -S localhost:$PORT multirequest.php >nohup.out 2>&1 &
+  PID=$!
 
-PID=`ps | grep "php" | awk '{print $1}'`
-echo $PID
+  if ! timeout $TIMEOUT bash -c "until echo > /dev/tcp/localhost/$PORT; do sleep 0.1; done" > /dev/null 2>&1; then
+    echo "Server failed to come up after $TIMEOUT seconds"
+    cat nohup.out
+    exit 1
+  fi
 
-if [[ -z "$PID" ]]
-then
-  echo "Failed"
-  exit 1
-else
-  kill $PID
-  echo "Succeeded"
-fi
+  seq 2 | xargs -I{} wget -nv http://localhost:$PORT/multirequest.result -O multirequest{}.result
+  REQUESTS_SUCCEEDED=$?
+
+
+  if kill $PID > /dev/null 2>&1 && [[ $REQUESTS_SUCCEEDED == "0" ]]; then
+    wait
+    echo "Multirequest test SUCCEEDED"
+  else
+    echo "Multirequest test FAILED"
+    cat nohup.out
+    exit 1
+  fi
+}
+
+run_test
+run_test --keep_descriptors
+run_test --valgrind
+run_test --valgrind --keep_descriptors
diff --git a/php/tests/valgrind.supp b/php/tests/valgrind.supp
index e83b0a3..8d31466 100644
--- a/php/tests/valgrind.supp
+++ b/php/tests/valgrind.supp
@@ -10,3 +10,24 @@
    obj:/usr/bin/php7.3
    fun:__scandir64_tail
 }
+
+{
+   PHP_ModuleLoadingLeaks
+   Memcheck:Leak
+   ...
+   fun:php_module_startup
+}
+
+{
+   PHP_ModuleLoadingLeaks
+   Memcheck:Leak
+   ...
+   fun:php_module_startup
+}
+
+{
+   PHP_ModuleLoadingLeaks2
+   Memcheck:Leak
+   ...
+   fun:php_load_shlib
+}