diff --git a/core/core.services.yml b/core/core.services.yml
index 8bce755..3439883 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']
+    arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '@profile_handler']
     lazy: true
   config.storage:
     class: Drupal\Core\Config\CachedStorage
@@ -516,6 +516,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 a016540..8079802 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -442,6 +442,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
@@ -1209,6 +1215,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
@@ -1231,11 +1239,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.
@@ -1568,7 +1573,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(array(drupal_get_profile()), FALSE);
+  $profiles = \Drupal::service('profile_handler')->getProfiles();
+
+  // Install all the profiles.
+  \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 49dd4cb..d52e2b9 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -1037,6 +1037,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
@@ -1063,18 +1071,7 @@ function install_profile_info($profile, $langcode = 'en') {
   $cache = &drupal_static(__FUNCTION__, array());
 
   if (!isset($cache[$profile][$langcode])) {
-    // Set defaults for module info.
-    $defaults = array(
-      'dependencies' => array(),
-      'themes' => array('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 2f30cce..05f1a6d 100644
--- a/core/lib/Drupal/Core/Config/ConfigInstaller.php
+++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Config\Entity\ConfigDependencyManager;
 use Drupal\Core\Config\Entity\ConfigEntityDependency;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\Extension\ProfileHandlerInterface;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
 class ConfigInstaller implements ConfigInstallerInterface {
@@ -54,6 +55,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
@@ -73,13 +81,21 @@ class ConfigInstaller implements ConfigInstallerInterface {
    *   The configuration manager.
    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
    *   The event dispatcher.
+   * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler
+   *   The profile handler.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher) {
+  public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, 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;
+    if (!isset($profile_handler) && \Drupal::hasService('profile_handler')) {
+      $this->profileHandler = \Drupal::service('profile_handler');
+    }
+    else {
+      $this->profileHandler = $profile_handler;
+    }
   }
 
   /**
@@ -462,7 +478,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 14e80dd..836adeb 100644
--- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php
+++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Config;
 
+use Drupal\Core\Extension\ProfileHandlerInterface;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\Extension\ExtensionDiscovery;
 
@@ -42,9 +43,11 @@ class ExtensionInstallStorage extends InstallStorage {
    * @param bool $include_profile
    *   (optional) Whether to include the install profile in extensions to
    *   search and to get overrides from.
+   * @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) {
-    parent::__construct($directory, $collection);
+  public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE, ProfileHandlerInterface $profile_handler = NULL) {
+    parent::__construct($directory, $collection, $profile_handler);
     $this->configStorage = $config_storage;
     $this->includeProfile = $include_profile;
   }
@@ -78,22 +81,13 @@ protected function getAllFolders() {
       $this->folders += $this->getCoreNames();
 
       $install_profile = Settings::get('install_profile');
-      $profile = drupal_get_profile();
       $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[$install_profile]);
-        $profile_list = $listing->scan('profile');
-        if ($profile && 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());
-        }
         $module_list_scan = $listing->scan('module');
         $module_list = array();
         foreach (array_keys($modules) as $module) {
@@ -114,18 +108,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 (isset($profile)) {
-          if (!isset($profile_list)) {
-            $profile_list = $listing->scan('profile');
-          }
-          if (isset($profile_list[$profile])) {
-            $profile_folders = $this->getComponentNames(array($profile_list[$profile]));
-            $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($install_profile));
       }
     }
     return $this->folders;
diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php
index bd9bf30..dd5f208 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,17 @@ 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 (!isset($profile_handler) && \Drupal::hasService('profile_handler')) {
+      $this->profileHandler = \Drupal::service('profile_handler');
+    }
+    else {
+      $this->profileHandler = $profile_handler;
+    }
   }
 
   /**
@@ -151,21 +167,12 @@ protected function getAllFolders() {
     if (!isset($this->folders)) {
       $this->folders = array();
       $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(array($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/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
index 177f011..1d44a96 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,23 @@ 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 called without a service container.
+    // (@drupalKernel::moduleData) so check if profile_handler is available.
+    if (!isset($profile_handler) && \Drupal::hasService('profile_handler')) {
+      $profile_handler = \Drupal::service('profile_handler');
+    }
+    elseif (!isset($profile_handler)) {
+      $profile_handler = new FallbackProfileHandler($root);
+    }
+    $this->profileHandler = $profile_handler;
   }
 
   /**
@@ -241,7 +261,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 0000000..516bb48
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/FallbackProfileHandler.php
@@ -0,0 +1,73 @@
+<?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];
+    }
+  }
+
+  /**
+   * {@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($profile_list) {
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ProfileHandler.php b/core/lib/Drupal/Core/Extension/ProfileHandler.php
new file mode 100644
index 0000000..c6ee85b
--- /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;
+  }
+
+  /**
+   * Returns an extension discovery object.
+   *
+   * @return \Drupal\Core\Extension\ExtensionDiscovery
+   *   The extension discovery object.
+   */
+  protected function getExtensionDiscovery() {
+    if (!isset($this->extensionDiscovery)) {
+      $this->extensionDiscovery = new ExtensionDiscovery($this->root);
+    }
+    return $this->extensionDiscovery;
+  }
+
+  /**
+   * 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)) {
+      $listing = $this->getExtensionDiscovery();
+      // 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');
+      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' => []
+        ],
+      ];
+
+      $profile_path = $this->getProfilePath($profile);
+      $profile_file = $profile_path . "/$profile.info.yml";
+      $info = $this->infoParser->parse($profile_file);
+      $info += $defaults;
+
+      // Normalize any base profile info.
+      if (is_string($info['base profile'])) {
+        $info['base profile'] = [
+          'name' => $info['base profile'],
+          'excluded_dependencies' => [],
+        ];
+      }
+
+      $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']);
+        // 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]);
+      }
+      $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($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 0000000..09ea250
--- /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($profile_list);
+
+}
diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php
index a06810e..3494eef 100644
--- a/core/modules/system/src/Form/ModulesUninstallForm.php
+++ b/core/modules/system/src/Form/ModulesUninstallForm.php
@@ -114,7 +114,7 @@ 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');
@@ -143,7 +143,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       // we can allow this module to be uninstalled. (The installation profile
       // is 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 6581e60..4c62328 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -950,27 +950,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 = array(
@@ -984,7 +970,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.
@@ -993,12 +986,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().
@@ -1012,7 +999,8 @@ function _system_rebuild_module_data() {
     _system_rebuild_module_data_ensure_required($module, $modules);
   }
 
-
+  // This must be done after _system_rebuild_module_data_ensure_required().
+  $profile = drupal_get_profile();
   if ($profile && isset($modules[$profile])) {
     // The installation profile is required, if it's a valid module.
     $modules[$profile]->info['required'] = TRUE;
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 0000000..3ca5802
--- /dev/null
+++ b/core/profiles/testing_inherited/testing_inherited.info.yml
@@ -0,0 +1,14 @@
+name: Testing Inherited
+type: profile
+description: 'Profile for testing base profile inheritance.'
+version: VERSION
+core: 8.x
+hidden: true
+
+base profile:
+  name: minimal
+  excluded_dependencies:
+    - dblog
+
+dependencies:
+  - config
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 0000000..8077713
--- /dev/null
+++ b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\testing_inherited\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests inherited profiles.
+ *
+ * @group profiles
+ */
+class InheritedProfileTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $profile = 'testing_inherited';
+
+  /**
+   * Tests inherited installation profile.
+   */
+  function testInheritedProfile() {
+    $this->drupalGet('');
+    // Check the login block is present.
+    $this->assertSession()->linkExists(t('Create new account'));
+    $this->assertSession()->statusCodeEquals(200);
+
+    // Check the excluded_dependencies flag on installation profiles.
+    $this->assertTrue(\Drupal::moduleHandler()->moduleExists('config'));
+    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('dblog'));
+  }
+
+}
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 0000000..fad80c0
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php
@@ -0,0 +1,106 @@
+<?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'], 'minimal');
+    $this->assertEquals($info['base profile']['excluded_dependencies'], ['dblog']);
+    $this->assertTrue(in_array('config', $info['dependencies'], 'config should be found in dependencies'));
+    $this->assertFalse(in_array('dblog', $info['dependencies'], 'dblog 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, [
+      'minimal' => 'minimal',
+      'testing_inherited' => 'testing_inherited'
+    ]);
+
+    // Test that profiles without any base return normalized info.
+    $info = $profile_handler->getProfileInfo('minimal');
+    $this->assertEquals($info['base profile'], ['name' => '', 'excluded_dependencies' => []]);
+  }
+
+  /**
+   * 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(), 'minimal');
+    $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 = ['minimal', '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');
+  }
+
+}
