diff --git a/core/includes/file.inc b/core/includes/file.inc
index 228515d..f6f2e24 100644
--- a/core/includes/file.inc
+++ b/core/includes/file.inc
@@ -307,7 +307,7 @@ function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS)
     return FALSE;
   }
   // The directory exists, so check to see if it is writable.
-  $writable = is_writable($directory);
+  $writable = file_directory_is_writable($directory);
   if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
     return drupal_chmod($directory);
   }
@@ -315,6 +315,47 @@ function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS)
   return $writable;
 }
 
+
+/**
+ * Determines if a directory is writable by the web server.
+ *
+ * In order to be able to write files within the directory, the directory
+ * itself must be writable, and it must also have the executable bit set. This
+ * helper function checks both at the same time.
+ *
+ * @param $uri
+ *   A URI or pathname pointing to the directory that will be checked.
+ *
+ * @return
+ *   TRUE if the directory is writable and executable; FALSE otherwise.
+ */
+function file_directory_is_writable($uri) {
+  // By converting the URI to a normal path using drupal_realpath(), we can
+  // correctly handle both stream wrappers and normal paths.
+  return is_writable(drupal_realpath($uri)) && drupal_is_executable($uri);
+}
+
+/**
+ * Determines if a file or directory is executable.
+ *
+ * PHP's is_executable() does not fully support stream wrappers, so this
+ * function fills that gap.
+ *
+ * @param $uri
+ *   A URI or pathname pointing to the file or directory that will be checked.
+ *
+ * @return
+ *   TRUE if the file or directory is executable; FALSE otherwise.
+ *
+ * @see is_executable()
+ * @ingroup php_wrappers
+ */
+function drupal_is_executable($uri) {
+  // By converting the URI to a normal path using drupal_realpath(), we can
+  // correctly handle both stream wrappers and normal paths.
+  return is_executable(drupal_realpath($uri));
+}
+
 /**
  * Creates a .htaccess file in each Drupal files directory if it is missing.
  */
@@ -371,7 +412,7 @@ function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALS
   $htaccess_lines = FileStorage::htaccessLines($private);
 
   // Write the .htaccess file.
