 core/lib/Drupal/Core/Render/Renderer.php           | 17 ++++---
 .../modules/system/src/Tests/Common/RenderTest.php | 57 ++++++++++++++--------
 .../tests/modules/common_test/common_test.module   | 28 +++++++++++
 3 files changed, 75 insertions(+), 27 deletions(-)

diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index ed9270a..373a0be 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -323,12 +323,14 @@ public function render(&$elements, $is_root_call = FALSE) {
       // further and create and render new child elements, so provide a fresh
       // stack frame to collect those additions, merge them back to the element,
       // and then update the current frame to match the modified element state.
-      self::$stack->push(new RenderStackFrame());
-      $this->processPostRenderCache($elements);
-      $post_render_additions = self::$stack->pop();
-      $elements['#cache']['tags'] = Cache::mergeTags($elements['#cache']['tags'], $post_render_additions->tags);
-      $elements['#attached'] = drupal_merge_attached($elements['#attached'], $post_render_additions->attached);
-      $elements['#post_render_cache'] = NestedArray::mergeDeep($elements['#post_render_cache'], $post_render_additions->postRenderCache);
+      do {
+        self::$stack->push(new RenderStackFrame());
+        $this->processPostRenderCache($elements);
+        $post_render_additions = self::$stack->pop();
+        $elements['#cache']['tags'] = Cache::mergeTags($elements['#cache']['tags'], $post_render_additions->tags);
+        $elements['#attached'] = drupal_merge_attached($elements['#attached'], $post_render_additions->attached);
+        $elements['#post_render_cache'] = $post_render_additions->postRenderCache;
+      } while (!empty($elements['#post_render_cache']));
       if (self::$stack->count() !== 1) {
         throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
       }
@@ -400,6 +402,7 @@ protected function bubbleStack() {
    * #post_render_cache callbacks may modify:
    * - #markup: to replace placeholders
    * - #attached: to add libraries or JavaScript settings
+   * - #post_render_cache: to execute additional #post_render_cache callbacks
    *
    * Note that in either of these cases, #post_render_cache callbacks are
    * implicitly idempotent: a placeholder that has been replaced can't be
@@ -407,8 +410,6 @@ protected function bubbleStack() {
    *
    * @param array &$elements
    *   The structured array describing the data being rendered.
-   *
-   * @see drupal_render_collect_post_render_cache
    */
   protected function processPostRenderCache(array &$elements) {
     if (isset($elements['#post_render_cache'])) {
diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php
index 523ab62..8f801d8 100644
--- a/core/modules/system/src/Tests/Common/RenderTest.php
+++ b/core/modules/system/src/Tests/Common/RenderTest.php
@@ -796,6 +796,10 @@ function testDrupalRenderChildrenPostRenderCache() {
   function testDrupalRenderRenderCachePlaceholder() {
     $context = array(
       'bar' => $this->randomContextValue(),
+      // Provide a token instead of letting one be generated by
+      // drupal_render_cache_generate_placeholder(), otherwise we cannot know
+      // what the token is.
+      'token' => \Drupal\Component\Utility\Crypt::randomBytesBase64(55),
     );
     $callback = 'common_test_post_render_cache_placeholder';
     $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
@@ -837,7 +841,7 @@ function testDrupalRenderRenderCachePlaceholder() {
     $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
 
     // GET request: validate cached data.
-    $expected_token = $element['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token'];
+    $expected_token = $context['token'];
     $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_GET'));
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     // Parse unique token out of the cached markup.
@@ -883,6 +887,10 @@ function testDrupalRenderRenderCachePlaceholder() {
   function testDrupalRenderChildElementRenderCachePlaceholder() {
     $context = array(
       'bar' => $this->randomContextValue(),
+      // Provide a token instead of letting one be generated by
+      // drupal_render_cache_generate_placeholder(), otherwise we cannot know
+      // what the token is.
+      'token' => \Drupal\Component\Utility\Crypt::randomBytesBase64(55),
     );
     $callback = 'common_test_post_render_cache_placeholder';
     $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
@@ -920,15 +928,13 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     $element['foo']['#cache'] = array('cid' => 'render_cache_placeholder_test_child_GET');
     // Render, which will use the common-test-render-element.html.twig template.
     $output = drupal_render_root($element);
-    $this->assertIdentical($output, $expected_output); //, 'Placeholder was replaced in output');
+    $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
     $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
 
     // GET request: validate cached data for child element.
-    $child_tokens = $element['foo']['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token'];
-    $parent_tokens = $element['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token'];
-    $expected_token = $child_tokens;
+    $expected_token = $context['token'];
     $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET'));
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     // Parse unique token out of the cached markup.
@@ -978,7 +984,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
       ),
       '#cache' => array('tags' => array('rendered')),
     );
-    $this->assertIdentical($cached_element, $expected_element); //, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+    $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
 
     // GET request: validate cached data.
     // Check the cache of the child element again after the parent has been
@@ -987,7 +993,6 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     // Verify that the child element contains the correct
     // render_cache_placeholder markup.
-    $expected_token = $child_tokens;
     $dom = Html::load($cached_element['#markup']);
     $xpath = new \DOMXPath($dom);
     $nodes = $xpath->query('//*[@token]');
@@ -1025,6 +1030,31 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
   }
 
   /**
+   * Tests a #post_render_cache callback that adds another #post_render_cache
+   * callback.
+   *
+   * E.g. when rendering a node in a #post_render_cache callback, the rendering
+   * of that node needs a #post_render_cache callback of its own to be executed
+   * (to render the node links).
+   */
+  function testRecursivePostRenderCache() {
+    $context = array('foo' => $this->randomContextValue());
+    $element = [];
+    $element['#markup'] = '';
+    $element['#post_render_cache']['common_test_post_render_cache_recursion'] = array(
+      $context
+    );
+
+    $output = drupal_render_root($element);
+    $this->assertEqual('<p>overridden</p>', $output, 'The output has been modified by the indirect, recursive #post_render_cache callback.');
+    $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden by the indirect, recursive #post_render_cache callback.');
+    $expected_js = [
+      ['type' => 'setting', 'data' => ['common_test' => $context]],
+    ];
+    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified by the indirect, recursive #post_render_cache callback.');
+  }
+
+  /**
    * #pre_render callback for testDrupalRenderBubbling().
    */
   public static function bubblingPreRender($elements) {
@@ -1123,18 +1153,7 @@ function testDrupalRenderBubbling() {
         ),
       );
       $this->assertEqual($expected_attached, $test_element['#attached'], 'Expected assets found.');
-      $expected_post_render_cache = array(
-        'Drupal\\system\\Tests\\Common\\RenderTest::bubblingPostRenderCache' => array(
-          0 => array (
-            'foo' => 'bar',
-            'baz' => 'qux',
-          ),
-        ),
-      );
-      $post_render_cache = $test_element['#post_render_cache'];
-      // We don't care about the exact token.
-      unset($post_render_cache['Drupal\\system\\Tests\\Common\\RenderTest::bubblingPostRenderCache'][0]['token']);
-      $this->assertEqual($expected_post_render_cache, $post_render_cache, 'Expected post-render cache data found.');
+      $this->assertEqual([], $test_element['#post_render_cache'], '#post_render_cache property is empty after rendering');
 
       // Ensure that #pre_render callbacks are only executed if they don't have
       // a render cache hit.
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index a1100b4..24aa3da 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -255,6 +255,34 @@ function common_test_post_render_cache_placeholder(array $element, array $contex
 }
 
 /**
+ * #post_render_cache callback; bubbles another #post_render_cache callback.
+ *
+ * @param array $element
+ *  A render array with the following keys:
+ *    - #markup
+ *    - #attached
+ * @param array $context
+ *  An array with the following keys:
+ *    - foo: contains a random string.
+ *
+ * @return array $element
+ *   The updated $element.
+ */
+function common_test_post_render_cache_recursion(array $element, array $context) {
+  // Render a child which itself also has a #post_render_cache callback that
+  // must be bubbled.
+  $child = [];
+  $child['#markup'] = 'foo';
+  $child['#post_render_cache']['common_test_post_render_cache'][] = $context;
+
+  // Render the child.
+  $element['#markup'] = drupal_render($child);
+
+  return $element;
+}
+
+
+/**
  * Implements hook_page_attachments().
  *
  * @see \Drupal\system\Tests\Common\PageRenderTest::assertPageRenderHookExceptions()
