diff --git a/core/includes/update.inc b/core/includes/update.inc
index b0d09a7..8c51d65 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -9,6 +9,7 @@
  */
 
 use Drupal\Component\Graph\Graph;
+use Drupal\Core\Config\FileStorage;
 
 /**
  * Minimum schema version of Drupal 7 required for upgrade to Drupal 8.
@@ -877,64 +878,61 @@ function update_retrieve_dependencies() {
  * Provide a generalised method to migrate variables from Drupal 7 to Drupal 8's
  * configuration management system.
  *
- * @param $config_name
- *   The name of the configuration object to retrieve. The name corresponds to
- *   an XML configuration file. For example, passing "book.admin" will return
- *   the config object containing the contents of book.admin.xml.
- * @param $variable_map
- *   An array to map new to old configuration naming conventions. Example:
+ * @param string $config_name
+ *   The configuration object name to retrieve.
+ * @param array $variable_map
+ *   An associative array that maps old variables names to new configuration
+ *   object keys; e.g.:
  *   @code
- *     array('new_config' => 'old_config')
+ *     array('old_variable' => 'new_config.sub_key')
  *   @endcode
- *   This would update the value for new_config to the value old_config has in
- *   the variable table.
+ *   This would migrate the value contained in variable name 'old_variable' into
+ *   the data key 'new_config.sub_key' of the configuration object $config_name.
  */