-  if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
+  if (file_exists($directory) && file_directory_is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
     return drupal_chmod($htaccess_path, 0444);
   }
   else {
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 9335221..80d271e 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -1836,7 +1836,7 @@ function install_check_translations($langcode, $server_pattern) {
   // Get values so the requirements errors can be specific.
   if (drupal_verify_install_file($translations_directory, FILE_EXIST, 'dir')) {
     $readable = is_readable($translations_directory);
-    $writable = is_writable($translations_directory);
+    $writable = file_directory_is_writable($translations_directory);
     $translations_directory_exists = TRUE;
   }
 
@@ -2018,7 +2018,7 @@ function install_check_requirements($install_state) {
     // Otherwise, if $file does not exist yet, we can try to copy
     // $default_file to create it.
     elseif (!$exists) {
-      $copied = drupal_verify_install_file($site_path, FILE_EXIST | FILE_WRITABLE, 'dir') && @copy($default_file, $file);
+      $copied = drupal_verify_install_file($site_path, FILE_EXIST | FILE_WRITABLE | FILE_EXECUTABLE, 'dir') && @copy($default_file, $file);
       if ($copied) {
         // If the new $file file has the same owner as $default_file this means
         // $default_file is owned by the webserver user. This is an inherent
diff --git a/core/includes/install.inc b/core/includes/install.inc
index 5c0abca..1cbda83 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -510,7 +510,7 @@ function drupal_install_config_directories() {
       ':handbook_url' => 'https://www.drupal.org/server-permissions',
     ]));
   }
-  elseif (is_writable($config_directories[CONFIG_SYNC_DIRECTORY])) {
+  elseif (file_directory_is_writable($config_directories[CONFIG_SYNC_DIRECTORY])) {
     // Put a README.txt into the sync config directory. This is required so that
     // they can later be added to git. Since this directory is auto-created, we
     // have to write out the README rather than just adding it to the drupal core
@@ -690,12 +690,12 @@ function drupal_verify_install_file($file, $mask = NULL, $type = 'file') {
             }
             break;
           case FILE_WRITABLE:
-            if (!is_writable($file) && !drupal_install_fix_file($file, $mask)) {
+            if (!file_directory_is_writable($file) && !drupal_install_fix_file($file, $mask)) {
               $return = FALSE;
             }
             break;
           case FILE_EXECUTABLE:
-            if (!is_executable($file) && !drupal_install_fix_file($file, $mask)) {
+            if (!drupal_is_executable($file) && !drupal_install_fix_file($file, $mask)) {
               $return = FALSE;
             }
             break;
@@ -705,12 +705,12 @@ function drupal_verify_install_file($file, $mask = NULL, $type = 'file') {
             }
             break;
           case FILE_NOT_WRITABLE:
-            if (is_writable($file) && !drupal_install_fix_file($file, $mask)) {
+            if (file_directory_is_writable($file) && !drupal_install_fix_file($file, $mask)) {
               $return = FALSE;
             }
             break;
           case FILE_NOT_EXECUTABLE:
-            if (is_executable($file) && !drupal_install_fix_file($file, $mask)) {
+            if (drupal_is_executable($file) && !drupal_install_fix_file($file, $mask)) {
               $return = FALSE;
             }
             break;
@@ -805,12 +805,12 @@ function drupal_install_fix_file($file, $mask, $message = TRUE) {
           }
           break;
         case FILE_WRITABLE:
-          if (!is_writable($file)) {
+          if (!file_directory_is_writable($file)) {
             $mod |= 0222;
           }
           break;
         case FILE_EXECUTABLE:
-          if (!is_executable($file)) {
+          if (!drupal_is_executable($file)) {
             $mod |= 0111;
           }
           break;
@@ -820,12 +820,12 @@ function drupal_install_fix_file($file, $mask, $message = TRUE) {
           }
           break;
         case FILE_NOT_WRITABLE:
-          if (is_writable($file)) {
+          if (file_directory_is_writable($file)) {
             $mod &= ~0222;
           }
           break;
         case FILE_NOT_EXECUTABLE:
-          if (is_executable($file)) {
+          if (drupal_is_executable($file)) {
             $mod &= ~0111;
           }
           break;
diff --git a/core/lib/Drupal/Component/FileSystem/FileSystem.php b/core/lib/Drupal/Component/FileSystem/FileSystem.php
index 7a1c551..27b4f57 100644
--- a/core/lib/Drupal/Component/FileSystem/FileSystem.php
+++ b/core/lib/Drupal/Component/FileSystem/FileSystem.php
@@ -34,7 +34,7 @@ public static function getOsTemporaryDirectory() {
     $directories[] = sys_get_temp_dir();
 
     foreach ($directories as $directory) {
-      if (is_dir($directory) && is_writable($directory)) {
+      if (is_dir($directory) && file_directory_is_writable($directory)) {
         // Both sys_get_temp_dir() and ini_get('upload_tmp_dir') can return paths
         // with a trailing directory separator.
         return rtrim($directory, DIRECTORY_SEPARATOR);
diff --git a/core/lib/Drupal/Core/Updater/Updater.php b/core/lib/Drupal/Core/Updater/Updater.php
index a1ba103..aecb3fd 100644
--- a/core/lib/Drupal/Core/Updater/Updater.php
+++ b/core/lib/Drupal/Core/Updater/Updater.php
@@ -305,7 +305,7 @@ public function prepareInstallDirectory(&$filetransfer, $directory) {
     // Make the parent dir writable if need be and create the dir.
     if (!is_dir($directory)) {
       $parent_dir = dirname($directory);
-      if (!is_writable($parent_dir)) {
+      if (!file_directory_is_writable($parent_dir)) {
         @chmod($parent_dir, 0755);
         // It is expected that this will fail if the directory is owned by the
         // FTP user. If the FTP user == web server, it will succeed.
@@ -347,7 +347,7 @@ public function prepareInstallDirectory(&$filetransfer, $directory) {
    *   If the chmod should be applied recursively.
    */
   public function makeWorldReadable(&$filetransfer, $path, $recursive = TRUE) {
-    if (!is_executable($path)) {
+    if (!drupal_is_executable($path)) {
       // Set it to read + execute.
       $new_perms = substr(sprintf('%o', fileperms($path)), -4, -1) . "5";
       $filetransfer->chmod($path, intval($new_perms, 8), $recursive);
diff --git a/core/modules/config/config.install b/core/modules/config/config.install
index c971ae6..3d9adea 100644
--- a/core/modules/config/config.install
+++ b/core/modules/config/config.install
@@ -21,7 +21,7 @@ function config_requirements($phase) {
   // Ensure the configuration sync directory is writable. This is only a warning
   // because only configuration import from a tarball requires the folder to be
   // web writable.
-  if ($phase !== 'install' && !is_writable($directory)) {
+  if ($phase !== 'install' && !file_directory_is_writable($directory)) {
     $requirements['config directory ' . CONFIG_SYNC_DIRECTORY] = [
       'title' => t('Configuration directory: %type', ['%type' => CONFIG_SYNC_DIRECTORY]),
       'description' => t('The directory %directory is not writable.', ['%directory' => $directory]),
diff --git a/core/modules/config/src/Form/ConfigImportForm.php b/core/modules/config/src/Form/ConfigImportForm.php
index 0c3def8..391509e 100644
--- a/core/modules/config/src/Form/ConfigImportForm.php
+++ b/core/modules/config/src/Form/ConfigImportForm.php
@@ -51,7 +51,7 @@ public function getFormId() {
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
     $directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
-    $directory_is_writable = is_writable($directory);
+    $directory_is_writable = file_directory_is_writable($directory);
     if (!$directory_is_writable) {
       drupal_set_message($this->t('The directory %directory is not writable.', ['%directory' => $directory]), 'error');
     }
diff --git a/core/modules/system/src/Form/PerformanceForm.php b/core/modules/system/src/Form/PerformanceForm.php
index 33f99db..4c0f90d 100644
--- a/core/modules/system/src/Form/PerformanceForm.php
+++ b/core/modules/system/src/Form/PerformanceForm.php
@@ -121,7 +121,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
 
     $directory = 'public://';
-    $is_writable = is_dir($directory) && is_writable($directory);
+    $is_writable = is_dir($directory) && file_directory_is_writable($directory);
     $disabled = !$is_writable;
     $disabled_message = '';
     if (!$is_writable) {
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 33fa077..2f008b9 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -643,7 +643,7 @@ function system_requirements($phase) {
     if ($phase == 'install') {
       file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
     }
-    $is_writable = is_writable($directory);
+    $is_writable = file_directory_is_writable($directory);
     $is_directory = is_dir($directory);
     if (!$is_writable || !$is_directory) {
       $description = '';
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index bde0346..38edd5e 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -907,7 +907,7 @@ function system_check_directory($form_element, FormStateInterface $form_state) {
     $logger->error('The directory %directory does not exist and could not be created.', ['%directory' => $directory]);
   }
 
-  if (is_dir($directory) && !is_writable($directory) && !drupal_chmod($directory)) {
+  if (is_dir($directory) && !file_directory_is_writable($directory) && !drupal_chmod($directory)) {
     // If the directory is not writable and cannot be made so.
     $form_state->setErrorByName($form_element['#parents'][0], t('The directory %directory exists but is not writable and could not be made writable.', ['%directory' => $directory]));
     $logger->error('The directory %directory exists but is not writable and could not be made writable.', ['%directory' => $directory]);
diff --git a/core/modules/system/tests/src/Functional/System/SitesDirectoryHardeningTest.php b/core/modules/system/tests/src/Functional/System/SitesDirectoryHardeningTest.php
index fbe97f4..bd8ea9d 100644
--- a/core/modules/system/tests/src/Functional/System/SitesDirectoryHardeningTest.php
+++ b/core/modules/system/tests/src/Functional/System/SitesDirectoryHardeningTest.php
@@ -57,8 +57,8 @@ public function testSitesDirectoryHardeningConfig() {
     $this->assertEqual(REQUIREMENT_WARNING, $requirements['configuration_files']['severity'], 'Warning severity is properly set.');
     $this->assertEqual($this->t('Protection disabled'), (string) $requirements['configuration_files']['description']['#context']['configuration_error_list']['#items'][0], 'Description is properly set.');
 
-    $this->assertTrue(is_writable($site_path), 'Site directory remains writable when automatically fixing permissions is disabled.');
-    $this->assertTrue(is_writable($settings_file), 'settings.php remains writable when automatically fixing permissions is disabled.');
+    $this->assertTrue(file_directory_is_writable($site_path), 'Site directory remains writable when automatically fixing permissions is disabled.');
+    $this->assertTrue(file_directory_is_writable($settings_file), 'settings.php remains writable when automatically fixing permissions is disabled.');
 
     // Re-enable permissions enforcement.
     $settings = Settings::getAll();
@@ -68,8 +68,8 @@ public function testSitesDirectoryHardeningConfig() {
     // Manually trigger the requirements check.
     $this->checkSystemRequirements();
 
-    $this->assertFalse(is_writable($site_path), 'Site directory is protected when automatically fixing permissions is enabled.');
-    $this->assertFalse(is_writable($settings_file), 'settings.php is protected when automatically fixing permissions is enabled.');
+    $this->assertFalse(file_directory_is_writable($site_path), 'Site directory is protected when automatically fixing permissions is enabled.');
+    $this->assertFalse(file_directory_is_writable($settings_file), 'settings.php is protected when automatically fixing permissions is enabled.');
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php
index ac22072..d4943e9 100644
--- a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php
+++ b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php
@@ -24,7 +24,7 @@ public function __construct($out, $verbose, $colors, $debug, $numberOfColumns) {
       $html_output_directory = rtrim($html_output_directory, '/');
 
       // Check if directory exists.
-      if (!is_dir($html_output_directory) || !is_writable($html_output_directory)) {
+      if (!is_dir($html_output_directory) || !file_directory_is_writable($html_output_directory)) {
         $this->writeWithColor('bg-red, fg-black', "HTML output directory $html_output_directory is not a writable directory.");
       }
       else {
