 core/core.services.yml                             |   8 +-
 .../cache_context_safeguards.info.yml              |   7 ++
 .../src/CacheContextSafeguardsServiceProvider.php  |  34 ++++++
 .../src/CacheabilityBubblingNodeGrantStorage.php   | 124 +++++++++++++++++++++
 .../src}/MetadataBubblingUrlGenerator.php          |   5 +-
 .../src/Tests/NodeAccessGrantsCacheContextTest.php |  36 ++++++
 .../node_access_test_auto_bubbling.info.yml        |   6 +
 .../node_access_test_auto_bubbling.routing.yml     |   6 +
 .../NodeAccessTestAutoBubblingController.php       |  62 +++++++++++
 core/profiles/minimal/minimal.info.yml             |   1 +
 core/profiles/standard/standard.info.yml           |   1 +
 core/profiles/testing/testing.info.yml             |   1 +
 12 files changed, 282 insertions(+), 9 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index ed1341c..589d574 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -745,15 +745,9 @@ services:
     arguments: ['@route_filter.lazy_collector']
     tags:
       - { name: event_subscriber }
-  url_generator.non_bubbling:
+  url_generator:
     class: Drupal\Core\Routing\UrlGenerator
     arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@request_stack', '%filter_protocols%']
-    public: false
-    calls:
-      - [setContext, ['@?router.request_context']]
-  url_generator:
-    class: Drupal\Core\Render\MetadataBubblingUrlGenerator
-    arguments: ['@url_generator.non_bubbling', '@renderer']
     calls:
       - [setContext, ['@?router.request_context']]
   redirect.destination:
