diff --git a/core/includes/common.inc b/core/includes/common.inc
index f1a7ce0..8aa1425 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -7,6 +7,7 @@
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Database\Database;
+use Drupal\Core\System\SystemListRecursiveFilterIterator;
 use Drupal\Core\Template\Attribute;
 
 /**
@@ -5159,7 +5160,7 @@ function drupal_cron_cleanup() {
  *   - 'filename': File name.
  *   - 'name': Name of file without the extension.
  */
-function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) {
+function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1, $procedural = FALSE) {
   $config = conf_path();
   $files = array();
 
@@ -5199,15 +5200,48 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1)
   if (!function_exists('file_scan_directory')) {
     require_once DRUPAL_ROOT . '/core/includes/file.inc';
   }
+  $skip_extension_dirs = array('config', 'lib', 'templates', 'js', 'css');
   foreach ($searchdir as $dir) {
-    $files_to_add = file_scan_directory($dir, $mask, array(
-      'key' => $key,
-      'min_depth' => $min_depth,
-      // Do not recurse into ./lib directories; they cannot contain extensions.
-      // We also skip templates, css, and js directories.
-      // @todo Find a way to skip ./config directories (but not modules/config).
-      'nomask' => '/^(CVS|lib|templates|css|js)$/',
-    ));
+    if (!$procedural) {
+      $files_to_add = array();
+      if (!is_dir($dir)) {
+        continue;
+      }
+      $flags = \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS;
+      $flags |= \FilesystemIterator::CURRENT_AS_SELF;
+      if ($key !== 'uri') {
+        $flags |= \FilesystemIterator::KEY_AS_FILENAME;
+      }
+      $directory = new \RecursiveDirectoryIterator($dir, $flags);
+      $filter = new SystemListRecursiveFilterIterator($directory, $mask, array_flip($skip_extension_dirs));
+      $flattened = new \RecursiveIteratorIterator($filter);
+      // All consuming functions expect an array containing file objects and not
+      // an iterator. They also expect $uri, $filename, and $name properties.
+      // @todo Something down the line tries to serialize all $files but neither
+      //   RecursiveDirectoryIterator nor SplFileInfo objects can be serialized.
+      foreach ($flattened as $filename => $iterator) {
+        $file = new stdClass;
+        $file->uri = $iterator->getPathName();
+        $file->dirname = $iterator->getPath();
+        $file->filename = $iterator->getFileName();
+        // @todo SplFileInfo->getBasename() does not strip the extension without
+        //   specifying it. Also note that pathinfo()'s PATHINFO_FILENAME
+        //   returns the basename without extension, *not* the filename as
+        //   returned by SplFileInfo->getFileName().
+        $file->name = pathinfo($filename, PATHINFO_FILENAME);
+        $files_to_add[$file->{$key}] = $file;
+      }
+    }
+    else {
+      $files_to_add = file_scan_directory($dir, $mask, array(
+        'key' => $key,
+        'min_depth' => $min_depth,
+        // Do not recurse into ./lib directories; they cannot contain extensions.
+        // We also skip templates, css, and js directories.
+        // @todo Find a way to skip ./config directories (but not modules/config).
+        'nomask' => '/^(' . implode('|', $skip_extension_dirs) . ')$/',
+      ));
+    }
 
     // Duplicate files found in later search directories take precedence over
     // earlier ones, so we want them to overwrite keys in our resulting
diff --git a/core/lib/Drupal/Core/System/SystemListRecursiveFilterIterator.php b/core/lib/Drupal/Core/System/SystemListRecursiveFilterIterator.php
new file mode 100644
index 0000000..3c781d6
--- /dev/null
+++ b/core/lib/Drupal/Core/System/SystemListRecursiveFilterIterator.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\System\SystemListRecursiveFilterIterator.
+ */
+
+namespace Drupal\Core\System;
+
+/**
+ * Recursively filters Extends CacheArray to lazy load .info properties for modules.
+ *
+ * Use system_get_module_info() rather than instantiating this class directly.
+ */
+class SystemListRecursiveFilterIterator extends \RecursiveFilterIterator {
+
+  protected $regex;
+  protected $hasExtension;
+  protected $skipExtensionDirs;
+
+  /**
+   * Overrides \RecursiveFilterIterator::__construct().
+   *
+   * @param \RecursiveDirectoryIterator $iterator
+   *   The recursive iterator to filter.
+   * @param string $regex
+   *   A regular expression to match files against.
+   * @param array $skip_dirs
+   *   An array whose keys are names of subdirectories within a system extension
+   *   directory that should not be recursed into.
+   * @param bool $has_extension
+   *   (optional) Whether the parent directory contains an extension file that
+   *   matched $regex.
+   */
+  public function __construct(\RecursiveDirectoryIterator $iterator, $regex, array $skip_dirs, $has_extension = FALSE) {
+    $this->hasExtension = $has_extension;
+    $this->regex = $regex;
+    $this->skipExtensionDirs = $skip_dirs;
+    parent::__construct($iterator);
+  }
+
+  /**
+   * Overrides \OuterRecursiveIterator::getChildren().
+   *
+   * Every RecursiveIterator implementation that uses additional constructor
+   * arguments has to override this method to instantiate child iterator
+   * instances with the required arguments.
+   */
+  public function getChildren() {
+    return new self($this->getInnerIterator()->getChildren(), $this->regex, $this->skipExtensionDirs, $this->hasExtension);
+  }
+
+  /**
+   * Implements \RecursiveFilterIterator::accept().
+   *
+   * This relies on the fact that RecursiveDirectoryIterator processes files
+   * within each directory, before processing subdirectories. Due to that, we
+   * first check whether a directory contains an extension that matches, and if
+   * that is the case, we skip all subdirectories that are defined in
+   * $skipExtensionDirs.
+   *
+   * @return bool
+   *   A Boolean indicating:
+   *   - For files: TRUE if the filename matches the regular expression.
+   *   - For directories: TRUE if the directory should be recursed into.
+   */
+  public function accept() {
+    if ($this->isDir()) {
+      // If the parent directory does not contain a matching extension filename,
+      // recurse into all subdirectories.
+      if (!$this->hasExtension) {
+        return TRUE;
+      }
+      // Otherwise, check each subdirectory for whether we are allowed to
+      // recurse into it.
+      return !isset($this->skipExtensionDirs[$this->current()->getFilename()]);
+    }
+    else {
+      $is_extension = preg_match($this->regex, $this->current()->getFilename());
+      if ($is_extension) {
+        $this->hasExtension = TRUE;
+      }
+      return $is_extension;
+    }
+  }
+
+}
