From ef5476820de9c419d58c8249b351e30c8d892982 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Tue, 13 Sep 2022 09:49:30 +0100
Subject: [PATCH] Issue #2531564 by neclimdul, andypost, chx, cburschka,
 Spokje, pdenooijer, Charlie ChX Negyesi, znerol, dawehner, longwave, Fabianx,
 geek-merlin, catch, alexpott: Fix leaky and brittle container serialization
 solution

---
 core/lib/Drupal.php                           |  4 +-
 .../DependencyInjection/Container.php         |  8 +--
 .../ContainerInterface.php                    | 42 ++++++++++++
 .../Dumper/OptimizedPhpArrayDumper.php        |  1 +
 .../ServiceIdHashTrait.php                    | 37 ++++++++++
 .../lib/Drupal/Core/Config/ConfigImporter.php | 14 ++--
 core/lib/Drupal/Core/CoreServiceProvider.php  |  2 -
 .../DependencySerializationTraitPass.php      | 34 ++--------
 .../Core/DependencyInjection/Container.php    | 12 ----
 .../DependencyInjection/ContainerBuilder.php  | 11 ++-
 .../DependencySerializationTrait.php          | 68 ++++++++++++-------
 core/lib/Drupal/Core/DrupalKernel.php         | 46 ++++++++++++-
 .../lib/Drupal/Core/DrupalKernelInterface.php |  5 ++
 core/lib/Drupal/Core/Test/TestKernel.php      | 20 ++++++
 .../tests/src/Kernel/DecoratedServiceTest.php |  8 ++-
 .../tests/src/Unit/ViewUIObjectTest.php       |  4 --
 .../Core/Entity/EntityTypeTest.php            | 64 +++++++++++++++++
 .../DependencyInjection/ContainerTest.php     | 16 ++++-
 .../Dumper/OptimizedPhpArrayDumperTest.php    | 43 +++++++++---
 .../Tests/Component/DrupalComponentTest.php   |  4 +-
 .../Entity/ConfigEntityBaseUnitTest.php       |  6 +-
 .../ContainerBuilderTest.php                  | 10 ---
 .../DependencyInjection/ContainerTest.php     | 12 ----
 .../DependencySerializationTest.php           | 31 +--------
 .../Core/DrupalKernel/DrupalKernelTest.php    | 12 ++++
 .../Tests/Core/Entity/EntityTypeTest.php      | 36 ----------
 .../Tests/Core/Plugin/ContextHandlerTest.php  |  4 +-
 .../Core/TempStore/SharedTempStoreTest.php    | 10 ++-
 28 files changed, 359 insertions(+), 205 deletions(-)
 create mode 100644 core/lib/Drupal/Component/DependencyInjection/ContainerInterface.php
 create mode 100644 core/lib/Drupal/Component/DependencyInjection/ServiceIdHashTrait.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Entity/EntityTypeTest.php

diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index ccd2d848147c..1d0a2bc2bacf 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -126,7 +126,7 @@ class Drupal {
   /**
    * The currently active container object, or NULL if not initialized yet.
    *
-   * @var \Symfony\Component\DependencyInjection\ContainerInterface|null
+   * @var \Drupal\Component\DependencyInjection\ContainerInterface|null
    */
   protected static $container;
 
@@ -150,7 +150,7 @@ public static function unsetContainer() {
   /**
    * Returns the currently active global container.
    *
-   * @return \Symfony\Component\DependencyInjection\ContainerInterface
+   * @return \Drupal\Component\DependencyInjection\ContainerInterface
    *
    * @throws \Drupal\Core\DependencyInjection\ContainerNotInitializedException
    */
diff --git a/core/lib/Drupal/Component/DependencyInjection/Container.php b/core/lib/Drupal/Component/DependencyInjection/Container.php
index 235cceab412c..639553df8633 100644
--- a/core/lib/Drupal/Component/DependencyInjection/Container.php
+++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\Component\DependencyInjection;
 
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\Exception\LogicException;
 use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
 use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@@ -46,6 +45,8 @@
  */
 class Container implements ContainerInterface, ResetInterface {
 
+  use ServiceIdHashTrait;
+
   /**
    * The parameters of the container.
    *
@@ -536,10 +537,7 @@ protected function getParameterAlternatives($name) {
   }
 
   /**
-   * Gets all defined service IDs.
-   *
-   * @return array
-   *   An array of all defined service IDs.
+   * {@inheritdoc}
    */
   public function getServiceIds() {
     return array_keys($this->serviceDefinitions + $this->services);
diff --git a/core/lib/Drupal/Component/DependencyInjection/ContainerInterface.php b/core/lib/Drupal/Component/DependencyInjection/ContainerInterface.php
new file mode 100644
index 000000000000..e6c5ea72d455
--- /dev/null
+++ b/core/lib/Drupal/Component/DependencyInjection/ContainerInterface.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\Component\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\ContainerInterface as BaseContainerInterface;
+
+/**
+ * The interface for Drupal service container classes.
+ *
+ * This interface extends Symfony's ContainerInterface and adds methods for
+ * managing mappings of instantiated services to its IDs.
+ */
+interface ContainerInterface extends BaseContainerInterface {
+
+  /**
+   * Gets all defined service IDs.
+   *
+   * @return array
+   *   An array of all defined service IDs.
+   */
+  public function getServiceIds();
+
+  /**
+   * Collect a mapping between service to ids.
+   *
+   * @return array
+   *   Service ids keyed by a unique hash.
+   */
+  public function getServiceIdMappings(): array;
+
+  /**
+   * Generate a unique hash for a service object.
+   *
+   * @param object $object
+   *   Object needing a unique hash.
+   *
+   * @return string
+   *   A unique hash identifying the object.
+   */
+  public function generateServiceIdHash(object $object): string;
+
+}
diff --git a/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
index 3f3d2455e41d..a390a3710776 100644
--- a/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
+++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
@@ -430,6 +430,7 @@ protected function dumpValue($value) {
     elseif (is_object($value)) {
       // Drupal specific: Instantiated objects have a _serviceId parameter.
       if (isset($value->_serviceId)) {
+        @trigger_error('_serviceId is deprecated in drupal:9.5.0 and is removed from drupal:11.0.0. Use \Drupal\Core\DrupalKernelInterface::getServiceIdMapping() instead. See https://www.drupal.org/node/3292540', E_USER_DEPRECATED);
         return $this->getReferenceCall($value->_serviceId);
       }
       throw new RuntimeException('Unable to dump a service container if a parameter is an object without _serviceId.');
diff --git a/core/lib/Drupal/Component/DependencyInjection/ServiceIdHashTrait.php b/core/lib/Drupal/Component/DependencyInjection/ServiceIdHashTrait.php
new file mode 100644
index 000000000000..d53f9cb650d7
--- /dev/null
+++ b/core/lib/Drupal/Component/DependencyInjection/ServiceIdHashTrait.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\Component\DependencyInjection;
+
+/**
+ * A trait for service id hashing implementations.
+ *
+ * Handles delayed cache tag invalidations.
+ */
+trait ServiceIdHashTrait {
+
+  /**
+   * Implements \Drupal\Component\DependencyInjection\ContainerInterface::getServiceIdMappings()
+   */
+  public function getServiceIdMappings(): array {
+    $mapping = [];
+    foreach ($this->getServiceIds() as $service_id) {
+      if ($this->initialized($service_id) && $service_id !== 'service_container') {
+        $mapping[$this->generateServiceIdHash($this->get($service_id))] = $service_id;
+      }
+    }
+    return $mapping;
+  }
+
+  /**
+   * Implements \Drupal\Component\DependencyInjection\ContainerInterface::generateServiceIdHash()
+   */
+  public function generateServiceIdHash(object $object): string {
+    // Include class name as an additional namespace for the hash since
+    // spl_object_hash's return can be recycled. This still is not a 100%
+    // guarantee to be unique but makes collisions incredibly difficult and even
+    // then the interface would be preserved.
+    // @see https://php.net/spl_object_hash#refsect1-function.spl-object-hash-notes
+    return hash('sha256', get_class($object) . spl_object_hash($object));
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php
index 58cc8b9844c5..e86cc052cba0 100644
--- a/core/lib/Drupal/Core/Config/ConfigImporter.php
+++ b/core/lib/Drupal/Core/Config/ConfigImporter.php
@@ -1064,13 +1064,13 @@ public function alreadyImporting() {
    * keep the services used by the importer in sync.
    */
   protected function reInjectMe() {
-    $this->_serviceIds = [];
-    $vars = get_object_vars($this);
-    foreach ($vars as $key => $value) {
-      if (is_object($value) && isset($value->_serviceId)) {
-        $this->$key = \Drupal::service($value->_serviceId);
-      }
-    }
+    // When rebuilding the container,
+    // \Drupal\Core\DrupalKernel::initializeContainer() saves the hashes of the
+    // old container and passes them to the new one. So __sleep() will
+    // recognize the old services and then __wakeup() will restore them from
+    // the new container.
+    $this->__sleep();
+    $this->__wakeup();
   }
 
 }
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 90488703d6a0..8eb1ec913928 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -10,7 +10,6 @@
 use Drupal\Core\DependencyInjection\Compiler\DeprecatedServicePass;
 use Drupal\Core\DependencyInjection\Compiler\ContextProvidersPass;
 use Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass;
-use Drupal\Core\DependencyInjection\Compiler\DependencySerializationTraitPass;
 use Drupal\Core\DependencyInjection\Compiler\StackedKernelPass;
 use Drupal\Core\DependencyInjection\Compiler\StackedSessionHandlerPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterStreamWrappersPass;
@@ -93,7 +92,6 @@ public function register(ContainerBuilder $container) {
     // Register plugin managers.
     $container->addCompilerPass(new PluginManagerPass());
 
-    $container->addCompilerPass(new DependencySerializationTraitPass());
     $container->addCompilerPass(new DeprecatedServicePass());
   }
 
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php
index a0ba7f0225e3..08a2b46e6533 100644
--- a/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php
@@ -8,6 +8,11 @@
 /**
  * Sets the _serviceId property on all services.
  *
+ * @deprecated in drupal:9.5.0 and is removed from drupal:11.0.0. The _serviceId
+ *   property is no longer part of the container. Use
+ *   \Drupal\Core\DrupalKernelInterface::getServiceIdMapping() instead.
+ *
+ * @see https://www.drupal.org/node/3292540
  * @see \Drupal\Core\DependencyInjection\DependencySerializationTrait
  */
 class DependencySerializationTraitPass implements CompilerPassInterface {
@@ -16,35 +21,6 @@ class DependencySerializationTraitPass implements CompilerPassInterface {
    * {@inheritdoc}
    */
   public function process(ContainerBuilder $container) {
-    $decorations = new \SplPriorityQueue();
-    $order = PHP_INT_MAX;
-
-    foreach ($container->getDefinitions() as $service_id => $definition) {
-      // Only add the property to services that are public (as private services
-      // can not be reloaded through Container::get()) and are objects.
-      if (!$definition->hasTag('parameter_service') && $definition->isPublic()) {
-        $definition->setProperty('_serviceId', $service_id);
-      }
-
-      if ($decorated = $definition->getDecoratedService()) {
-        $decorations->insert([$service_id, $definition], [$decorated[2], --$order]);
-      }
-    }
-
-    foreach ($decorations as list($service_id, $definition)) {
-      list($inner, $renamedId) = $definition->getDecoratedService();
-      if (!$renamedId) {
-        $renamedId = $service_id . '.inner';
-      }
-
-      $original = $container->getDefinition($inner);
-      if ($original->isPublic()) {
-        // The old service is renamed.
-        $original->setProperty('_serviceId', $renamedId);
-        // The decorating service takes over the old ID.
-        $definition->setProperty('_serviceId', $inner);
-      }
-    }
   }
 
 }
diff --git a/core/lib/Drupal/Core/DependencyInjection/Container.php b/core/lib/Drupal/Core/DependencyInjection/Container.php
index 564336331691..349261052d21 100644
--- a/core/lib/Drupal/Core/DependencyInjection/Container.php
+++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
@@ -9,18 +9,6 @@
  */
 class Container extends DrupalContainer {
 
-  /**
-   * {@inheritdoc}
-   */
-  public function set($id, $service) {
-    parent::set($id, $service);
-
-    // Ensure that the _serviceId property is set on synthetic services as well.
-    if (isset($this->services[$id]) && is_object($this->services[$id]) && !isset($this->services[$id]->_serviceId)) {
-      $this->services[$id]->_serviceId = $id;
-    }
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php
index f8b7a916ceed..d24bf2cbf5d8 100644
--- a/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php
+++ b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Core\DependencyInjection;
 
+use Drupal\Component\DependencyInjection\ContainerInterface;
+use Drupal\Component\DependencyInjection\ServiceIdHashTrait;
 use Symfony\Component\DependencyInjection\Alias;
 use Symfony\Component\DependencyInjection\ContainerBuilder as SymfonyContainerBuilder;
 use Symfony\Component\DependencyInjection\Container as SymfonyContainer;
@@ -15,7 +17,9 @@
  *
  * @ingroup container
  */
-class ContainerBuilder extends SymfonyContainerBuilder {
+class ContainerBuilder extends SymfonyContainerBuilder implements ContainerInterface {
+
+  use ServiceIdHashTrait;
 
   /**
    * {@inheritdoc}
@@ -41,11 +45,6 @@ public function set($id, $service) {
       throw new \InvalidArgumentException("Service ID names must be lowercase: $id");
     }
     SymfonyContainer::set($id, $service);
-
-    // Ensure that the _serviceId property is set on synthetic services as well.
-    if (isset($this->services[$id]) && is_object($this->services[$id]) && !isset($this->services[$id]->_serviceId)) {
-      $this->services[$id]->_serviceId = $id;
-    }
   }
 
   /**
diff --git a/core/lib/Drupal/Core/DependencyInjection/DependencySerializationTrait.php b/core/lib/Drupal/Core/DependencyInjection/DependencySerializationTrait.php
index c24598689e3c..a23a2f487ab0 100644
--- a/core/lib/Drupal/Core/DependencyInjection/DependencySerializationTrait.php
+++ b/core/lib/Drupal/Core/DependencyInjection/DependencySerializationTrait.php
@@ -3,7 +3,8 @@
 namespace Drupal\Core\DependencyInjection;
 
 use Drupal\Core\Entity\EntityStorageInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
+use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
 
 /**
  * Provides dependency injection friendly methods for serialization.
@@ -30,33 +31,50 @@ trait DependencySerializationTrait {
    * {@inheritdoc}
    */
   public function __sleep() {
-    $this->_serviceIds = [];
     $vars = get_object_vars($this);
-    foreach ($vars as $key => $value) {
-      if (is_object($value) && isset($value->_serviceId)) {
-        // If a class member was instantiated by the dependency injection
-        // container, only store its ID so it can be used to get a fresh object
-        // on unserialization.
-        $this->_serviceIds[$key] = $value->_serviceId;
-        unset($vars[$key]);
-      }
-      // Special case the container, which might not have a service ID.
-      elseif ($value instanceof ContainerInterface) {
-        $this->_serviceIds[$key] = 'service_container';
-        unset($vars[$key]);
-      }
-      elseif ($value instanceof EntityStorageInterface) {
-        // If a class member is an entity storage, only store the entity type ID
-        // the storage is for so it can be used to get a fresh object on
-        // unserialization. By doing this we prevent possible memory leaks when
-        // the storage is serialized when it contains a static cache of entity
-        // objects and additionally we ensure that we'll not have multiple
-        // storage objects for the same entity type and therefore prevent
-        // returning different references for the same entity.
-        $this->_entityStorages[$key] = $value->getEntityTypeId();
-        unset($vars[$key]);
+    try {
+      $container = \Drupal::getContainer();
+      $mapping = \Drupal::service('kernel')->getServiceIdMapping();
+      foreach ($vars as $key => $value) {
+        if ($value instanceof EntityStorageInterface) {
+          // If a class member is an entity storage, only store the entity type
+          // ID the storage is for, so it can be used to get a fresh object on
+          // unserialization. By doing this we prevent possible memory leaks
+          // when the storage is serialized and it contains a static cache of
+          // entity objects. Additionally we ensure that we'll not have multiple
+          // storage objects for the same entity type and therefore prevent
+          // returning different references for the same entity.
+          $this->_entityStorages[$key] = $value->getEntityTypeId();
+          unset($vars[$key]);
+        }
+        elseif (is_object($value)) {
+          $service_id = FALSE;
+          // Special case the container.
+          if ($value instanceof SymfonyContainerInterface) {
+            $service_id = 'service_container';
+          }
+          else {
+            $id = $container->generateServiceIdHash($value);
+            if (isset($mapping[$id])) {
+              $service_id = $mapping[$id];
+            }
+          }
+          if ($service_id) {
+            // If a class member was instantiated by the dependency injection
+            // container, only store its ID so it can be used to get a fresh object
+            // on unserialization.
+            $this->_serviceIds[$key] = $service_id;
+            unset($vars[$key]);
+          }
+        }
       }
     }
+    catch (ContainerNotInitializedException $e) {
+      // No container, no problem.
+    }
+    catch (ServiceNotFoundException $e) {
+      // No kernel, very strange, but still no problem.
+    }
 
     return array_keys($vars);
   }
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 0461afd5af5f..bb6be38b337f 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -100,7 +100,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
   /**
    * Holds the container instance.
    *
-   * @var \Symfony\Component\DependencyInjection\ContainerInterface
+   * @var \Drupal\Component\DependencyInjection\ContainerInterface
    */
   protected $container;
 
@@ -237,6 +237,11 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
    */
   protected $root;
 
+  /**
+   * A mapping from service classes to service IDs.
+   */
+  protected $serviceIdMapping = [];
+
   /**
    * Create a DrupalKernel object from a request.
    *
@@ -783,6 +788,32 @@ public function updateModules(array $module_list, array $module_filenames = [])
     }
   }
 
+  /**
+   * Generate a unique hash for a service object.
+   *
+   * @param object $object
+   *   A service object.
+   *
+   * @return string
+   *   A unique hash value.
+   */
+  public static function generateServiceIdHash($object) {
+    // Include class name as an additional namespace for the hash since
+    // spl_object_hash's return can be recycled. This still is not a 100%
+    // guarantee to be unique but makes collisions incredibly difficult and even
+    // then the interface would be preserved.
+    // @see https://php.net/spl_object_hash#refsect1-function.spl-object-hash-notes
+    return hash('sha256', get_class($object) . spl_object_hash($object));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getServiceIdMapping() {
+    $this->collectServiceIdMapping();
+    return $this->serviceIdMapping;
+  }
+
   /**
    * Returns the container cache key based on the environment.
    *
@@ -828,6 +859,8 @@ protected function initializeContainer() {
       if ($this->container->initialized('current_user')) {
         $current_user_id = $this->container->get('current_user')->id();
       }
+      // Save the current services.
+      $this->collectServiceIdMapping();
 
       // If there is a session, close and save it.
       if ($this->container->initialized('session')) {
@@ -1535,6 +1568,17 @@ protected function addServiceFiles(array $service_yamls) {
     $this->serviceYamls['site'] = array_filter($service_yamls, 'file_exists');
   }
 
+  /**
+   * Collect a mapping between service to ids.
+   */
+  protected function collectServiceIdMapping() {
+    if (isset($this->container)) {
+      foreach ($this->container->getServiceIdMappings() as $hash => $service_id) {
+        $this->serviceIdMapping[$hash] = $service_id;
+      }
+    }
+  }
+
   /**
    * Gets the active install profile.
    *
diff --git a/core/lib/Drupal/Core/DrupalKernelInterface.php b/core/lib/Drupal/Core/DrupalKernelInterface.php
index 28a24006dd43..0378dff3e7da 100644
--- a/core/lib/Drupal/Core/DrupalKernelInterface.php
+++ b/core/lib/Drupal/Core/DrupalKernelInterface.php
@@ -137,4 +137,9 @@ public function preHandle(Request $request);
    */
   public function loadLegacyIncludes();
 
+  /**
+   * Get a mapping from service hashes to service IDs.
+   */
+  public function getServiceIdMapping();
+
 }
diff --git a/core/lib/Drupal/Core/Test/TestKernel.php b/core/lib/Drupal/Core/Test/TestKernel.php
index 26917b4a80e1..855d695fac28 100644
--- a/core/lib/Drupal/Core/Test/TestKernel.php
+++ b/core/lib/Drupal/Core/Test/TestKernel.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Test;
 
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DrupalKernel;
 
 /**
@@ -22,4 +23,23 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE)
     parent::__construct($environment, $class_loader, $allow_dumping);
   }
 
+  /**
+   * Sets a container with a kernel service on the Drupal class.
+   *
+   * @return \Drupal\Component\DependencyInjection\ContainerInterface
+   *   A container with the kernel service set.
+   */
+  public static function setContainerWithKernel() {
+    $container = new ContainerBuilder();
+    $kernel = new DrupalKernel('test', NULL);
+    // Objects of the same type will have access to each others private and
+    // protected members even though they are not the same instances. This is
+    // because the implementation specific details are already known when
+    // inside those objects.
+    $kernel->container = $container;
+    $container->set('kernel', $kernel);
+    \Drupal::setContainer($container);
+    return $container;
+  }
+
 }
diff --git a/core/modules/system/tests/src/Kernel/DecoratedServiceTest.php b/core/modules/system/tests/src/Kernel/DecoratedServiceTest.php
index 854662df18bb..099019da517e 100644
--- a/core/modules/system/tests/src/Kernel/DecoratedServiceTest.php
+++ b/core/modules/system/tests/src/Kernel/DecoratedServiceTest.php
@@ -22,12 +22,16 @@ class DecoratedServiceTest extends KernelTestBase {
   public function testDecoratedServiceId() {
     // Service decorated once.
     $test_service = $this->container->get('test_service');
-    $this->assertEquals('test_service', $test_service->_serviceId);
+    $hash = $this->container->generateServiceIdHash($test_service);
+    $mappings = $this->container->getServiceIdMappings();
+    $this->assertEquals('test_service', $mappings[$hash]);
     $this->assertInstanceOf(TestServiceDecorator::class, $test_service);
 
     // Service decorated twice.
     $test_service2 = $this->container->get('test_service2');
-    $this->assertEquals('test_service2', $test_service2->_serviceId);
+    $hash = $this->container->generateServiceIdHash($test_service2);
+    $mappings = $this->container->getServiceIdMappings();
+    $this->assertEquals('test_service2', $mappings[$hash]);
     $this->assertInstanceOf(TestServiceDecorator::class, $test_service2);
   }
 
diff --git a/core/modules/views_ui/tests/src/Unit/ViewUIObjectTest.php b/core/modules/views_ui/tests/src/Unit/ViewUIObjectTest.php
index 480b679537d7..17f93a4777d6 100644
--- a/core/modules/views_ui/tests/src/Unit/ViewUIObjectTest.php
+++ b/core/modules/views_ui/tests/src/Unit/ViewUIObjectTest.php
@@ -114,10 +114,6 @@ public function testIsLocked() {
    * Tests serialization of the ViewUI object.
    */
   public function testSerialization() {
-    // Set a container so the DependencySerializationTrait has it.
-    $container = new ContainerBuilder();
-    \Drupal::setContainer($container);
-
     $storage = new View([], 'view');
     $executable = $this->getMockBuilder('Drupal\views\ViewExecutable')
       ->disableOriginalConstructor()
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityTypeTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypeTest.php
new file mode 100644
index 000000000000..6b47489844f6
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypeTest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\EntityType;
+use Drupal\Core\StringTranslation\TranslationManager;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests general features of entity types.
+ *
+ * @group Entity
+ */
+class EntityTypeTest extends KernelTestBase {
+
+  /**
+   * Sets up an EntityType object for a given set of values.
+   *
+   * @param array $definition
+   *   An array of values to use for the EntityType.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface
+   */
+  protected function setUpEntityType($definition) {
+    $definition += [
+      'id' => 'example_entity_type',
+    ];
+    return new EntityType($definition);
+  }
+
+  /**
+   * Tests that the EntityType object can be serialized.
+   */
+  public function testIsSerializable() {
+    $entity_type = $this->setUpEntityType([]);
+
+    $translation_service = new class () extends TranslationManager {
+
+      /**
+       * Constructs a UnserializableTranslationManager object.
+       */
+      public function __construct() {
+      }
+
+      /**
+       * @return array
+       */
+      public function __serialize(): array {
+        throw new \Exception();
+      }
+
+    };
+
+    $this->container->set('bar', $translation_service);
+    $entity_type->setStringTranslation($this->container->get('string_translation'));
+
+    // This should not throw an exception.
+    $tmp = serialize($entity_type);
+    $entity_type = unserialize($tmp);
+    // And this should have the correct id.
+    $this->assertEquals('example_entity_type', $entity_type->id());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
index f6b5cbb3f2d3..1a2e3115d6dd 100644
--- a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
+++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
@@ -30,7 +30,7 @@ class ContainerTest extends TestCase {
   /**
    * The tested container.
    *
-   * @var \Symfony\Component\DependencyInjection\ContainerInterface
+   * @var \Drupal\Component\DependencyInjection\ContainerInterface
    */
   protected $container;
 
@@ -685,6 +685,20 @@ public function testResolveServicesAndParametersForRawArgument() {
     $this->assertEquals(['ccc'], $this->container->get('service_with_raw_argument')->getArguments());
   }
 
+  /**
+   * @covers \Drupal\Component\DependencyInjection\ServiceIdHashTrait::getServiceIdMappings
+   * @covers \Drupal\Component\DependencyInjection\ServiceIdHashTrait::generateServiceIdHash
+   */
+  public function testGetServiceIdMappings() {
+    $this->assertEquals([], $this->container->getServiceIdMappings());
+    $s1 = $this->container->get('other.service');
+    $s2 = $this->container->get('late.service');
+    $this->assertEquals([
+      $this->container->generateServiceIdHash($s1) => 'other.service',
+      $this->container->generateServiceIdHash($s2) => 'late.service',
+    ], $this->container->getServiceIdMappings());
+  }
+
   /**
    * Gets a mock container definition.
    *
diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
index b0d9a132df80..6771cee67841 100644
--- a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
+++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
@@ -10,6 +10,7 @@
   use Drupal\Component\Utility\Crypt;
   use Drupal\Tests\PhpUnitCompatibilityTrait;
   use PHPUnit\Framework\TestCase;
+  use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
   use Symfony\Component\DependencyInjection\Definition;
   use Symfony\Component\DependencyInjection\Reference;
   use Symfony\Component\DependencyInjection\Parameter;
@@ -26,6 +27,7 @@
   class OptimizedPhpArrayDumperTest extends TestCase {
 
     use PhpUnitCompatibilityTrait;
+    use ExpectDeprecationTrait;
 
     /**
      * The container builder instance.
@@ -335,16 +337,6 @@ public function getDefinitionsDataProvider() {
         'arguments_expected' => $this->getCollection([$this->getParameterCall('llama_parameter')]),
       ] + $base_service_definition;
 
-      // Test objects that have _serviceId property.
-      $drupal_service = new \stdClass();
-      $drupal_service->_serviceId = 'bar';
-
-      $service_definitions[] = [
-        'arguments' => [$drupal_service],
-        'arguments_count' => 1,
-        'arguments_expected' => $this->getCollection([$this->getServiceCall('bar')]),
-      ] + $base_service_definition;
-
       // Test getMethodCalls.
       $calls = [
         ['method', $this->getCollection([])],
@@ -578,6 +570,37 @@ public function testGetServiceDefinitionForObject() {
       $this->dumper->getArray();
     }
 
+    /**
+     * Tests that the correct RuntimeException is thrown for dumping an object.
+     *
+     * @covers ::dumpValue
+     * @group legacy
+     */
+    public function testGetServiceDefinitionForObjectServiceId() {
+      $service = new \stdClass();
+      $service->_serviceId = 'foo';
+
+      $services['foo'] = new Definition('\stdClass');
+      $services['bar'] = new Definition('\stdClass');
+      $services['bar']->addArgument($service);
+      foreach ($services as $s) {
+        $s->setPublic(TRUE);
+      }
+
+      $this->containerBuilder->getDefinitions()->willReturn($services);
+      $this->containerBuilder->getDefinition('foo')->willReturn($services['foo']);
+      $this->containerBuilder->getDefinition('bar')->willReturn($services['bar']);
+      $this->expectDeprecation('_serviceId is deprecated in drupal:9.5.0 and is removed from drupal:11.0.0. Use \Drupal\Core\DrupalKernelInterface::getServiceIdMapping() instead. See https://www.drupal.org/node/3292540');
+      $a = $this->dumper->getArray();
+      $this->assertEquals(
+        $this->serializeDefinition([
+          'class' => '\stdClass',
+          // Legacy code takes care of converting _serviceId into this.
+          'arguments' => $this->getCollection([$this->getServiceCall('foo')]),
+          'arguments_count' => 1,
+        ]), $a['services']['bar']);
+    }
+
     /**
      * Tests that the correct RuntimeException is thrown for dumping a resource.
      *
diff --git a/core/tests/Drupal/Tests/Component/DrupalComponentTest.php b/core/tests/Drupal/Tests/Component/DrupalComponentTest.php
index 74cd216fe55c..11855926a641 100644
--- a/core/tests/Drupal/Tests/Component/DrupalComponentTest.php
+++ b/core/tests/Drupal/Tests/Component/DrupalComponentTest.php
@@ -97,8 +97,8 @@ protected function assertNoCoreUsage(string $class_path): void {
     $contents = file_get_contents($class_path);
     preg_match_all('/^.*Drupal\\\Core.*$/m', $contents, $matches);
     $matches = array_filter($matches[0], function ($line) {
-      // Filter references to @see as they don't really matter.
-      return strpos($line, '@see') === FALSE;
+      // Filter references that don't really matter.
+      return preg_match('/@see|E_USER_DEPRECATED|expectDeprecation/', $line) === 0;
     });
     $this->assertEmpty($matches, "Checking for illegal reference to 'Drupal\\Core' namespace in $class_path");
   }
diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php
index ccdd71975a35..b54150a0a98c 100644
--- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php
@@ -15,6 +15,7 @@
 use Drupal\Core\Extension\ThemeHandlerInterface;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Plugin\DefaultLazyPluginCollection;
+use Drupal\Core\Test\TestKernel;
 use Drupal\Tests\Core\Config\Entity\Fixtures\ConfigEntityBaseWithPluginCollections;
 use Drupal\Tests\Core\Plugin\Fixtures\TestConfigurablePlugin;
 use Drupal\Tests\UnitTestCase;
@@ -357,9 +358,8 @@ public function testSleepWithPluginCollections() {
 
     // Also set up a container with the plugin manager so that we can assert
     // that the plugin manager itself is also not serialized.
-    $container = new ContainerBuilder();
-    $container->set('plugin.manager.foo', $plugin_manager);
-    \Drupal::setContainer($container);
+    $container = TestKernel::setContainerWithKernel();
+    $container->set('plugin.manager.foo', $plugin_manager->reveal());
 
     $entity_values = ['the_plugin_collection_config' => [$instance_id => ['foo' => 'original_value']]];
     $entity = new TestConfigEntityWithPluginCollections($entity_values, $this->entityTypeId);
diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerBuilderTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerBuilderTest.php
index bff672f3a7e3..7f9394236b62 100644
--- a/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerBuilderTest.php
@@ -24,16 +24,6 @@ public function testGet() {
     $this->assertInstanceOf(BarClass::class, $result);
   }
 
-  /**
-   * @covers ::set
-   */
-  public function testSet() {
-    $container = new ContainerBuilder();
-    $class = new BarClass();
-    $container->set('bar', $class);
-    $this->assertEquals('bar', $class->_serviceId);
-  }
-
   /**
    * @covers ::set
    */
diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerTest.php
index df86cddc0f05..2d7bd078b16c 100644
--- a/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerTest.php
+++ b/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerTest.php
@@ -4,7 +4,6 @@
 
 use Drupal\Core\DependencyInjection\Container;
 use Drupal\Tests\UnitTestCase;
-use Drupal\Tests\Core\DependencyInjection\Fixture\BarClass;
 
 /**
  * @coversDefaultClass \Drupal\Core\DependencyInjection\Container
@@ -21,15 +20,4 @@ public function testSerialize() {
     serialize($container);
   }
 
-  /**
-   * @covers ::set
-   */
-  public function testSet() {
-    $container = new Container();
-    $class = new BarClass();
-    $container->set('bar', $class);
-    // Ensure that _serviceId is set on the object.
-    $this->assertEquals('bar', $class->_serviceId);
-  }
-
 }
diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/DependencySerializationTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/DependencySerializationTest.php
index 6c2446e8f556..22f4d53a96fa 100644
--- a/core/tests/Drupal/Tests/Core/DependencyInjection/DependencySerializationTest.php
+++ b/core/tests/Drupal/Tests/Core/DependencyInjection/DependencySerializationTest.php
@@ -7,8 +7,8 @@
 
 namespace Drupal\Tests\Core\DependencyInjection;
 
-use Drupal\Core\DependencyInjection\Container;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Test\TestKernel;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\DependencyInjection\ContainerAwareInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -26,11 +26,9 @@ class DependencySerializationTest extends UnitTestCase {
   public function testSerialization() {
     // Create a pseudo service and dependency injected object.
     $service = new \stdClass();
-    $service->_serviceId = 'test_service';
-    $container = new Container();
+    $container = TestKernel::setContainerWithKernel();
     $container->set('test_service', $service);
-    $container->set('service_container', $container);
-    \Drupal::setContainer($container);
+    $this->assertSame($container, $container->get('service_container'));
 
     $dependencySerialization = new DependencySerializationTestDummy($service);
     $dependencySerialization->setContainer($container);
@@ -44,29 +42,6 @@ public function testSerialization() {
     $this->assertEmpty($dependencySerialization->getServiceIds());
   }
 
-  /**
-   * @covers ::__sleep
-   * @covers ::__wakeup
-   */
-  public function testSerializationWithMissingService() {
-    // Create a pseudo service and dependency injected object.
-    $service = new \stdClass();
-    $service->_serviceId = 'test_service_not_existing';
-    $container = new Container();
-    $container->set('test_service', $service);
-    $container->set('service_container', $container);
-    \Drupal::setContainer($container);
-
-    $dependencySerialization = new DependencySerializationTestDummy($service);
-    $dependencySerialization->setContainer($container);
-
-    $string = serialize($dependencySerialization);
-    /** @var \Drupal\Tests\Core\DependencyInjection\DependencySerializationTestDummy $dependencySerialization */
-    $dependencySerialization = unserialize($string);
-
-    $this->assertSame($container, $dependencySerialization->container);
-  }
-
 }
 
 /**
diff --git a/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTest.php b/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTest.php
index ac30f40b6ff1..b94cf7ec05cc 100644
--- a/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTest.php
+++ b/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTest.php
@@ -3,6 +3,8 @@
 namespace Drupal\Tests\Core\DrupalKernel;
 
 use Drupal\Core\DrupalKernel;
+use Drupal\Core\Test\TestKernel;
+use Drupal\Tests\Core\DependencyInjection\Fixture\BarClass;
 use Drupal\Tests\UnitTestCase;
 use org\bovigo\vfs\vfsStream;
 use Symfony\Component\HttpFoundation\Request;
@@ -136,6 +138,16 @@ public function testFindSitePath() {
     $this->assertEquals('sites/example', DrupalKernel::findSitePath($request, FALSE, $vfs_root->url('drupal_root')));
   }
 
+  /**
+   * @covers ::getServiceIdMapping
+   */
+  public function testGetServiceIdMapping() {
+    $service = new BarClass();
+    $container = TestKernel::setContainerWithKernel();
+    $container->set('bar', $service);
+    $this->assertEquals($container->get('kernel')->getServiceIdMapping()[$container->generateServiceIdHash($service)], 'bar');
+  }
+
 }
 
 /**
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php
index 05d4078ef99e..523a40f2ecc4 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php
@@ -7,7 +7,6 @@
 use Drupal\Core\Entity\EntityType;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\Core\StringTranslation\TranslationManager;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -501,39 +500,4 @@ public function testEntityClassImplements() {
     $this->assertFalse($entity_type->entityClassImplements(\DateTimeInterface::class));
   }
 
-  /**
-   * Tests that the EntityType object can be serialized.
-   */
-  public function testIsSerializable() {
-    $entity_type = $this->setUpEntityType([]);
-
-    $translation_service = new UnserializableTranslationManager();
-    $translation_service->_serviceId = 'string_translation';
-
-    $entity_type->setStringTranslation($translation_service);
-    $entity_type = unserialize(serialize($entity_type));
-
-    $this->assertEquals('example_entity_type', $entity_type->id());
-  }
-
-}
-
-/**
- * Test class.
- */
-class UnserializableTranslationManager extends TranslationManager {
-
-  /**
-   * Constructs a UnserializableTranslationManager object.
-   */
-  public function __construct() {
-  }
-
-  /**
-   * @return array
-   */
-  public function __serialize(): array {
-    throw new \Exception();
-  }
-
 }
diff --git a/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php
index 075cd51d92f6..0863c064af6c 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php
@@ -16,11 +16,11 @@
 use Drupal\Component\Plugin\Exception\MissingValueContextException;
 use Drupal\Core\Cache\NullBackend;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
-use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\Context\ContextDefinition;
 use Drupal\Core\Plugin\Context\ContextHandler;
 use Drupal\Core\Plugin\ContextAwarePluginInterface;
+use Drupal\Core\Test\TestKernel;
 use Drupal\Core\TypedData\TypedDataManager;
 use Drupal\Core\Validation\ConstraintManager;
 use Drupal\Tests\UnitTestCase;
@@ -63,7 +63,7 @@ protected function setUp(): void {
       new ConstraintManager($namespaces, $cache_backend, $module_handler->reveal())
     );
 
-    $container = new ContainerBuilder();
+    $container = TestKernel::setContainerWithKernel();
     $container->set('typed_data_manager', $type_data_manager);
     \Drupal::setContainer($container);
   }
diff --git a/core/tests/Drupal/Tests/Core/TempStore/SharedTempStoreTest.php b/core/tests/Drupal/Tests/Core/TempStore/SharedTempStoreTest.php
index e65c644afba5..6aa700182135 100644
--- a/core/tests/Drupal/Tests/Core/TempStore/SharedTempStoreTest.php
+++ b/core/tests/Drupal/Tests/Core/TempStore/SharedTempStoreTest.php
@@ -4,10 +4,10 @@
 
 use Drupal\Core\Session\AccountProxyInterface;
 use Drupal\Core\TempStore\Lock;
+use Drupal\Core\Test\TestKernel;
 use Drupal\Tests\UnitTestCase;
 use Drupal\Core\TempStore\SharedTempStore;
 use Drupal\Core\TempStore\TempStoreException;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Session\SessionInterface;
@@ -357,12 +357,10 @@ public function testSerialization() {
     $unserializable_request = new UnserializableRequest();
 
     $this->requestStack->push($unserializable_request);
-    $this->requestStack->_serviceId = 'request_stack';
 
-    $container = $this->prophesize(ContainerInterface::class);
-    $container->get('request_stack')->willReturn($this->requestStack);
-    $container->has('request_stack')->willReturn(TRUE);
-    \Drupal::setContainer($container->reveal());
+    $container = TestKernel::setContainerWithKernel();
+    $container->set('request_stack', $this->requestStack);
+    \Drupal::setContainer($container);
 
     $store = unserialize(serialize($this->tempStore));
     $this->assertInstanceOf(SharedTempStore::class, $store);
-- 
GitLab