-function update_variables_to_config($config_name, array $variable_map = array()) {
+function update_variables_to_config($config_name, array $variable_map) {
+  // Build the new configuration object.
+  // This potentially loads an existing configuration object, in case another
+  // update function migrated configuration values into $config_name already.
   $config = config($config_name);
-  $config_data = array_keys($config->get());
-
-  if (!empty($config_data)) {
-    // Build a list of variables to select from the database and build a mapping
-    // of variable names to config keys.
-    foreach ($config_data as $config_key) {
-      if (isset($variable_map[$config_key])) {
-        $variables[] = $variable_map[$config_key];
-        $config_keys[$variable_map[$config_key]] = $config_key;
-      }
-      else {
-        $variables[] = $config_key;
-        $config_keys[$config_key] = $config_key;
-      }
-    }
-
-    // Get any variables currently defined that match the new setting names in
-    // the config file.
-    $query = db_select('variable', 'v')
-      ->fields('v')
-      ->condition('name', $variables, 'IN');
-
-    $var_values = $query->execute()->fetchAllKeyed(0);
-    if (!empty($var_values)) {
-      // Update the config system settings to use the values previously stored in
-      // the variable table.
-      try {
-        foreach($var_values as $name => $val) {
-          $config->set($config_keys[$name], unserialize($val));
-        }
-        $config->save();
-        // Delete the old variables. The config system will throw an exception if a
-        // value cannot be saved, so this code will not run if there is a problem
-        // running the update.
-        $del = db_delete('variable')->condition('name', $variables, 'IN');
-        $del->execute();
-      }
-      // @TODO We may want to do different error handling for different
-      // exception types, but for now we'll just log the exception.
-      catch (Exception $e) {
-        watchdog_exception('update', $e);
-      }
+  $original_data = $config->get();
+
+  // Extract the module namespace/owner from the configuration object name.
+  $module = strtok($config_name, '.');
+
+  // Load and set default configuration values.
+  // Throws a FileStorageReadException if there is no default configuration
+  // file, which is required to exist.
+  $file = new FileStorage($config_name);
+  $file->setPath(drupal_get_path('module', $module) . '/config');
+  $default_data = $file->read();
+
+  // Merge any possibly existing original data into default values.
+  // Only relevant when being called repetitively on the same config object.
+  $data = drupal_array_merge_deep($default_data, $original_data);
+
+  // Apply the default values.
+  $config->setData($data);
+
+  // Fetch existing variables.
+  $variables = db_query('SELECT name, value FROM {variable} WHERE name IN (:variables)', array(':variables' => array_keys($variable_map)))->fetchAllKeyed();
+
+  // Set configuration values according to the provided variable mapping.
+  foreach ($variable_map as $variable_name => $config_key) {
+    // This function migrates variables regardless of their value, including
+    // NULL values. Any possibly required customizations need to be performed
+    // manually, either via variable_set() before calling this function or via
+    // config() after calling this function.
+    if (isset($variables[$variable_name])) {
+      $value = unserialize($variables[$variable_name]);
+      $config->set($config_key, $value);
     }
   }
+
+  // Save the configuration object.
+  $config->save();
+
+  // Delete the migrated variables.
+  db_delete('variable')->condition('name', array_keys($variable_map), 'IN')->execute();
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php
index 5e2ac1e..ed53f4b 100644
--- a/core/lib/Drupal/Core/Config/FileStorage.php
+++ b/core/lib/Drupal/Core/Config/FileStorage.php
@@ -105,13 +105,14 @@ class FileStorage {
    */
   public function read() {
     if (!$this->exists()) {
-      throw new FileStorageReadException('Configuration file does not exist.');
+      // Use var_export to quote $this->name to separate from text.
+      throw new FileStorageReadException('Configuration file ' . var_export($this->name, TRUE) . ' does not exist.');
     }
 
     $data = file_get_contents($this->getFilePath());
     $data = $this->decode($data);
     if ($data === FALSE) {
-      throw new FileStorageReadException('Unable to decode configuration file.');
+      throw new FileStorageReadException('Failed to decode configuration file ' . var_export($this->name, TRUE) . '.');
     }
     return $data;
   }
diff --git a/core/modules/block/block.install b/core/modules/block/block.install
index 632e106..7544c33 100644
--- a/core/modules/block/block.install
+++ b/core/modules/block/block.install
@@ -224,6 +224,8 @@ function block_install() {
 
 /**
  * Block cache is always enabled in 8.x.
+ *
+ * @ingroup config_upgrade
  */
 function block_update_8000() {
   variable_del('block_cache');
diff --git a/core/modules/config/config.test b/core/modules/config/config.test
index 257ab0f..26092d1 100644
--- a/core/modules/config/config.test
+++ b/core/modules/config/config.test
@@ -315,16 +315,15 @@ class ConfOverrideTestCase extends WebTestBase {
 }
 
 /**
- * Tests function providing configuration upgrade from Drupal 7 to 8.
+ * Tests migration of variables into configuration objects.
  */
-class ConfUpdate7to8TestCase extends WebTestBase {
+class ConfigUpgradeTestCase extends WebTestBase {
   protected $testContent = 'Olá, Sao Paulo!';
 
   public static function getInfo() {
     return array(
-      'name' => 'Configuration update from Drupal 7 to 8',
-      'description' => 'Tests the ability to update Drupal 7 variables to the
-        configuration management system.',
+      'name' => 'Variable migration',
+      'description' => 'Tests migration of variables into configuration objects.',
       'group' => 'Configuration',
     );
   }
@@ -335,18 +334,32 @@ class ConfUpdate7to8TestCase extends WebTestBase {
   }
 
   /**
-   * Test configuration update function.
+   * Tests update_variables_to_config().
    */
   function testConfigurationUpdate() {
     // Ensure that the variable table has the object. The variable table will
     // remain in place for Drupal 8 to provide an upgrade path for overridden
     // variables.
-    db_merge('variable')->key(array('name' => 'config_test_foo'))->fields(array('value' => serialize($this->testContent)))->execute();
-    db_merge('variable')->key(array('name' => 'config_bar'))->fields(array('value' => serialize($this->testContent)))->execute();
-
-    update_variables_to_config('config.test', array('config_test_bar' => 'config_bar'));
-    $config = config('config.test');
-    $this->assertEqual($config->get('config_test_foo'), $this->testContent);
-    $this->assertEqual($config->get('config_test_bar'), $this->testContent);
+    db_insert('variable')
+      ->fields(array('name', 'value'))
+      ->values(array('config_upgrade_foo', serialize($this->testContent)))
+      ->values(array('config_upgrade_bar', serialize($this->testContent)))
+      ->execute();
+
+    update_variables_to_config('config_upgrade.test', array(
+      'config_upgrade_bar' => 'parent.bar',
+      'config_upgrade_foo' => 'foo',
+      // Non-existing variable.
+      'config_upgrade_baz' => 'parent.baz',
+    ));
+
+    $config = config('config_upgrade.test');
+    $this->assertIdentical($config->get('foo'), $this->testContent);
+    $this->assertIdentical($config->get('parent.bar'), $this->testContent);
+    $this->assertIdentical($config->get('parent.baz'), 'Baz');
+
+    // Verify that variables have been deleted.
+    $variables = db_query('SELECT name FROM {variable} WHERE name IN (:names)', array(':names' => array('config_upgrade_bar', 'config_upgrade_foo')))->fetchCol();
+    $this->assertFalse($variables);
   }
 }
diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install
index b29bdee..ac03baa 100644
--- a/core/modules/locale/locale.install
+++ b/core/modules/locale/locale.install
@@ -157,6 +157,8 @@ function locale_update_8000() {
 
 /**
  * Language type 'language' renamed to 'language_interface'.
+ *
+ * @ingroup config_upgrade
  */
 function locale_update_8001() {
   // Only change language_types if we had this setting saved. Keep order
@@ -242,6 +244,8 @@ function locale_update_8002() {
 
 /**
  * Converts language domains to new format.
+ *
+ * @ingroup config_upgrade
  */
 function locale_update_8003() {
   $message = '';
@@ -274,6 +278,8 @@ function locale_update_8003() {
 
 /**
  * Rename language providers to language negotiation methods.
+ *
+ * @ingroup config_upgrade
  */
 function locale_update_8004() {
   $types = variable_get('language_types', NULL);
@@ -437,6 +443,8 @@ function locale_update_8006() {
 
 /**
  * Convert language_negotiation_* variables to use the new callbacks.
+ *
+ * @ingroup config_upgrade
  */
 function locale_update_8007() {
   $variable_names = array(
@@ -510,6 +518,8 @@ function locale_update_8007() {
 
 /**
  * Rename the option variables of the locale language negotiation.
+ *
+ * @ingroup config_upgrade
  */
 function locale_update_8008() {
   $variable_name_map = array(
diff --git a/core/modules/node/node.install b/core/modules/node/node.install
index ee89059..6f806e9 100644
--- a/core/modules/node/node.install
+++ b/core/modules/node/node.install
@@ -511,26 +511,11 @@ function _update_7000_node_get_types() {
  */
 
 /**
- * Set 'node' as front page path if it implicitly was before.
- *
- * Node module became optional. The default front page path was changed to
- * 'user'. Since 'node' was the implicit default front page path previously and
- * may not have been explicitly configured as such, this update ensures that the
- * old implicit default is still the default.
- *
- * @see http://drupal.org/node/375397
- */
-function node_update_8000() {
-  $front_page = variable_get('site_frontpage');
-  if (!isset($front_page)) {
-    variable_set('site_frontpage', 'node');
-  }
-}
-
-/**
  * Rename node type language variable names.
  *
  * @see http://drupal.org/node/540294
+ *
+ * @ingroup config_upgrade
  */
 function node_update_8001() {
   $types = db_query('SELECT type FROM {node_type}')->fetchCol();
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 5581d01..84822c7 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -1688,6 +1688,17 @@ function system_update_last_removed() {
 }
 
 /**
+ * @defgroup config_upgrade Configuration system upgrade functions
+ * @{
+ * Module update functions that
+ * - update variables prior to configuration system conversions
+ * - convert variables to the new configuration system
+ * - update configuration system values after conversion
+ *
+ * @} End of "defgroup config_upgrade".
+ */
+
+/**
  * @defgroup updates-7.x-to-8.x Updates from 7.x to 8.x
  * @{
  * Update functions from 7.x to 8.x.
@@ -1713,7 +1724,14 @@ function system_update_8001() {
 }
 
 /**
- * Set Bartik as default theme if it implicitly was the default before.
+ * Set 'node' as front page path and Bartik as default theme if it implicitly was before.
+ *
+ * Node module became optional. The default front page path was changed to
+ * 'user'. Since 'node' was the implicit default front page path previously and
+ * may not have been explicitly configured as such, this update ensures that the
+ * old implicit default is still the default.
+ *
+ * @see http://drupal.org/node/375397
  *
  * The default theme for Drupal core was changed from Bartik to Stark.
  * Installation profiles (including Standard and Minimal) were changed to
@@ -1723,8 +1741,14 @@ function system_update_8001() {
  * default.
  *
  * @see http://drupal.org/node/1181776
+ *
+ * @ingroup config_upgrade
  */
 function system_update_8002() {
+  $front_page = variable_get('site_frontpage');
+  if (!isset($front_page)) {
+    variable_set('site_frontpage', 'node');
+  }
   $theme = variable_get('theme_default');
   if (!isset($theme)) {
     variable_set('theme_default', 'bartik');
@@ -1864,16 +1888,30 @@ function system_update_8008() {
 
 /**
  * Moves cron system settings from variable to config.
+ *
+ * @ingroup config_upgrade
  */
 function system_update_8009() {
-  update_variables_to_config('system.cron');
+  update_variables_to_config('system.cron', array(
+    'cron_max_threshold' => 'cron_max_threshold',
+    'cron_safe_threshold' => 'cron_safe_threshold',
+    'cron_threshold_warning' => 'cron_threshold_warning',
+    'cron_threshold_error' => 'cron_threshold_error',
+    'cron_key' => 'cron_key',
+  ));
 }
 
 /**
- * Moves system settings from variable to config.
+ * Moves RSS system settings from variable to config.
+ *
+ * @ingroup config_upgrade
  */
 function system_update_8010() {
-  update_variables_to_config('system.rss-publishing');
+  update_variables_to_config('system.rss-publishing', array(
+    'feed_description' => 'feed_description',
+    'feed_default_items' => 'feed_default_items',
+    'feed_item_length' => 'feed_item_length',
+  ));
 }
 
 /**
diff --git a/core/modules/system/tests/modules/config_upgrade/config/config.test.yml b/core/modules/system/tests/modules/config_upgrade/config/config.test.yml
deleted file mode 100644
index 4e5eb16..0000000
--- a/core/modules/system/tests/modules/config_upgrade/config/config.test.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-config_test_foo: bar
-config_test_bar: foo
diff --git a/core/modules/system/tests/modules/config_upgrade/config/config_upgrade.test.yml b/core/modules/system/tests/modules/config_upgrade/config/config_upgrade.test.yml
new file mode 100644
index 0000000..3957538
--- /dev/null
+++ b/core/modules/system/tests/modules/config_upgrade/config/config_upgrade.test.yml
@@ -0,0 +1,4 @@
+parent:
+    bar: Bar
+    baz: Baz
+foo: Foo
diff --git a/core/modules/system/tests/upgrade/upgrade.test b/core/modules/system/tests/upgrade/upgrade.test
index 2f942fa..429bddb 100644
--- a/core/modules/system/tests/upgrade/upgrade.test
+++ b/core/modules/system/tests/upgrade/upgrade.test
@@ -128,6 +128,7 @@ abstract class UpgradePathTestCase extends WebTestBase {
     $this->prepareD8Session();
 
     // Restore necessary variables.
+    // @todo Convert into config('system.site')->set('mail')?
     $this->variable_set('site_mail', 'simpletest@example.com');
 
     drupal_set_time_limit($this->timeLimit);
@@ -142,6 +143,8 @@ abstract class UpgradePathTestCase extends WebTestBase {
    * @param $value
    *   The value to set. This can be any PHP data type; these functions take care
    *   of serialization as necessary.
+   *
+   * @todo Update for D8 configuration system.
    */
   protected function variable_set($name, $value) {
     db_delete('variable')
