diff --git a/core/core.services.yml b/core/core.services.yml
index 796ece3e9b..c9505228d7 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -303,7 +303,7 @@ services:
       - { name: event_subscriber }
   config.installer:
     class: Drupal\Core\Config\ConfigInstaller
-    arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '%install_profile%']
+    arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '%install_profile%', '@profile_handler']
     lazy: true
   config.storage:
     class: Drupal\Core\Config\CachedStorage
@@ -521,6 +521,9 @@ services:
       - { name: module_install.uninstall_validator }
     arguments: ['@string_translation']
     lazy: true
+  profile_handler:
+    class: Drupal\Core\Extension\ProfileHandler
+    arguments: ['@app.root', '@info_parser']
   theme_handler:
     class: Drupal\Core\Extension\ThemeHandler
     arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser']
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index cfc0497bbc..bae5acfde2 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -443,6 +443,12 @@ function install_begin_request($class_loader, &$install_state) {
     if (isset($install_state['profile_info']['distribution']['install']['theme'])) {
       $install_state['theme'] = $install_state['profile_info']['distribution']['install']['theme'];
     }
+    // Ensure all profile directories are registered.
+    $profiles = \Drupal::service('profile_handler')->getProfiles($profile);
+    $profile_directories = array_map(function($extension) {
+      return $extension->getPath();
+    }, $profiles);
+    $listing->setProfileDirectories($profile_directories);
   }
 
   // Use the language from the profile configuration, if available, to override
@@ -1210,6 +1216,8 @@ function install_select_profile(&$install_state) {
  *   - For non-interactive installations via install_drupal() settings.
  * - A discovered profile that is a distribution. If multiple profiles are
  *   distributions, then the first discovered profile will be selected.
+ *   If an inherited profile is detected that is a distribution, it will be
+ *   chosen over its base profile.
  * - Only one visible profile is available.
  *
  * @param array $install_state
@@ -1232,11 +1240,8 @@ function _install_select_profile(&$install_state) {
     }
   }
   // Check for a distribution profile.