diff --git a/core/modules/cache_context_safeguards/cache_context_safeguards.info.yml b/core/modules/cache_context_safeguards/cache_context_safeguards.info.yml
new file mode 100644
index 0000000..1bffaab
--- /dev/null
+++ b/core/modules/cache_context_safeguards/cache_context_safeguards.info.yml
@@ -0,0 +1,7 @@
+name: Cache Context Safeguards
+type: module
+description: "…"
+package: Core
+core: 8.x
+hidden: true
+version: VERSION
diff --git a/core/modules/cache_context_safeguards/src/CacheContextSafeguardsServiceProvider.php b/core/modules/cache_context_safeguards/src/CacheContextSafeguardsServiceProvider.php
new file mode 100644
index 0000000..7158726
--- /dev/null
+++ b/core/modules/cache_context_safeguards/src/CacheContextSafeguardsServiceProvider.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\cache_context_safeguards\CacheContextSafeguardsServiceProvider.
+ */
+
+namespace Drupal\cache_context_safeguards;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\DependencyInjection\ServiceModifierInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * …
+ */
+class CacheContextSafeguardsServiceProvider implements ServiceModifierInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alter(ContainerBuilder $container) {
+    if ($container->has('node.grant_storage')) {
+      $container->setDefinition('node.grant_storage.non_bubbling', $container->getDefinition('node.grant_storage'))
+        ->setPublic(FALSE);
+      $container->register('node.grant_storage')
+        ->setClass('\Drupal\cache_context_safeguards\CacheabilityBubblingNodeGrantStorage')
+        ->addArgument(new Reference('node.grant_storage.non_bubbling'))
+        ->addMethodCall('setContainer', [new Reference('service_container')]);
+    }
+  }
+
+}
+
diff --git a/core/modules/cache_context_safeguards/src/CacheabilityBubblingNodeGrantStorage.php b/core/modules/cache_context_safeguards/src/CacheabilityBubblingNodeGrantStorage.php
new file mode 100644
index 0000000..e279a62
--- /dev/null
+++ b/core/modules/cache_context_safeguards/src/CacheabilityBubblingNodeGrantStorage.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\cache_context_safeguards\CacheabilityBubblingNodeGrantStorage.
+ */
+
+namespace Drupal\cache_context_safeguards;
+
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\node\NodeGrantDatabaseStorageInterface;
+use Drupal\node\NodeInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+
+/**
+ * Decorator for node grants storage, which bubbles access grants cacheability.
+ *
+ * Code that performs a node_access query should explicitly add the
+ * 'user.node_grants:$op' cache context to the render element or
+ * \Drupal\Core\Cache\CacheableDependencyInterface object that is affected by
+ * the query. Doing so ensures that the context is attached to the most
+ * appropriate element or object. However, since cache contexts are new to
+ * Drupal 8, to safeguard route controllers or other code that forget to do
+ * this, this decorator also adds it to the current render context.
+ *
+ * The renderer is not injected to avoid initializing the render and theme
+ * system for REST routes. Instead, this service is container-aware.
+ *
+ * @see \Drupal\Core\Render\MetadataBubblingUrlGenerator
+ *
+ * @todo Remove before Drupal 9.0.0.
+ *
+ * @ingroup node_access
+ */
+class CacheabilityBubblingNodeGrantStorage implements NodeGrantDatabaseStorageInterface, ContainerAwareInterface {
+
+  use ContainerAwareTrait;
+  use DependencySerializationTrait;
+
+  /**
+   * The non-bubbling node grant storage.
+   *
+   * @var \Drupal\node\NodeGrantDatabaseStorageInterface
+   */
+  protected $nodeGrantStorage;
+
+  /**
+   * Constructs a CacheabilityBubblingNodeGrantStorage object.
+   *
+   * @param \Drupal\node\NodeGrantDatabaseStorageInterface $node_grant_storage
+   *   The non-bubbling node grant storage.
+   */
+  public function __construct(NodeGrantDatabaseStorageInterface $node_grant_storage) {
+    $this->nodeGrantStorage = $node_grant_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(NodeInterface $node, $operation, $langcode, AccountInterface $account) {
+    return $this->nodeGrantStorage->access($node, $operation, $langcode, $account);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkAll(AccountInterface $account) {
+    return $this->nodeGrantStorage->checkAll($account);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterQuery($query, array $tables, $op, AccountInterface $account, $base_table) {
+    // Bubble the 'user.node_grants:$op' cache context to the current render
+    // context.
+    /** @var \Drupal\Core\Render\RendererInterface $renderer */
+    $renderer = $this->container->get('renderer');
+    if ($renderer->hasRenderContext()) {
+      $build = ['#cache' => ['contexts' => ['user.node_grants:' . $op]]];
+      $renderer->render($build);
+    }
+
+    return $this->nodeGrantStorage->alterQuery($query, $tables, $op, $account, $base_table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function write(NodeInterface $node, array $grants, $realm = NULL, $delete = TRUE) {
+    return $this->nodeGrantStorage->write($node, $grants, $realm, $delete);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete() {
+    return $this->nodeGrantStorage->delete();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function writeDefault() {
+    return $this->nodeGrantStorage->writeDefault();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function count() {
+    return $this->nodeGrantStorage->count();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteNodeRecords(array $nids) {
+    return $this->nodeGrantStorage->deleteNodeRecords($nids);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/MetadataBubblingUrlGenerator.php b/core/modules/cache_context_safeguards/src/MetadataBubblingUrlGenerator.php
similarity index 96%
rename from core/lib/Drupal/Core/Render/MetadataBubblingUrlGenerator.php
rename to core/modules/cache_context_safeguards/src/MetadataBubblingUrlGenerator.php
index 3a4535e..a62fe93 100644
--- a/core/lib/Drupal/Core/Render/MetadataBubblingUrlGenerator.php
+++ b/core/modules/cache_context_safeguards/src/MetadataBubblingUrlGenerator.php
@@ -2,12 +2,13 @@
 
 /**
  * @file
- * Contains \Drupal\Core\Render\MetadataBubblingUrlGenerator.
+ * Contains \Drupal\cache_contexts_safeguards\MetadataBubblingUrlGenerator.
  */
 
-namespace Drupal\Core\Render;
+namespace Drupal\cache_contexts_safeguards;
 
 use Drupal\Core\GeneratedUrl;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Symfony\Component\Routing\RequestContext as SymfonyRequestContext;
 
diff --git a/core/modules/node/src/Tests/NodeAccessGrantsCacheContextTest.php b/core/modules/node/src/Tests/NodeAccessGrantsCacheContextTest.php
index 1d34bc1..b8fad35 100644
--- a/core/modules/node/src/Tests/NodeAccessGrantsCacheContextTest.php
+++ b/core/modules/node/src/Tests/NodeAccessGrantsCacheContextTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\node\Tests;
 
+use Drupal\Core\Url;
+
 /**
  * Tests the node access grants cache context service.
  *
@@ -140,4 +142,38 @@ public function testCacheContext() {
     ]);
   }
 
+  /**
+   * Tests that the node grants cache context is auto-added, only when needed.
+   *
+   * @see node_query_node_access_alter()
+   */
+  public function testAutomaticNodeAccessGrantsCacheContextBubbling() {
+    $this->dumpHeaders = TRUE;
+
+    // Install the module that contains our test route and controller.
+    $this->container->get('module_installer')->install(['node_access_test_auto_bubbling']);
+    $this->rebuildContainer();
+    $this->container->get('router.builder')->rebuild();
+
+    // The node grants cache context should be added automatically.
+    $this->drupalGet(new Url('node_access_test_auto_bubbling'));
+    $this->assertCacheContext('user.node_grants:view');
+
+    // The root user has the 'bypass node access' permission, which means the
+    // node grants cache context is not necessary.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet(new Url('node_access_test_auto_bubbling'));
+    $this->assertNoCacheContext('user.node_grants:view');
+    $this->drupalLogout();
+
+    // Uninstall the module with the only hook_node_grants() implementation.
+    $this->container->get('module_installer')->uninstall(['node_access_test']);
+    $this->rebuildContainer();
+
+    // Because there are no node grants defined, there also is no need for the
+    // node grants cache context to be bubbled.
+    $this->drupalGet(new Url('node_access_test_auto_bubbling'));
+    $this->assertNoCacheContext('user.node_grants:view');
+  }
+
 }
diff --git a/core/modules/node/tests/modules/node_access_test_auto_bubbling/node_access_test_auto_bubbling.info.yml b/core/modules/node/tests/modules/node_access_test_auto_bubbling/node_access_test_auto_bubbling.info.yml
new file mode 100644
index 0000000..49a990d
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test_auto_bubbling/node_access_test_auto_bubbling.info.yml
@@ -0,0 +1,6 @@
+name: 'Node module access automatic cacheability bubbling tests'
+type: module
+description: 'Support module for node permission testing. Provides a route which does a node access query without explicitly specifying the corresponding cache context.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/node/tests/modules/node_access_test_auto_bubbling/node_access_test_auto_bubbling.routing.yml b/core/modules/node/tests/modules/node_access_test_auto_bubbling/node_access_test_auto_bubbling.routing.yml
new file mode 100644
index 0000000..34fd420
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test_auto_bubbling/node_access_test_auto_bubbling.routing.yml
@@ -0,0 +1,6 @@
+node_access_test_auto_bubbling:
+  path: '/node_access_test_auto_bubbling'
+  defaults:
+    _controller: '\Drupal\node_access_test_auto_bubbling\Controller\NodeAccessTestAutoBubblingController::latest'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/node/tests/modules/node_access_test_auto_bubbling/src/Controller/NodeAccessTestAutoBubblingController.php b/core/modules/node/tests/modules/node_access_test_auto_bubbling/src/Controller/NodeAccessTestAutoBubblingController.php
new file mode 100644
index 0000000..c7788d0
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test_auto_bubbling/src/Controller/NodeAccessTestAutoBubblingController.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node_access_test_auto_bubbling\Controller\NodeAccessTestAutoBubblingController.
+ */
+
+namespace Drupal\node_access_test_auto_bubbling\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\Query\QueryFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Returns a node ID listing.
+ */
+class NodeAccessTestAutoBubblingController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * The entity query factory service.
+   *
+   * @var \Drupal\Core\Entity\Query\QueryFactory
+   */
+  protected $entityQuery;
+
+  /**
+   * Constructs a new NodeAccessTestAutoBubblingController.
+   *
+   * @param \Drupal\Core\Entity\Query\QueryFactory $entity_query
+   *   The entity query factory.
+   */
+  public function __construct(QueryFactory $entity_query) {
+    $this->entityQuery = $entity_query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.query')
+    );
+  }
+
+  /**
+   * Lists the three latest published node IDs.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function latest() {
+    $nids = $this->entityQuery->get('node')
+      ->condition('status', NODE_PUBLISHED)
+      ->sort('created', 'DESC')
+      ->range(0, 3)
+      ->addTag('node_access')
+      ->execute();
+    return ['#markup' => $this->t('The three latest nodes are: !nids.', ['!nids' => implode(', ', $nids)])];
+  }
+
+}
diff --git a/core/profiles/minimal/minimal.info.yml b/core/profiles/minimal/minimal.info.yml
index 206b8e7..b94b9f0 100644
--- a/core/profiles/minimal/minimal.info.yml
+++ b/core/profiles/minimal/minimal.info.yml
@@ -8,5 +8,6 @@ dependencies:
   - block
   - dblog
   - page_cache
+  - cache_context_safeguards
 themes:
   - stark
diff --git a/core/profiles/standard/standard.info.yml b/core/profiles/standard/standard.info.yml
index a356ae8..1c82dcd 100644
--- a/core/profiles/standard/standard.info.yml
+++ b/core/profiles/standard/standard.info.yml
@@ -37,6 +37,7 @@ dependencies:
   - views
   - views_ui
   - tour
+  - cache_context_safeguards
 themes:
   - bartik
   - seven
diff --git a/core/profiles/testing/testing.info.yml b/core/profiles/testing/testing.info.yml
index 5ded376..50a7c7f 100644
--- a/core/profiles/testing/testing.info.yml
+++ b/core/profiles/testing/testing.info.yml
@@ -8,6 +8,7 @@ dependencies:
   # Enable page_cache in testing, to ensure that as many tests as possible run
   # with page caching enabled.
   - page_cache
+  - cache_context_safeguards
 # @todo: Remove this in https://www.drupal.org/node/2352949
 themes:
   - classy
