diff --git a/core/modules/block/block.install b/core/modules/block/block.install
new file mode 100644
index 0000000..1a0ac23
--- /dev/null
+++ b/core/modules/block/block.install
@@ -0,0 +1,137 @@
+<?php
+
+/**
+ * @file
+ * Install, update, and uninstall functions for the Block module.
+ */
+
+/**
+ * @addtogroup updates-8.0.0-beta
+ * @{
+ */
+
+/**
+ * Update block visibility context mapping.
+ */
+function block_update_8001() {
+  // This update function updates blocks for the change of
+  // https://www.drupal.org/node/2354889
+  //
+  // Core visibility context plugins are updated automatically; blocks with
+  // unknown plugins are disabled and their previous visibility settings are
+  // saved in key value storage; see change record
+  // https://www.drupal.org/node/2527840 for more explanation.
+
+  // These 3 are all the contexts that Drupal core provides.
+  $context_service_id_map = [
+    'language' => 'language.current_language_context',
+    'node' => 'node.node_route_context',
+    'user' => 'user.current_user_context',
+  ];
+
+  // Using the Entity API is fine as long we change the value from one valid
+  // value to another value. In this update function however, we deal with
+  // converting the format of the context_mapping, which makes code reacting
+  // to Entity API hooks tricky, because they would need to be different for
+  // before/after the update.
+  // For updating the status flag of the block in the next update function,
+  // however, we can use Entity API.
+  // Contributed modules should leverage hook_update_dependencies() in order to
+  // be executed before block_update_8002().
+  $config_factory = \Drupal::configFactory();
+  $message = NULL;
+  $backup_values = [];
+
+  $update_backup = [];
+  foreach ($config_factory->listAll('block.block.') as $block_config_name) {
+    $block = $config_factory->getEditable($block_config_name);
+    if ($visibility = $block->get('visibility')) {
+      foreach ($visibility as $condition_plugin_id => &$condition) {
+        foreach ($condition['context_mapping'] as $key => $context) {
+          if (!isset($context_service_id_map[$key])) {
+            // Remove the visibility condition for unknown context mapping
+            // entries, so the update process itself runs through and users can
+            // fix their block placements manually OR alternatively contrib
+            // modules can run their own update functions to update mappings
+            // that they provide.
+            $backup_values[] = $context;
+            unset($visibility[$condition_plugin_id]);
+            continue;
+          }
+          // We replace the previous format of "{$context_id}"
+          // with "@{$service_id}:{$unqualified_context_id}".
+          $new_context_id = explode('.', $context, 2);
+          $condition['context_mapping'][$key] = '@' . $context_service_id_map[$key] . ':' . $new_context_id[1];
+        }
+      }
+      $block->set('visibility', $visibility);
+
+      if ($backup_values) {
+        // We not only store the missing context mappings but also the
+        // previous block status so contributed and custom modules could update.
+        $update_backup[$block->get('id')] = ['missing_context_ids' => $backup_values, 'status' => $block->get('status')];
+      }
+    }
+
+    $block->save();
+  }
+
+  if ($update_backup) {
+    \Drupal::keyValue('update_backup')->set('block_update_8001', $update_backup);
+  }
+
+  return $message;
+}
+
+/**
+ * Disables all blocks from the previous update.
+ */
+function block_update_8002() {
+  // Note: For this update function it's fine to use the entity API; see the
+  // explanation in block_update_8001().
+  $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
+
+  $block_storage = \Drupal::entityManager()->getStorage('block');
+  $blocks = $block_storage->loadMultiple(array_keys($block_update_8001));
+  /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $block */
+  foreach ($blocks as $block) {
+    // This block will have an invalid context mapping service and must be
+    // disabled in order to prevent information disclosure.
+    $block->setStatus(FALSE);
+    $block->save();
+  }
+
+  // Provides a list of plugin labels, keyed by plugin ID.
+  $condition_plugin_id_label_map = array_map(function($definition) {
+    // The label might be a translation wrapper.
+    return (string) $definition['label'];
+  }, \Drupal::service('plugin.manager.condition')->getDefinitions());
+
+
+  // Override with the UI labels we are aware of. Sadly they are not machine
+  // accessible, see
+  // \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm().
+  $condition_plugin_id_label_map['node_type'] = \Drupal::translation()->translate('Content types');
+  $condition_plugin_id_label_map['request_path'] = \Drupal::translation()->translate('Pages');
+  $condition_plugin_id_label_map['user_role'] = \Drupal::translation()->translate('Roles');
+
+  if (count($blocks) > 0) {
+    $message = \Drupal::translation()
+      ->translate('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:');
+    $message .= '<ul>';
+    foreach ($blocks as $disabled_block_id => $disabled_block) {
+      $message .= '<li>' . Drupal::translation()
+          ->translate('@label (Visibility: @plugin_ids)', array(
+            '@label' => $disabled_block->label(),
+            '@plugin_ids' => implode(', ', array_intersect_key($condition_plugin_id_label_map, array_flip($block_update_8001[$disabled_block_id]['missing_context_ids'])))
+          )) . '</li>';
+    }
+    $message .= '</ul>';
+
+    return $message;
+  }
+}
+
+/**
+ * @} End of "addtogroup updates-8.0.0-beta".
+ */
diff --git a/core/modules/block/src/Tests/Update/BlockContextMappingUpdateTest.php b/core/modules/block/src/Tests/Update/BlockContextMappingUpdateTest.php
new file mode 100644
index 0000000..dd23d74
--- /dev/null
+++ b/core/modules/block/src/Tests/Update/BlockContextMappingUpdateTest.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block\Tests\Update\BlockContextMappingUpdateTest.
+ */
+
+namespace Drupal\block\Tests\Update;
+
+use Drupal\block\Entity\Block;
+use Drupal\node\Entity\Node;
+use Drupal\system\Tests\Update\UpdatePathTestBase;
+
+/**
+ * Tests the upgrade path for block context mapping renames.
+ *
+ * @see https://www.drupal.org/node/2354889
+ *
+ * @group Update
+ */
+class BlockContextMappingUpdateTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['block_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
+      __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.block-context-manager-2354889.php',
+    ];
+    parent::setUp();
+  }
+
+  /**
+   * Tests that block context mapping is updated properly.
+   */
+  public function testUpdateHookN() {
+    $this->runUpdates();
+    $this->assertRaw('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:<ul><li>User login (Visibility: Baloney spam)</li></ul>');
+
+    // Disable maintenance mode.
+    \Drupal::state()->set('system.maintenance_mode', FALSE);
+
+    // The block that we are testing has the following visibility rules:
+    // - only visible on node pages
+    // - only visible to authenticated users.
+    $block_title = 'Test for 2354889';
+
+    // Create two nodes, a page and an article.
+    $page = Node::create([
+      'type' => 'page',
+      'title' => 'Page node',
+    ]);
+    $page->save();
+
+    $article = Node::create([
+      'type' => 'article',
+      'title' => 'Article node',
+    ]);
+    $article->save();
+
+    // Check that the block appears only on Page nodes for authenticated users.
+    $this->drupalGet('node/' . $page->id());
+    $this->assertRaw($block_title, 'Test block is visible on a Page node as an authenticated user.');
+
+    $this->drupalGet('node/' . $article->id());
+    $this->assertNoRaw($block_title, 'Test block is not visible on a Article node as an authenticated user.');
+
+    $this->drupalLogout();
+
+    // Check that the block does not appear on any page for anonymous users.
+    $this->drupalGet('node/' . $page->id());
+    $this->assertNoRaw($block_title, 'Test block is not visible on a Page node as an anonymous user.');
+
+    $this->drupalGet('node/' . $article->id());
+    $this->assertNoRaw($block_title, 'Test block is not visible on a Article node as an anonymous user.');
+
+    // Check that a block with invalid context is being disabled and that it can
+    // still be edited afterward.
+    $disabled_block = Block::load('thirdtestfor2354889');
+    $this->assertFalse($disabled_block->status(), 'Block with invalid context is disabled');
+
+    $this->assertEqual(['thirdtestfor2354889' => ['missing_context_ids' => ['baloney.spam'], 'status' => TRUE]], \Drupal::keyValue('update_backup')->get('block_update_8001'));
+
+    $disabled_block_visibility = $disabled_block->get('visibility');
+    $this->assertTrue(!isset($disabled_block_visibility['node_type']), 'The problematic visibility condition has been removed.');
+
+    $admin_user = $this->drupalCreateUser(['administer blocks']);
+    $this->drupalLogin($admin_user);
+
+    $this->drupalGet('admin/structure/block/manage/thirdtestfor2354889');
+    $this->assertResponse('200');
+  }
+
+}
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Condition/BaloneySpam.php b/core/modules/block/tests/modules/block_test/src/Plugin/Condition/BaloneySpam.php
new file mode 100644
index 0000000..06ecc8a
--- /dev/null
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Condition/BaloneySpam.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block_test\Plugin\Condition\BaloneySpam.
+ */
+
+namespace Drupal\block_test\Plugin\Condition;
+
+use Drupal\Core\Condition\ConditionPluginBase;
+
+/**
+ * Provides a 'baloney spam' condition.
+ *
+ * @Condition(
+ *   id = "baloney.spam",
+ *   label = @Translation("Baloney spam"),
+ * )
+ *
+ */
+class BaloneySpam extends ConditionPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function evaluate() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function summary() {
+    return 'Summary';
+  }
+
+}
diff --git a/core/modules/system/tests/fixtures/update/block.block.secondtestfor2354889.yml b/core/modules/system/tests/fixtures/update/block.block.secondtestfor2354889.yml
new file mode 100644
index 0000000..1a8a904
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/block.block.secondtestfor2354889.yml
@@ -0,0 +1,23 @@
+uuid: C499B41D-035E-432E-9462-36410C43C49F
+langcode: en
+status: true
+dependencies:
+  module:
+    - search
+  theme:
+    - bartik
+id: secondtestfor2354889
+theme: bartik
+region: sidebar_first
+weight: -6
+provider: null
+plugin: search_form_block
+settings:
+  id: search_form_block
+  label: Search
+  provider: search
+  label_display: visible
+  cache:
+    max_age: -1
+  status: true
+visibility: {  }
diff --git a/core/modules/system/tests/fixtures/update/block.block.testfor2354889.yml b/core/modules/system/tests/fixtures/update/block.block.testfor2354889.yml
new file mode 100644
index 0000000..376b04e
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/block.block.testfor2354889.yml
@@ -0,0 +1,43 @@
+uuid: 9d204071-a923-4707-8200-c298a540fb0c
+langcode: en
+status: true
+dependencies:
+  content:
+    - 'block_content:basic:c1895145-893e-460b-a24e-78cd2cefbb1f'
+  module:
+    - block_content
+    - node
+    - user
+  theme:
+    - bartik
+id: testfor2354889
+theme: bartik
+region: content
+weight: -6
+provider: null
+plugin: 'block_content:c1895145-893e-460b-a24e-78cd2cefbb1f'
+settings:
+  id: 'block_content:c1895145-893e-460b-a24e-78cd2cefbb1f'
+  label: 'Test for 2354889'
+  provider: block_content
+  label_display: visible
+  cache:
+    max_age: -1
+  status: true
+  info: ''
+  view_mode: full
+visibility:
+  node_type:
+    id: node_type
+    bundles:
+      page: page
+    negate: false
+    context_mapping:
+      node: node.node
+  user_role:
+    id: user_role
+    roles:
+      authenticated: authenticated
+    negate: false
+    context_mapping:
+      user: user.current_user
diff --git a/core/modules/system/tests/fixtures/update/block.block.thirdtestfor2354889.yml b/core/modules/system/tests/fixtures/update/block.block.thirdtestfor2354889.yml
new file mode 100644
index 0000000..472e131
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/block.block.thirdtestfor2354889.yml
@@ -0,0 +1,30 @@
+uuid: 4558907D-2918-48FE-B56F-8A007B5FBDD5
+langcode: en
+status: true
+dependencies:
+  module:
+    - user
+  theme:
+    - bartik
+id: thirdtestfor2354889
+theme: bartik
+region: sidebar_first
+weight: -6
+provider: null
+plugin: user_login_block
+settings:
+  id: user_login_block
+  label: 'User login'
+  provider: user
+  label_display: visible
+  cache:
+    max_age: -1
+  status: true
+visibility:
+  node_type:
+    id: node_type
+    bundles:
+      page: page
+    negate: false
+    context_mapping:
+      baloney: baloney.spam
diff --git a/core/modules/system/tests/fixtures/update/drupal-8.block-context-manager-2354889.php b/core/modules/system/tests/fixtures/update/drupal-8.block-context-manager-2354889.php
new file mode 100644
index 0000000..207fc30
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/drupal-8.block-context-manager-2354889.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains database additions to drupal-8.bare.standard.php.gz for testing the
+ * upgrade path of https://www.drupal.org/node/2354889.
+ */
+
+use Drupal\Core\Database\Database;
+
+$connection = Database::getConnection();
+
+// Structure of a custom block with visibility settings.
+
+$block_configs[] = \Drupal\Component\Serialization\Yaml::decode(file_get_contents(__DIR__ . '/block.block.testfor2354889.yml'));
+$block_configs[] = \Drupal\Component\Serialization\Yaml::decode(file_get_contents(__DIR__ . '/block.block.secondtestfor2354889.yml'));
+$block_configs[] = \Drupal\Component\Serialization\Yaml::decode(file_get_contents(__DIR__ . '/block.block.thirdtestfor2354889.yml'));
+
+foreach ($block_configs as $block_config) {
+  $connection->insert('config')
+    ->fields(array(
+      'collection',
+      'name',
+      'data',
+    ))
+    ->values(array(
+      'collection' => '',
+      'name' => 'block.block.' . $block_config['id'],
+      'data' => serialize($block_config),
+    ))
+    ->execute();
+}
