diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 3cbbd82..e63b5c3 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -7,6 +7,7 @@
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\CoreServiceProvider;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
 use Drupal\Core\Database\Install\TaskException;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
@@ -156,14 +157,17 @@ function install_state_defaults() {
     // The last task that was completed during the previous installation
     // request.
     'completed_task' => NULL,
-    // This becomes TRUE only when a valid config directory is created or
-    // detected.
+    // TRUE when there are valid config directories.
     'config_verified' => FALSE,
     // This becomes TRUE only when Drupal's system module is installed.
     'database_tables_exist' => FALSE,
-    // This becomes TRUE only when a valid database connection can be
-    // established.
+    // TRUE when there is a valid database connection.
     'database_verified' => FALSE,
+    // TRUE when a valid settings.php exists (containing both database
+    // connection information and config directory names).
+    'settings_verified' => FALSE,
+    // TRUE when the base system has been installed and is ready to operate.
+    'base_system_verified' => FALSE,
     // Whether a translation file for the selected language will be downloaded
     // from the translation server.
     'download_translation' => FALSE,
@@ -209,10 +213,6 @@ function install_state_defaults() {
     // Tokens in the pattern will be replaced by appropriate values for the
     // required translation file.
     'server_pattern' => 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po',
-    // This becomes TRUE only when a valid settings.php file is written
-    // (containing both valid database connection information and a valid
-    // config directory).
-    'settings_verified' => FALSE,
     // Installation tasks can set this to TRUE to force the page request to
     // end (even if there is no themable output), in the case of an interactive
     // installation. This is needed only rarely; for example, it would be used
@@ -322,48 +322,39 @@ function install_begin_request(&$install_state) {
   require_once __DIR__ . '/install.inc';
   require_once __DIR__ . '/schema.inc';
   require_once __DIR__ . '/../../' . settings()->get('path_inc', 'core/includes/path.inc');
+  require_once __DIR__ . '/cache.inc';
+  require_once __DIR__ . '/database.inc';
+  require_once __DIR__ . '/form.inc';
+  require_once __DIR__ . '/batch.inc';
+  require_once __DIR__ . '/ajax.inc';
 
   // Load module basics (needed for hook invokes).
+  require_once __DIR__ . '/menu.inc';
   include_once __DIR__ . '/module.inc';
   include_once __DIR__ . '/session.inc';
   require_once __DIR__ . '/entity.inc';
 
-  // Determine whether the configuration system is ready to operate.
-  $install_state['config_verified'] = install_verify_config_directory(CONFIG_ACTIVE_DIRECTORY) && install_verify_config_directory(CONFIG_STAGING_DIRECTORY);
-
   // Register the translation services.
   install_register_translation_service($container);
   \Drupal::setContainer($container);
 
-  // Check existing settings.php.
+  // Determine whether base system services are ready to operate.
+  $install_state['config_verified'] = install_verify_config_directory(CONFIG_ACTIVE_DIRECTORY) && install_verify_config_directory(CONFIG_STAGING_DIRECTORY);
   $install_state['database_verified'] = install_verify_database_settings();
   $install_state['settings_verified'] = $install_state['config_verified'] && $install_state['database_verified'];
 
-  // If it is not, replace the configuration storage with the InstallStorage
-  // implementation, for the following reasons:
-  // - The first call into drupal_container() will try to set up the regular
-  //   runtime configuration storage, using the CachedStorage by default. It
-  //   calls config_get_config_directory() to retrieve the config directory to
-  //   use, but that throws an exception, since $config_directories is not
-  //   defined since there is no settings.php yet. If there is a prepared
-  //   settings.php already, then the returned directory still cannot be used,
-  //   because it does not necessarily exist. The installer ensures that it
-  //   exists and is writeable in a later step.
-  // - The installer outputs maintenance theme pages and performs many other
-  //   operations, which try to load configuration. Since there is no active
-  //   configuration yet, and because the configuration system does not have a
-  //   notion of default values at runtime, data is missing in many places. The
-  //   lack of data does not trigger errors, but results in a broken user
-  //   interface (e.g., missing page title, etc).
-  // - The actual configuration data to read during installation is essentially
-  //   the default configuration provided by the installation profile and
-  //   modules (most notably System module). The InstallStorage therefore reads
-  //   from the default configuration directories of extensions.
-  // This override is reverted as soon as the config directory and the
-  // database has been set up successfully.
-  // @see drupal_install_config_directories()
-  // @see install_settings_form_submit()
-  if ($install_state['settings_verified']) {
+  if ($install_state['database_verified']) {
+    try {
+      $system_schema = system_schema();
+      end($system_schema);
+      $table = key($system_schema);
+      $install_state['base_system_verified'] = Database::getConnection()->schema()->tableExists($table);
+    }
+    catch (DatabaseExceptionWrapper $e) {
+    }
+  }
+
+  if ($install_state['base_system_verified']) {
     $kernel = new DrupalKernel('install', drupal_classloader(), FALSE);
     $kernel->boot();
     $container = $kernel->getContainer();
@@ -371,6 +362,9 @@ function install_begin_request(&$install_state) {
     $container->set('string_translator.file_translation', install_file_translation_service());
     $container->get('string_translation')->addTranslator($container->get('string_translator.file_translation'));
   }
+  // Replace services with in-memory implementations and specialized installer
+  // implementations. This service container is reverted to a regular
+  // DrupalKernel in install_bootstrap_full().
   else {
     // @todo Move into a proper Drupal\Core\DependencyInjection\InstallContainerBuilder.
     $container = new ContainerBuilder();
@@ -403,14 +397,6 @@ function install_begin_request(&$install_state) {
         ->register("cache.$bin", 'Drupal\Core\Cache\MemoryBackend')
         ->addArgument($bin);
     }
-
-    // The install process cannot use the database lock backend since the database
-    // is not fully up, so we use a null backend implementation during the
-    // installation process. This will also speed up the installation process.
-    // The site being installed will use the real lock backend when doing AJAX
-    // requests but, except for a WSOD, there is no chance for a a lock to stall
-    // (as opposed to the cache backend) so we can afford having a null
-    // implementation here.
     $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
 
     $container
@@ -437,24 +423,11 @@ function install_begin_request(&$install_state) {
     $container->register('settings', 'Drupal\Component\Utility\Settings')
       ->setFactoryClass('Drupal\Component\Utility\Settings')
       ->setFactoryMethod('getSingleton');
-    $container->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
-      ->addArgument(new Reference('service_container'))
-      ->addArgument(new Reference('settings'));
-    // Register the expirable key value store used by form cache.
+
     $container
-      ->register('keyvalue.expirable', 'Drupal\Core\KeyValueStore\KeyValueExpirableFactory')
-      ->addArgument(new Reference('service_container'))
-      ->addArgument(new Reference('settings'));
-    $container->register('keyvalue.memory', 'Drupal\Core\KeyValueStore\KeyValueMemoryFactory');
+      ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueMemoryFactory');
     $container
-      ->register('keyvalue.expirable.null', 'Drupal\Core\KeyValueStore\KeyValueNullExpirableFactory');
-    $settings = Settings::getSingleton()->getAll();
-    $settings['settings_old'] = $settings;
-    $settings['keyvalue_expirable_default'] = 'keyvalue.expirable.null';
-    // Override the default keyvalue storage to use memory as the database is
-    // not available.
-    $settings['keyvalue_default'] = 'keyvalue.memory';
-    new Settings($settings);
+      ->register('keyvalue.expirable', 'Drupal\Core\KeyValueStore\KeyValueNullExpirableFactory');
 
     $container->register('state', 'Drupal\Core\KeyValueStore\State')
       ->addArgument(new Reference('keyvalue'));
@@ -506,8 +479,6 @@ function install_begin_request(&$install_state) {
     \Drupal::translation()->setDefaultLangcode($install_state['parameters']['langcode']);
   }
 
-  require_once __DIR__ . '/ajax.inc';
-
   $module_handler = \Drupal::moduleHandler();
   if (!$module_handler->moduleExists('system')) {
     // Override the module list with a minimal set of modules.
@@ -515,8 +486,6 @@ function install_begin_request(&$install_state) {
   }
   $module_handler->load('system');
 
-  require_once __DIR__ . '/cache.inc';
-
   // Prepare for themed output. We need to run this at the beginning of the
   // page request to avoid a different theme accidentally getting set. (We also
   // need to run it even in the case of command-line installations, to prevent
@@ -525,10 +494,6 @@ function install_begin_request(&$install_state) {
   drupal_maintenance_theme();
 
   if ($install_state['database_verified']) {
-    // Initialize the database system. Note that the connection
-    // won't be initialized until it is actually requested.
-    require_once __DIR__ . '/database.inc';
-
     // Verify the last completed task in the database, if there is one.
     $task = install_verify_completed_task();
   }
@@ -597,7 +562,7 @@ function install_run_tasks(&$install_state) {
     if (!$install_state['task_not_complete']) {
       $install_state['tasks_performed'][] = $task_name;
       $install_state['installation_finished'] = empty($tasks_to_perform);
-      if ($install_state['database_tables_exist'] && ($task['run'] == INSTALL_TASK_RUN_IF_NOT_COMPLETED || $install_state['installation_finished'])) {
+      if ($task['run'] == INSTALL_TASK_RUN_IF_NOT_COMPLETED || $install_state['installation_finished']) {
         \Drupal::state()->set('install_task', $install_state['installation_finished'] ? 'done' : $task_name);
       }
     }
@@ -626,7 +591,6 @@ function install_run_task($task, &$install_state) {
   $function = $task['function'];
 
   if ($task['type'] == 'form') {
-    require_once __DIR__ . '/form.inc';
     if ($install_state['interactive']) {
       // For interactive forms, build the form and ensure that it will not
       // redirect, since the installer handles its own redirection only after
@@ -704,7 +668,6 @@ function install_run_task($task, &$install_state) {
     // If we are in the middle of processing this batch, keep sending back
     // any output from the batch process, until the task is complete.
     elseif ($current_batch == $function) {
-      include_once __DIR__ . '/batch.inc';
       $output = _batch_page(\Drupal::request());
       // Because Batch API now returns a JSON response for intermediary steps,
       // but the installer doesn't handle Response objects yet, just send the
@@ -825,9 +788,11 @@ function install_tasks($install_state) {
       'run' => $install_state['settings_verified'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED,
     ),
     'install_base_system' => array(
+      'run' => $install_state['base_system_verified'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED,
     ),
+    // All tasks below are executed in a regular, full Drupal environment.
     'install_bootstrap_full' => array(
-      'run' => INSTALL_TASK_RUN_IF_REACHED,
+      'run' => $install_state['base_system_verified'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED,
     ),
     'install_profile_modules' => array(
       'display_name' => count($install_state['profiles']) == 1 ? t('Install site') : t('Installation profile'),
@@ -1297,11 +1262,6 @@ function install_settings_form_submit($form, &$form_state) {
   // Add the config directories to settings.php.
   drupal_install_config_directories($install_state['mode']);
 
-  // The container is about to be rebuilt so we need to unset the keyvalue
-  // storage override that the installer is using.
-  $settings = Settings::getSingleton()->getAll();
-  new Settings($settings['settings_old']);
-
   // Indicate that the settings file has been verified, and check the database
   // for the last completed task, now that we have a valid connection. This
   // last step is important since we want to trigger an error if the new
diff --git a/core/includes/install.inc b/core/includes/install.inc
index f6848c9..c831f20 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -622,8 +622,16 @@ function drupal_verify_profile($install_state) {
  * functions can be made available while other modules are installed.
  */
 function drupal_install_system() {
-  // Create tables.
-  drupal_install_schema('system');
+  // Create base system tables (persistent storage).
+  // Since this task is executed in the early installer environment,
+  // install_run_tasks() cannot record that this task has been completed. Thus,
+  // the interactive installer invokes this task a second time and the fact that it
+  // completed can only be recorded after the second invocation.
+  $schema = drupal_get_schema_unprocessed('system');
+  $table = key($schema);
+  if (!db_table_exists($table)) {
+    drupal_install_schema('system');
+  }
 
   if (!\Drupal::getContainer()->has('kernel')) {
     // Immediately boot a kernel to have real services ready. If there's already
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index f01f78b..99a2d73 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -15,6 +15,7 @@
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\HttpKernel;
 use Drupal\Core\KeyValueStore\KeyValueExpirableFactory;
+use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\StringTranslation\TranslationInterface;
@@ -41,7 +42,7 @@ class FormBuilder implements FormBuilderInterface {
   /**
    * The factory for expirable key value stores used by form cache.
    *
-   * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactory
+   * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
    */
   protected $keyValueExpirableFactory;
 
@@ -123,7 +124,7 @@ class FormBuilder implements FormBuilderInterface {
    *
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
-   * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactory $key_value_expirable_factory
+   * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_expirable_factory
    *   The keyvalue expirable factory.
    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
    *   The event dispatcher.
@@ -136,7 +137,7 @@ class FormBuilder implements FormBuilderInterface {
    * @param \Drupal\Core\HttpKernel $http_kernel
    *   The HTTP kernel.
    */
-  public function __construct(ModuleHandlerInterface $module_handler, KeyValueExpirableFactory $key_value_expirable_factory, EventDispatcherInterface $event_dispatcher, UrlGeneratorInterface $url_generator, TranslationInterface $translation_manager, CsrfTokenGenerator $csrf_token = NULL, HttpKernel $http_kernel = NULL) {
+  public function __construct(ModuleHandlerInterface $module_handler, KeyValueFactoryInterface $key_value_expirable_factory, EventDispatcherInterface $event_dispatcher, UrlGeneratorInterface $url_generator, TranslationInterface $translation_manager, CsrfTokenGenerator $csrf_token = NULL, HttpKernel $http_kernel = NULL) {
     $this->moduleHandler = $module_handler;
     $this->keyValueExpirableFactory = $key_value_expirable_factory;
     $this->eventDispatcher = $event_dispatcher;
diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseFactory.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseFactory.php
index ded528e..27793a1 100644
--- a/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseFactory.php
+++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseFactory.php
@@ -12,7 +12,7 @@
 /**
  * Defines the key/value store factory for the database backend.
  */
-class KeyValueDatabaseFactory {
+class KeyValueDatabaseFactory implements KeyValueFactoryInterface {
 
   /**
    * Constructs this factory object.
diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueFactory.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueFactory.php
index 74ccd6e..ac41128 100644
--- a/core/lib/Drupal/Core/KeyValueStore/KeyValueFactory.php
+++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueFactory.php
@@ -12,7 +12,7 @@
 /**
  * Defines the key/value store factory.
  */
-class KeyValueFactory {
+class KeyValueFactory implements KeyValueFactoryInterface {
 
   /**
    * The specific setting name prefix.
diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueMemoryFactory.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueMemoryFactory.php
index 95c21be..b02e4c0 100644
--- a/core/lib/Drupal/Core/KeyValueStore/KeyValueMemoryFactory.php
+++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueMemoryFactory.php
@@ -10,7 +10,7 @@
 /**
  * Defines the key/value store factory for the database backend.
  */
-class KeyValueMemoryFactory {
+class KeyValueMemoryFactory implements KeyValueFactoryInterface {
 
   /**
    * An array of keyvalue collections that are stored in memory.
diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueNullExpirableFactory.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueNullExpirableFactory.php
index 27310da..c95fbde 100644
--- a/core/lib/Drupal/Core/KeyValueStore/KeyValueNullExpirableFactory.php
+++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueNullExpirableFactory.php
@@ -10,7 +10,7 @@
 /**
  * Defines the key/value store factory for the null backend.
  */
-class KeyValueNullExpirableFactory {
+class KeyValueNullExpirableFactory implements KeyValueFactoryInterface {
 
   /**
    * Constructs a new key/value expirable null storage object for a given
diff --git a/core/lib/Drupal/Core/KeyValueStore/State.php b/core/lib/Drupal/Core/KeyValueStore/State.php
index 9a7d7aa..f143d67 100644
--- a/core/lib/Drupal/Core/KeyValueStore/State.php
+++ b/core/lib/Drupal/Core/KeyValueStore/State.php
@@ -29,10 +29,10 @@ class State implements StateInterface {
   /**
    * Constructs a State object.
    *
-   * @param \Drupal\Core\KeyValueStore\KeyValueFactory $key_value_factory
-   *  The key value store to use.
+   * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
+   *   The key value store to use.
    */
-  function __construct(KeyValueFactory $key_value_factory) {
+  function __construct(KeyValueFactoryInterface $key_value_factory) {
     $this->keyValueStore = $key_value_factory->get('state');
   }
 