-  foreach ($install_state['profiles'] as $profile) {
-    $profile_info = install_profile_info($profile->getName());
-    if (!empty($profile_info['distribution'])) {
-      return $profile->getName();
-    }
+  if ($distribution = \Drupal::service('profile_handler')->selectDistribution(array_keys($install_state['profiles']))) {
+    return $distribution;
   }
 
   // Get all visible (not hidden) profiles.
@@ -1569,7 +1574,10 @@ function install_profile_themes(&$install_state) {
  *   An array of information about the current installation state.
  */
 function install_install_profile(&$install_state) {
-  \Drupal::service('module_installer')->install([drupal_get_profile()], FALSE);
+  // Install all the profiles.
+  $profiles = \Drupal::service('profile_handler')->getProfiles();
+  \Drupal::service('module_installer')->install(array_keys($profiles), FALSE);
+
   // Install all available optional config. During installation the module order
   // is determined by dependencies. If there are no dependencies between modules
   // then the order in which they are installed is dependent on random factors
diff --git a/core/includes/install.inc b/core/includes/install.inc
index c5b93b58a6..ac4b26528e 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -1043,6 +1043,14 @@ function drupal_check_module($module) {
  *   - install: Optional parameters to override the installer:
  *     - theme: The machine name of a theme to use in the installer instead of
  *       Drupal's default installer theme.
+ * - base profile: Existence of this key denotes that the installation profile
+ *   depends on a parent installation profile.
+ *   - name: The shortname of the base installation profile.
+ *   - excluded_dependencies: An array of shortnames of other modules that have
+ *     to be excluded from the base profile requirements. This allows e.g. to
+ *     disable a demo module that would be installed by the base profile.
+ *   If there are no excluded_dependencies, a shortcut of "base profile: name"
+ *   can be used.
  *
  * Note that this function does an expensive file system scan to get info file
  * information for dependencies. If you only need information from the info
@@ -1069,18 +1077,7 @@ function install_profile_info($profile, $langcode = 'en') {
   $cache = &drupal_static(__FUNCTION__, []);
 
   if (!isset($cache[$profile][$langcode])) {
-    // Set defaults for module info.
-    $defaults = [
-      'dependencies' => [],
-      'themes' => ['stark'],
-      'description' => '',
-      'version' => NULL,
-      'hidden' => FALSE,
-      'php' => DRUPAL_MINIMUM_PHP,
-    ];
-    $profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml";
-    $info = \Drupal::service('info_parser')->parse($profile_file);
-    $info += $defaults;
+    $info = \Drupal::service('profile_handler')->getProfileInfo($profile);
 
     // drupal_required_modules() includes the current profile as a dependency.
     // Remove that dependency, since a module cannot depend on itself.
diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php
index b023764ebf..bcb2144df7 100644
--- a/core/lib/Drupal/Core/Config/ConfigInstaller.php
+++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php
@@ -6,6 +6,7 @@
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Config\Entity\ConfigDependencyManager;
 use Drupal\Core\Config\Entity\ConfigEntityDependency;
+use Drupal\Core\Extension\ProfileHandlerInterface;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
 class ConfigInstaller implements ConfigInstallerInterface {
@@ -53,6 +54,13 @@ class ConfigInstaller implements ConfigInstallerInterface {
   protected $sourceStorage;
 
   /**
+   * The profile handler.
+   *
+   * @var \Drupal\Core\Extension\ProfileHandlerInterface
+   */
+  protected $profileHandler;
+
+  /**
    * Is configuration being created as part of a configuration sync.
    *
    * @var bool
@@ -81,14 +89,17 @@ class ConfigInstaller implements ConfigInstallerInterface {
    *   The event dispatcher.
    * @param string $install_profile
    *   The name of the currently active installation profile.
+   * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler
+   *   (optional) The profile handler.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, $install_profile) {
+  public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, $install_profile, ProfileHandlerInterface $profile_handler = NULL) {
     $this->configFactory = $config_factory;
     $this->activeStorages[$active_storage->getCollectionName()] = $active_storage;
     $this->typedConfig = $typed_config;
     $this->configManager = $config_manager;
     $this->eventDispatcher = $event_dispatcher;
     $this->installProfile = $install_profile;
+    $this->profileHandler = $profile_handler ?: \Drupal::service('profile_handler');
   }
 
   /**
@@ -471,7 +482,8 @@ public function checkConfigurationToInstall($type, $name) {
 
     // Install profiles can not have config clashes. Configuration that
     // has the same name as a module's configuration will be used instead.
-    if ($name != $this->drupalGetProfile()) {
+    $profiles = $this->profileHandler->getProfiles();
+    if (!isset($profiles[$name])) {
       // Throw an exception if the module being installed contains configuration
       // that already exists. Additionally, can not continue installing more
       // modules because those may depend on the current module being installed.
diff --git a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php
index 9103ab8c07..2ad1ae35a0 100644
--- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php
+++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php
@@ -3,6 +3,7 @@
 namespace Drupal\Core\Config;
 
 use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\Extension\ProfileHandlerInterface;
 
 /**
  * Storage to access configuration and schema in enabled extensions.
@@ -52,9 +53,11 @@ class ExtensionInstallStorage extends InstallStorage {
    *   (optional) The current installation profile. This parameter will be
    *   mandatory in Drupal 9.0.0. In Drupal 8.3.0 not providing this parameter
    *   will trigger a silenced deprecation warning.
+   * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler
+   *   (optional) The profile handler.
    */
-  public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE, $profile = NULL) {
-    parent::__construct($directory, $collection);
+  public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE, $profile = NULL, ProfileHandlerInterface $profile_handler = NULL) {
+    parent::__construct($directory, $collection, $profile_handler);
     $this->configStorage = $config_storage;
     $this->includeProfile = $include_profile;
     if (is_null($profile)) {
@@ -93,19 +96,11 @@ protected function getAllFolders() {
 
       $extensions = $this->configStorage->read('core.extension');
       // @todo Remove this scan as part of https://www.drupal.org/node/2186491
-      $listing = new ExtensionDiscovery(\Drupal::root());
+      $listing = new ExtensionDiscovery(\Drupal::root(), TRUE, NULL, NULL, $this->profileHandler);
       if (!empty($extensions['module'])) {
         $modules = $extensions['module'];
         // Remove the install profile as this is handled later.
         unset($modules[$this->installProfile]);
-        $profile_list = $listing->scan('profile');
-        if ($this->installProfile && isset($profile_list[$this->installProfile])) {
-          // Prime the drupal_get_filename() static cache with the profile info
-          // file location so we can use drupal_get_path() on the active profile
-          // during the module scan.
-          // @todo Remove as part of https://www.drupal.org/node/2186491
-          drupal_get_filename('profile', $this->installProfile, $profile_list[$this->installProfile]->getPathname());
-        }
         $module_list_scan = $listing->scan('module');
         $module_list = [];
         foreach (array_keys($modules) as $module) {
@@ -126,18 +121,11 @@ protected function getAllFolders() {
       }
 
       if ($this->includeProfile) {
-        // The install profile can override module default configuration. We do
-        // this by replacing the config file path from the module/theme with the
-        // install profile version if there are any duplicates.
-        if ($this->installProfile) {
-          if (!isset($profile_list)) {
-            $profile_list = $listing->scan('profile');
-          }
-          if (isset($profile_list[$this->installProfile])) {
-            $profile_folders = $this->getComponentNames([$profile_list[$this->installProfile]]);
-            $this->folders = $profile_folders + $this->folders;
-          }
-        }
+        // The install profile (and any parent profiles) can override module
+        // default configuration. We do this by replacing the config file path
+        // from the module/theme with the install profile version if there are
+        // any duplicates.
+        $this->folders += $this->getComponentNames($this->profileHandler->getProfiles($this->installProfile));
       }
     }
     return $this->folders;
diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php
index 9fea3df473..fad317a4d9 100644
--- a/core/lib/Drupal/Core/Config/InstallStorage.php
+++ b/core/lib/Drupal/Core/Config/InstallStorage.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ProfileHandlerInterface;
 
 /**
  * Storage used by the Drupal installer.
@@ -48,6 +49,13 @@ class InstallStorage extends FileStorage {
   protected $directory;
 
   /**
+   * The profile handler used to find additional folders to scan for config.
+   *
+   * @var \Drupal\Core\Extension\ProfileHandlerInterface
+   */
+  protected $profileHandler;
+
+  /**
    * Constructs an InstallStorage object.
    *
    * @param string $directory
@@ -56,9 +64,14 @@ class InstallStorage extends FileStorage {
    * @param string $collection
    *   (optional) The collection to store configuration in. Defaults to the
    *   default collection.
+   * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler
+   *   (optional) The profile handler.
    */
-  public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION) {
+  public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, ProfileHandlerInterface $profile_handler = NULL) {
     parent::__construct($directory, $collection);
+    if (\Drupal::hasService('profile_handler')) {
+      $this->profileHandler = $profile_handler ?: \Drupal::service('profile_handler');
+    }
   }
 
   /**
@@ -151,21 +164,12 @@ protected function getAllFolders() {
     if (!isset($this->folders)) {
       $this->folders = [];
       $this->folders += $this->getCoreNames();
+      // Get dependent profiles and add the extension components.
+      $this->folders += $this->getComponentNames($this->profileHandler->getProfiles());
       // Perform an ExtensionDiscovery scan as we cannot use drupal_get_path()
       // yet because the system module may not yet be enabled during install.
       // @todo Remove as part of https://www.drupal.org/node/2186491
       $listing = new ExtensionDiscovery(\Drupal::root());
-      if ($profile = drupal_get_profile()) {
-        $profile_list = $listing->scan('profile');
-        if (isset($profile_list[$profile])) {
-          // Prime the drupal_get_filename() static cache with the profile info
-          // file location so we can use drupal_get_path() on the active profile
-          // during the module scan.
-          // @todo Remove as part of https://www.drupal.org/node/2186491
-          drupal_get_filename('profile', $profile, $profile_list[$profile]->getPathname());
-          $this->folders += $this->getComponentNames([$profile_list[$profile]]);
-        }
-      }
       // @todo Remove as part of https://www.drupal.org/node/2186491
       $this->folders += $this->getComponentNames($listing->scan('module'));
       $this->folders += $this->getComponentNames($listing->scan('theme'));
diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
index e948aa584c..4cd3d4195e 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
@@ -111,33 +111,67 @@ protected function validateModules(ConfigImporter $config_importer) {
       }
     }
 
-    // Get the install profile from the site's configuration.
+    // Get the active install profile from the site's configuration.
     $current_core_extension = $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension');
-    $install_profile = isset($current_core_extension['profile']) ? $current_core_extension['profile'] : NULL;
+    if (isset($current_core_extension['profile'])) {
+      // Ensure the active profile is not changing.
+      if ($current_core_extension['profile'] !== $core_extension['profile']) {
+        $config_importer->logError($this->t('Cannot change the install profile from %new_profile to %profile once Drupal is installed.', ['%profile' => $current_core_extension['profile'], '%new_profile' => $core_extension['profile']]));
+      }
+    }
+
+    // Get a list of parent profiles and the main profile.
+    /* @var $profiles \Drupal\Core\Extension\Extension[] */
+    $profiles = \Drupal::service('profile_handler')->getProfiles();
+    /* @var $main_profile \Drupal\Core\Extension\Extension */
+    $main_profile = end($profiles);
 
     // Ensure that all modules being uninstalled are not required by modules
     // that will be installed after the import.
     $uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall');
     foreach ($uninstalls as $module) {
       foreach (array_keys($module_data[$module]->required_by) as $dependent_module) {
-        if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE) && $dependent_module !== $install_profile) {
-          $module_name = $module_data[$module]->info['name'];
-          $dependent_module_name = $module_data[$dependent_module]->info['name'];
-          $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', ['%module' => $module_name, '%dependent_module' => $dependent_module_name]));
+        if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE)) {
+          if (!array_key_exists($dependent_module, $profiles)) {
+            $module_name = $module_data[$module]->info['name'];
+            $dependent_module_name = $module_data[$dependent_module]->info['name'];
+            $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', [
+              '%module' => $module_name,
+              '%dependent_module' => $dependent_module_name
+            ]));
+          }
         }
       }
     }
 
-    // Ensure that the install profile is not being uninstalled.
-    if (in_array($install_profile, $uninstalls, TRUE)) {
-      $profile_name = $module_data[$install_profile]->info['name'];
-      $config_importer->logError($this->t('Unable to uninstall the %profile profile since it is the install profile.', ['%profile' => $profile_name]));
-    }
+    // Don't allow profiles to be uninstalled. It's possible for no profile to
+    // be set yet if the  config is being imported during initial site install.
+    if (isset($main_profile)) {
+      if (in_array($main_profile->getName(), $uninstalls, TRUE)) {
+        // Ensure that the active profile is not being uninstalled.
+        $profile_name = $main_profile->info['name'];
+        $config_importer->logError($this->t('Unable to uninstall the %profile profile since it is the main install profile.', ['%profile' => $profile_name]));
+      }
 
-    // Ensure the profile is not changing.
-    if ($install_profile !== $core_extension['profile']) {
-      $config_importer->logError($this->t('Cannot change the install profile from %new_profile to %profile once Drupal is installed.', ['%profile' => $install_profile, '%new_profile' => $core_extension['profile']]));
+      if ($profile_uninstalls = array_intersect_key($profiles, array_flip($uninstalls))) {
+        // Ensure that none of the parent profiles are being uninstalled.
+        $profile_names = [];
+        foreach ($profile_uninstalls as $profile) {
+          if ($profile->getName() !== $main_profile->getName()) {
+            $profile_names[] = $module_data[$profile->getName()]->info['name'];
+          }
+        }
+        if (!empty($profile_names)) {
+          $message = $this->formatPlural(count($profile_names),
+            'Unable to uninstall the :profile profile since it is a parent of another installed profile.',
+            'Unable to uninstall the :profile profiles since they are parents of another installed profile.',
+            [':profile' => implode(', ', $profile_names)]
+          );
+          $config_importer->logError($message);
+        }
+      }
     }
+
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
index b74fc04f1d..1996db4de9 100644
--- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -92,6 +92,15 @@ class ExtensionDiscovery {
   protected $sitePath;
 
   /**
+   * The profile handler.
+   *
+   * Used to determine the directories in which we want to scan for modules.
+   *
+   * @var \Drupal\Core\Extension\ProfileHandlerInterface|null
+   */
+  protected $profileHandler;
+
+  /**
    * Constructs a new ExtensionDiscovery object.
    *
    * @param string $root
@@ -102,12 +111,27 @@ class ExtensionDiscovery {
    *   The available profile directories
    * @param string $site_path
    *   The path to the site.
+   * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler
+   *   (optional) The profile handler.
    */
-  public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) {
+  public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL, ProfileHandlerInterface $profile_handler = NULL) {
     $this->root = $root;
     $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL;
     $this->profileDirectories = $profile_directories;
     $this->sitePath = $site_path;
+
+    // ExtensionDiscovery can be used without a service container
+    // (@drupalKernel::moduleData), so create a fallback profile handler if the
+    // profile_handler service is unavailable.
+    if ($profile_handler) {
+      $this->profileHandler = $profile_handler;
+    }
+    elseif (\Drupal::hasService('profile_handler')) {
+      $this->profileHandler = \Drupal::service('profile_handler');
+    }
+    else {
+      $this->profileHandler = new FallbackProfileHandler($root);
+    }
   }
 
   /**
@@ -241,7 +265,11 @@ public function setProfileDirectoriesFromSettings() {
     // In case both profile directories contain the same extension, the actual
     // profile always has precedence.
     if ($profile) {
-      $this->profileDirectories[] = drupal_get_path('profile', $profile);
+      $profiles = $this->profileHandler->getProfiles($profile);
+      $profile_directories = array_map(function($extension) {
+        return $extension->getPath();
+      }, $profiles);
+      $this->profileDirectories = array_unique(array_merge($profile_directories, $this->profileDirectories));
     }
     return $this;
   }
diff --git a/core/lib/Drupal/Core/Extension/FallbackProfileHandler.php b/core/lib/Drupal/Core/Extension/FallbackProfileHandler.php
new file mode 100644
index 0000000000..f858ca21ab
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/FallbackProfileHandler.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+/**
+ * Implementation of the profile handler usable before any working system.
+ */
+class FallbackProfileHandler implements ProfileHandlerInterface {
+
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $root;
+
+  /**
+   * Creates a new FallbackProfileHandler instance.
+   *
+   * @param string $root
+   *   The app root.
+   */
+  public function __construct($root) {
+    $this->root = $root;
+  }
+
+  /**
+   * The stored profile info.
+   *
+   * @var array[]
+   */
+  protected $profileInfo = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProfileInfo($profile) {
+    if (isset($this->profileInfo[$profile])) {
+      return $this->profileInfo[$profile];
+    }
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProfileInfo($profile, array $info) {
+    $this->profileInfo[$profile] = $info;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearProfileCache() {
+    unset($this->profileInfo);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProfiles($profile = NULL) {
+    $profile_path = drupal_get_path('profile', $profile);
+    return [
+      $profile => new Extension($this->root, 'profile', $profile_path),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function selectDistribution(array $profile_list) {
+    return NULL;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index f9500a5fc7..2dc55d96eb 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -330,7 +330,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
     if ($uninstall_dependents) {
       // Add dependent modules to the list. The new modules will be processed as
       // the while loop continues.
-      $profile = drupal_get_profile();
+      $profiles = \Drupal::service('profile_handler')->getProfiles();
       while (list($module) = each($module_list)) {
         foreach (array_keys($module_data[$module]->required_by) as $dependent) {
           if (!isset($module_data[$dependent])) {
@@ -338,8 +338,8 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
             return FALSE;
           }
 
-          // Skip already uninstalled modules.
-          if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) {
+          // Skip already uninstalled modules and dependencies of profiles.
+          if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && (!array_key_exists($dependent, $profiles))) {
             $module_list[$dependent] = $dependent;
           }
         }
diff --git a/core/lib/Drupal/Core/Extension/ProfileHandler.php b/core/lib/Drupal/Core/Extension/ProfileHandler.php
new file mode 100644
index 0000000000..b429972fdb
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ProfileHandler.php
@@ -0,0 +1,301 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+/**
+ * Class that manages profiles in a Drupal installation.
+ */
+class ProfileHandler implements ProfileHandlerInterface {
+
+  /**
+   * Stores profiles with their parents for caching purposes.
+   *
+   * This cache stores the profile extensions keyed by base profile.
+   *
+   * @var \Drupal\Core\Extension\Extension[][]
+   */
+  protected $profilesWithParentsCache = [];
+
+  /**
+   * Cache for processing info files.
+   *
+   * @var array
+   */
+  protected $infoCache = [];
+
+  /**
+   * Whether we have primed the filename cache.
+   *
+   * @var bool
+   */
+  protected $scanCache = FALSE;
+
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $root;
+
+  /**
+   * The info parser to parse the profile info.yml files.
+   *
+   * @var \Drupal\Core\Extension\InfoParserInterface
+   */
+  protected $infoParser;
+
+  /**
+   * An extension discovery instance.
+   *
+   * @var \Drupal\Core\Extension\ExtensionDiscovery
+   */
+  protected $extensionDiscovery;
+
+  /**
+   * Local variable used to set profile weights
+   *
+   * @var int
+   */
+  protected $weight;
+
+  /**
+   * Constructs a new ProfileHandler.
+   *
+   * @param string $root
+   *   The app root.
+   * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
+   *   The info parser to parse the profile.info.yml files.
+   * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery
+   *   (optional) A extension discovery instance (for unit tests).
+   */
+  public function __construct($root, InfoParserInterface $info_parser, ExtensionDiscovery $extension_discovery = NULL) {
+    $this->root = $root;
+    $this->infoParser = $info_parser;
+    $this->extensionDiscovery = $extension_discovery ?: new ExtensionDiscovery($root, TRUE, NULL, NULL, $this);
+  }
+
+  /**
+   * Return the full path to a profile.
+   *
+   * Wrapper around drupal_get_path. If profile path is not available yet we
+   * call scan('profile') and prime the cache.
+   *
+   * @param string $profile
+   *   The name of the profile.
+   *
+   * @return string
+   *   The full path to the profile.
+   */
+  protected function getProfilePath($profile) {
+    // Check to see if system_rebuild_module_data cache is primed.
+    // @todo Remove as part of https://www.drupal.org/node/2186491.
+    $modules_cache = &drupal_static('system_rebuild_module_data');
+    if (!$this->scanCache && !isset($modules_cache)) {
+      // Find installation profiles. This needs to happen before performing a
+      // module scan as the module scan requires knowing what the active profile
+      // is.
+      // @todo Remove as part of https://www.drupal.org/node/2186491.
+      $profiles = $this->extensionDiscovery->scan('profile');
+      foreach ($profiles as $profile_name => $extension) {
+        // Prime the drupal_get_filename() static cache with the profile info
+        // file location so we can use drupal_get_path() on the active profile
+        // during the module scan.
+        // @todo Remove as part of https://www.drupal.org/node/2186491.
+        drupal_get_filename('profile', $profile_name, $extension->getPathname());
+      }
+      $this->scanCache = TRUE;
+    }
+    return drupal_get_path('profile', $profile);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProfileInfo($profile) {
+    // Even though info_parser caches the info array, we need to also cache
+    // this since it is recursive.
+    if (!isset($this->infoCache[$profile])) {
+      // Set defaults for profile info.
+      $defaults = [
+        'dependencies' => [],
+        'themes' => ['stark'],
+        'description' => '',
+        'version' => NULL,
+        'hidden' => FALSE,
+        'php' => DRUPAL_MINIMUM_PHP,
+        'base profile' => [
+          'name' => '',
+          'excluded_dependencies' => [],
+          'excluded_themes' => [],
+        ],
+      ];
+
+      $profile_path = $this->getProfilePath($profile);
+      $profile_file = $profile_path . "/$profile.info.yml";
+      $info = $this->infoParser->parse($profile_file) + $defaults;
+
+      // Normalize any base profile info.
+      if (is_string($info['base profile'])) {
+        $info['base profile'] = [
+          'name' => $info['base profile'],
+          'excluded_dependencies' => [],
+          'excluded_themes' => [],
+        ];
+      }
+
+      $profile_list = [];
+      // Get the base profile dependencies.
+      if ($base_profile_name = $info['base profile']['name']) {
+        $base_info = $this->getProfileInfo($base_profile_name);
+        $profile_list += $base_info['profile_list'];
+
+        // Ensure all dependencies are cleanly merged.
+        $info['dependencies'] = array_merge($info['dependencies'], $base_info['dependencies']);
+        if (isset($info['base profile']['excluded_dependencies'])) {
+          // Apply excluded dependencies.
+          $info['dependencies'] = array_diff($info['dependencies'], $info['base profile']['excluded_dependencies']);
+        }
+        // Ensure there's no circular dependency.
+        $info['dependencies'] = array_diff($info['dependencies'], [$profile]);
+
+        // Ensure all themes are cleanly merged.
+        $info['themes'] = array_unique(array_merge($info['themes'], $base_info['themes']));
+        if (isset($info['base profile']['excluded_themes'])) {
+          // Apply excluded themes.
+          $info['themes'] = array_diff($info['themes'], $info['base profile']['excluded_themes']);
+        }
+        // Ensure each theme is listed only once.
+        $info['themes'] = array_unique($info['themes']);
+
+      }
+      $profile_list[$profile] = $profile;
+      $info['profile_list'] = $profile_list;
+
+      // Ensure the same dependency notation as in modules can be used.
+      array_walk($info['dependencies'], function(&$dependency) {
+        $dependency = ModuleHandler::parseDependency($dependency)['name'];
+      });
+
+      // Installation profiles are hidden by default, unless explicitly
+      // specified otherwise in the .info.yml file.
+      $info['hidden'] = isset($info['hidden']) ? $info['hidden'] : TRUE;
+
+      $this->infoCache[$profile] = $info;
+    }
+    return $this->infoCache[$profile];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProfileInfo($profile, array $info) {
+    $this->infoCache[$profile] = $info;
+    // Also unset the cached profile extension so the updated info will
+    // be picked up.
+    unset($this->profilesWithParentsCache[$profile]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearProfileCache() {
+    $this->profilesWithParentsCache = [];
+    $this->infoCache = [];
+  }
+
+  /**
+   * Create an Extension object for a profile.
+   *
+   * @param string $profile
+   *   The name of the profile.
+   *
+   * @return \Drupal\Core\Extension\Extension
+   *   The extension object for the profile
+   *   Properties added to extension:
+   *     info: The parsed info.yml data.
+   *     origin: The directory origin as used in ExtensionDiscovery.
+   */
+  protected function getProfileExtension($profile) {
+    $profile_info = $this->getProfileInfo($profile);
+
+    $type = $profile_info['type'];
+    $profile_path = $this->getProfilePath($profile);
+    $profile_file = $profile_path . "/$profile.info.yml";
+    $filename = file_exists($profile_path . "/$profile.$type") ? "$profile.$type" : NULL;
+    $extension = new Extension($this->root, $type, $profile_file, $filename);
+
+    $extension->info = $profile_info;
+    $extension->origin = '';
+
+    return $extension;
+  }
+
+  /**
+   * Get a list of dependent profile names.
+   *
+   * @param string $profile
+   *   Name of profile.
+   *
+   * @return string[]
+   *   An associative array of profile names, keyed by profile name
+   *   in descending order of their dependencies.
+   *   (parent profiles first, main profile last)
+   */
+  protected function getProfileList($profile) {
+    $profile_info = $this->getProfileInfo($profile);
+    return $profile_info['profile_list'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProfiles($profile = NULL) {
+    if (empty($profile)) {
+      $profile = drupal_get_profile();
+    }
+    if (!isset($this->profilesWithParentsCache[$profile])) {
+      $profiles = [];
+      // Check if a valid profile name was given.
+      if (!empty($profile)) {
+        $list = $this->getProfileList($profile);
+
+        // Starting weight for profiles ensures their hooks run last.
+        $weight = 1000;
+
+        // Loop through profile list and create Extension objects.
+        $profiles = [];
+        foreach ($list as $profile_name) {
+          $extension = $this->getProfileExtension($profile_name);
+          $extension->weight = $weight;
+          $weight++;
+          $profiles[$profile_name] = $extension;
+        }
+      }
+      $this->profilesWithParentsCache[$profile] = $profiles;
+    }
+    return $this->profilesWithParentsCache[$profile];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function selectDistribution(array $profile_list) {
+    // First, find all profiles marked as distributions
+    $distributions = [];
+    foreach ($profile_list as $profile_name) {
+      $profile_info = $this->getProfileInfo($profile_name);
+      if (!empty($profile_info['distribution'])) {
+        $distributions[$profile_name] = $profile_name;
+      }
+    }
+    // Remove any base profiles.
+    foreach ($profile_list as $profile_name) {
+      $profile_info = $this->getProfileInfo($profile_name);
+      if ($base_profile = $profile_info['base profile']['name']) {
+        unset($distributions[$base_profile]);
+      }
+    }
+    return !empty($distributions) ? current($distributions) : NULL;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php
new file mode 100644
index 0000000000..37b4bdf002
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+/**
+ * Lists and manages installation profiles.
+ */
+interface ProfileHandlerInterface {
+
+  /**
+   * Retrieve the info array for a profile.
+   *
+   * Parse and process the profile info.yml file.
+   * Processing steps:
+   *   1) Ensure default keys are set.
+   *   2) Recursively collect dependencies from parent profiles.
+   *   3) Exclude dependencies explicitly mentioned in
+   *      $info['base profile']['exclude_dependencies']
+   *   4) Add the $info['profile_list'] list of dependent profiles.
+   *
+   * @param string $profile
+   *   The name of profile.
+   *
+   * @return array
+   *   The processed $info array.
+   *
+   * @see install_profile_info()
+   */
+  public function getProfileInfo($profile);
+
+  /**
+   * Stores info data for a profile.
+   *
+   * This can be used in situations where the info cache needs to be changed
+   * This is used for testing.
+   *
+   * @param string $profile
+   *  The name of profile.
+   * @param array $info
+   *   The info array to be set.
+   *
+   * @see install_profile_info()
+   */
+  public function setProfileInfo($profile, array $info);
+
+  /**
+   * Clears the profile cache.
+   */
+  public function clearProfileCache();
+
+  /**
+   * Returns a list of dependent installation profiles.
+   *
+   * @param string $profile
+   *   The name of profile. If none is specified, use the current profile.
+   *
+   * @return \Drupal\Core\Extension\Extension[]
+   *   An associative array of Extension objects, keyed by profile name in
+   *   descending order of their dependencies.
+   *   (parent profiles first, main profile last)
+   */
+  public function getProfiles($profile = NULL);
+
+  /**
+   * Select the install distribution from the list of profiles.
+   *
+   * If there are multiple profiles marked as distributions, select the first.
+   * If there is an inherited profile marked as a distribution, select it over
+   * its base profile.
+   *
+   * @param string[] $profile_list
+   *   List of profile names to search.
+   *
+   * @return string|null
+   *   The selected distribution profile name, or NULL if none is found.
+   */
+  public function selectDistribution(array $profile_list);
+
+}
diff --git a/core/modules/config/src/Tests/ConfigImportBaseInstallProfileTest.php b/core/modules/config/src/Tests/ConfigImportBaseInstallProfileTest.php
new file mode 100644
index 0000000000..f323eb3896
--- /dev/null
+++ b/core/modules/config/src/Tests/ConfigImportBaseInstallProfileTest.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\config\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the importing/exporting configuration based on install sub-profile.
+ *
+ * @group config
+ */
+class ConfigImportBaseInstallProfileTest extends WebTestBase {
+
+  /**
+   * The profile to install as a basis for testing.
+   *
+   * @var string
+   */
+  protected $profile = 'testing_inherited';
+
+  /**
+   * A user with the 'synchronize configuration' permission.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $webUser;
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->webUser = $this->drupalCreateUser(['synchronize configuration']);
+    $this->drupalLogin($this->webUser);
+    $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
+  }
+
+  /**
+   * Tests config importer cannot uninstall parent install profiles and
+   * dependencies of parent profiles can be uninstalled.
+   *
+   * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
+   */
+  public function testInstallParentProfileValidation() {
+    $sync = $this->container->get('config.storage.sync');
+    $this->copyConfig($this->container->get('config.storage'), $sync);
+    $core = $sync->read('core.extension');
+
+    // Ensure that parent profile can not be uninstalled.
+    unset($core['module']['testing']);
+    $sync->write('core.extension', $core);
+
+    $this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
+    $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:');
+    $this->assertText('Unable to uninstall the Testing profile since it is a parent of another installed profile.');
+
+    // Uninstall dependencies of parent profile.
+    $core['module']['testing'] = 0;
+    unset($core['module']['dynamic_page_cache']);
+    $sync->write('core.extension', $core);
+    $sync->deleteAll('dynamic_page_cache.');
+    $this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
+    $this->assertText('The configuration was imported successfully.');
+    $this->rebuildContainer();
+    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('dynamic_page_cache'), 'The dynamic_page_cache module has been uninstalled.');
+  }
+
+  /**
+   * Tests config importer cannot uninstall sub-profiles and dependencies of
+   * sub-profiles can be uninstalled.
+   *
+   * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
+   */
+  public function testInstallSubProfileValidation() {
+    $sync = $this->container->get('config.storage.sync');
+    $this->copyConfig($this->container->get('config.storage'), $sync);
+    $core = $sync->read('core.extension');
+
+    // Ensure install sub-profiles can not be uninstalled.
+    unset($core['module']['testing_inherited']);
+    $sync->write('core.extension', $core);
+
+    $this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
+    $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:');
+    $this->assertText('Unable to uninstall the Testing Inherited profile since it is the main install profile.');
+
+    // Uninstall dependencies of main profile.
+    $core['module']['testing_inherited'] = 0;
+    unset($core['module']['syslog']);
+    $sync->write('core.extension', $core);
+    $sync->deleteAll('syslog.');
+    $this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
+    $this->assertText('The configuration was imported successfully.');
+    $this->rebuildContainer();
+    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('syslog'), 'The syslog module has been uninstalled.');
+  }
+
+}
diff --git a/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php b/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php
index eaece36db3..01d04d8084 100644
--- a/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php
+++ b/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php
@@ -56,7 +56,7 @@ public function testInstallProfileValidation() {
 
     $this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
     $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:');
-    $this->assertText('Unable to uninstall the Testing config import profile since it is the install profile.');
+    $this->assertText('Unable to uninstall the Testing config import profile since it is the main install profile.');
 
     // Uninstall dependencies of testing_config_import.
     $core['module']['testing_config_import'] = 0;
diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php
index 80348c59f1..bcd60652ba 100644
--- a/core/modules/system/src/Form/ModulesUninstallForm.php
+++ b/core/modules/system/src/Form/ModulesUninstallForm.php
@@ -114,12 +114,15 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       return $form;
     }
 
-    $profile = drupal_get_profile();
+    $profiles = \Drupal::service('profile_handler')->getProfiles();
 
     // Sort all modules by their name.
     uasort($uninstallable, 'system_sort_modules_by_info_name');
     $validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable));
 
+    // Remove any profiles from the list.
+    $uninstallable = array_diff_key($uninstallable, $profiles);
+
     $form['uninstall'] = ['#tree' => TRUE];
     foreach ($uninstallable as $module_key => $module) {
       $name = $module->info['name'] ?: $module->getName();
@@ -140,10 +143,10 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         $form['uninstall'][$module->getName()]['#disabled'] = TRUE;
       }
       // All modules which depend on this one must be uninstalled first, before
-      // we can allow this module to be uninstalled. (The installation profile
-      // is excluded from this list.)
+      // we can allow this module to be uninstalled. (Installation profiles are
+      // excluded from this list.)
       foreach (array_keys($module->required_by) as $dependent) {
-        if ($dependent != $profile && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) {
+        if (!in_array($dependent, array_keys($profiles)) && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) {
           $name = isset($modules[$dependent]->info['name']) ? $modules[$dependent]->info['name'] : $dependent;
           $form['modules'][$module->getName()]['#required_by'][] = $name;
           $form['uninstall'][$module->getName()]['#disabled'] = TRUE;
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index a1a6b715a3..3d013178a7 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -994,27 +994,13 @@ function system_get_info($type, $name = NULL) {
 function _system_rebuild_module_data() {
   $listing = new ExtensionDiscovery(\Drupal::root());
 
-  // Find installation profiles. This needs to happen before performing a
-  // module scan as the module scan requires knowing what the active profile is.
-  // @todo Remove as part of https://www.drupal.org/node/2186491.
-  $profiles = $listing->scan('profile');
-  $profile = drupal_get_profile();
-  if ($profile && isset($profiles[$profile])) {
-    // Prime the drupal_get_filename() static cache with the profile info file
-    // location so we can use drupal_get_path() on the active profile during
-    // the module scan.
-    // @todo Remove as part of https://www.drupal.org/node/2186491.
-    drupal_get_filename('profile', $profile, $profiles[$profile]->getPathname());
-  }
-
   // Find modules.
   $modules = $listing->scan('module');
-  // Include the installation profile in modules that are loaded.
-  if ($profile) {
-    $modules[$profile] = $profiles[$profile];
-    // Installation profile hooks are always executed last.
-    $modules[$profile]->weight = 1000;
-  }
+
+  // Find profiles.
+  /** @var \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler */
+  $profile_handler = \Drupal::service('profile_handler');
+  $modules = array_merge($modules, $profile_handler->getProfiles());
 
   // Set defaults for module info.
   $defaults = [
@@ -1028,7 +1014,14 @@ function _system_rebuild_module_data() {
   // Read info files for each module.
   foreach ($modules as $key => $module) {
     // Look for the info file.
-    $module->info = \Drupal::service('info_parser')->parse($module->getPathname());
+    // @todo On the longrun we should leverage the extension lists services,
+    //    see https://www.drupal.org/node/2208429.
+    if ($module->getType() === 'profile') {
+      $module->info = $profile_handler->getProfileInfo($module->getName());
+    }
+    else {
+      $module->info = \Drupal::service('info_parser')->parse($module->getPathname());
+    }
 
     // Add the info file modification time, so it becomes available for
     // contributed modules to use for ordering module lists.
@@ -1037,12 +1030,6 @@ function _system_rebuild_module_data() {
     // Merge in defaults and save.
     $modules[$key]->info = $module->info + $defaults;
 
-    // Installation profiles are hidden by default, unless explicitly specified
-    // otherwise in the .info.yml file.
-    if ($key == $profile && !isset($modules[$key]->info['hidden'])) {
-      $modules[$key]->info['hidden'] = TRUE;
-    }
-
     // Invoke hook_system_info_alter() to give installed modules a chance to
     // modify the data in the .info.yml files if necessary.
     // @todo Remove $type argument, obsolete with $module->getType().
@@ -1056,15 +1043,18 @@ function _system_rebuild_module_data() {
     _system_rebuild_module_data_ensure_required($module, $modules);
   }
 
-
-  if ($profile && isset($modules[$profile])) {
-    // The installation profile is required, if it's a valid module.
-    $modules[$profile]->info['required'] = TRUE;
-    // Add a default distribution name if the profile did not provide one.
-    // @see install_profile_info()
-    // @see drupal_install_profile_distribution_name()
-    if (!isset($modules[$profile]->info['distribution']['name'])) {
-      $modules[$profile]->info['distribution']['name'] = 'Drupal';
+  // This must be done after _system_rebuild_module_data_ensure_required().
+  $profiles = \Drupal::service('profile_handler')->getProfiles();
+  foreach ($profiles as $profile_name => $profile) {
+    if (isset($modules[$profile_name])) {
+      // Installation profiles are required, if it's a valid module.
+      $modules[$profile_name]->info['required'] = TRUE;
+      // Add a default distribution name if the profile did not provide one.
+      // @see install_profile_info()
+      // @see drupal_install_profile_distribution_name()
+      if (!isset($modules[$profile_name]->info['distribution']['name'])) {
+        $modules[$profile_name]->info['distribution']['name'] = 'Drupal';
+      }
     }
   }
 
diff --git a/core/profiles/testing_inherited/config/install/block.block.stable_login.yml b/core/profiles/testing_inherited/config/install/block.block.stable_login.yml
new file mode 100644
index 0000000000..3650c6c41a
--- /dev/null
+++ b/core/profiles/testing_inherited/config/install/block.block.stable_login.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - user
+  theme:
+    - stable
+id: stable_login
+theme: stable
+region: sidebar_first
+weight: 0
+provider: null
+plugin: user_login_block
+settings:
+  id: user_login_block
+  label: 'User login'
+  provider: user
+  label_display: visible
+visibility: {  }
diff --git a/core/profiles/testing_inherited/config/install/system.theme.yml b/core/profiles/testing_inherited/config/install/system.theme.yml
new file mode 100644
index 0000000000..67aeeeeac7
--- /dev/null
+++ b/core/profiles/testing_inherited/config/install/system.theme.yml
@@ -0,0 +1,2 @@
+# @todo: Remove this file in https://www.drupal.org/node/2352949
+default: stable
diff --git a/core/profiles/testing_inherited/testing_inherited.info.yml b/core/profiles/testing_inherited/testing_inherited.info.yml
new file mode 100644
index 0000000000..d76332ddee
--- /dev/null
+++ b/core/profiles/testing_inherited/testing_inherited.info.yml
@@ -0,0 +1,21 @@
+name: Testing Inherited
+type: profile
+description: 'Profile for testing base profile inheritance.'
+version: VERSION
+core: 8.x
+hidden: true
+
+base profile:
+  name: testing
+  excluded_dependencies:
+    - page_cache
+  excluded_themes:
+    - classy
+
+dependencies:
+  - block
+  - config
+  - syslog
+
+themes:
+  - stable
diff --git a/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php
new file mode 100644
index 0000000000..dba4580ab5
--- /dev/null
+++ b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\Tests\testing_inherited\Functional;
+
+use Drupal\block\BlockInterface;
+use Drupal\block\Entity\Block;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests inherited profiles.
+ *
+ * @group profiles
+ */
+class InheritedProfileTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $profile = 'testing_inherited';
+
+  /**
+   * Tests inherited installation profile.
+   */
+  public function testInheritedProfile() {
+    // Check that the stable_login block exists.
+    $this->assertInstanceOf(BlockInterface::class, Block::load('stable_login'));
+
+    // Check that stable is the default theme.
+    $this->assertEquals('stable', $this->config('system.theme')->get('default'));
+
+    // Check the excluded_dependencies flag on installation profiles.
+    $this->assertTrue(\Drupal::moduleHandler()->moduleExists('config'));
+    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('page_cache'));
+
+    // Check that all themes were installed, except excluded ones.
+    $this->assertTrue(\Drupal::service('theme_handler')->themeExists('stable'));
+    $this->assertFalse(\Drupal::service('theme_handler')->themeExists('classy'));
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php
new file mode 100644
index 0000000000..ebb20f6a00
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Extension;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the ProfileHandler class.
+ *
+ * @coversDefaultClass \Drupal\Core\Extension\ProfileHandler
+ *
+ * @group Extension
+ */
+class ProfileHandlerTest extends KernelTestBase {
+
+  /**
+   * Modules to install.
+   *
+   * The System module is required because system_rebuild_module_data() is used.
+   *
+   * @var array
+   */
+  public static $modules = ['system'];
+
+  /**
+   * Tests getting profile info.
+   *
+   * @covers ::getProfileInfo
+   */
+  public function testGetProfileInfo() {
+    $profile_handler = $this->container->get('profile_handler');
+    $info = $profile_handler->getProfileInfo('testing_inherited');
+    $this->assertNotEmpty($info);
+    $this->assertEquals($info['name'], 'Testing Inherited');
+    $this->assertEquals($info['base profile']['name'], 'testing');
+    $this->assertEquals($info['base profile']['excluded_dependencies'], ['page_cache']);
+    $this->assertTrue(in_array('config', $info['dependencies'], 'config should be found in dependencies'));
+    $this->assertFalse(in_array('page_cache', $info['dependencies'], 'page_cache should not be found in dependencies'));
+    $this->assertTrue($info['hidden'], 'Profiles should be hidden');
+    $this->assertNotEmpty($info['profile_list']);
+    $profile_list = $info['profile_list'];
+    // Testing order of profile list.
+    $this->assertEquals($profile_list, [
+      'testing' => 'testing',
+      'testing_inherited' => 'testing_inherited'
+    ]);
+
+    // Test that profiles without any base return normalized info.
+    $info = $profile_handler->getProfileInfo('minimal');
+    $this->assertInternalType('array', $info['base profile']);
+
+    $this->assertArrayHasKey('name', $info['base profile']);
+    $this->assertEmpty($info['base profile']['name']);
+
+    $this->assertArrayHasKey('excluded_dependencies', $info['base profile']);
+    $this->assertInternalType('array', $info['base profile']['excluded_dependencies']);
+    $this->assertEmpty($info['base profile']['excluded_dependencies']);
+
+    $this->assertArrayHasKey('excluded_themes', $info['base profile']);
+    $this->assertInternalType('array', $info['base profile']['excluded_themes']);
+    $this->assertEmpty($info['base profile']['excluded_themes']);
+  }
+
+  /**
+   * Tests getting profile dependency list.
+   *
+   * @covers ::getProfiles
+   */
+  public function testGetProfiles() {
+    $profile_handler = $this->container->get('profile_handler');
+    $profiles = $profile_handler->getProfiles('testing_inherited');
+    $this->assertCount(2, $profiles);
+
+    $first_profile = current($profiles);
+    $this->assertEquals(get_class($first_profile), 'Drupal\Core\Extension\Extension');
+    $this->assertEquals($first_profile->getName(), 'testing');
+    $this->assertEquals($first_profile->weight, 1000);
+    $this->assertObjectHasAttribute('origin', $first_profile);
+
+    $second_profile = next($profiles);
+    $this->assertEquals(get_class($second_profile), 'Drupal\Core\Extension\Extension');
+    $this->assertEquals($second_profile->getName(), 'testing_inherited');
+    $this->assertEquals($second_profile->weight, 1001);
+    $this->assertObjectHasAttribute('origin', $second_profile);
+  }
+
+  /**
+   * @covers ::selectDistribution
+   * @covers ::setProfileInfo
+   */
+  public function testSelectDistribution() {
+    /** @var \Drupal\Core\Extension\ProfileHandler $profile_handler */
+    $profile_handler = $this->container->get('profile_handler');
+    $profiles = ['testing', 'testing_inherited'];
+    $base_info = $profile_handler->getProfileInfo('minimal');
+    $profile_info = $profile_handler->getProfileInfo('testing_inherited');
+
+    // Neither profile has distribution set
+    $distribution = $profile_handler->selectDistribution($profiles);
+    $this->assertEmpty($distribution, 'No distribution should be selected');
+
+    // Set base profile distribution
+    $base_info['distribution']['name'] = 'Minimal';
+    $profile_handler->setProfileInfo('minimal', $base_info);
+    // Base profile distribution should not be selected
+    $distribution = $profile_handler->selectDistribution($profiles);
+    $this->assertEmpty($distribution, 'Base profile distribution should not be selected');
+
+    // Set main profile distribution
+    $profile_info['distribution']['name'] = 'Testing Inherited';
+    $profile_handler->setProfileInfo('testing_inherited', $profile_info);
+    // Main profile distribution should be selected
+    $distribution = $profile_handler->selectDistribution($profiles);
+    $this->assertEquals($distribution, 'testing_inherited');
+  }
+
+}
