diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 7d1b4da8a98679f55c7da3682881321479fd3c65..d532351b81ff98e347185c077dbcdbc29210e028 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -1972,7 +1972,7 @@ function install_check_translations($langcode, $server_pattern): array {
   // 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_system->isWritable($translations_directory);
     $translations_directory_exists = TRUE;
   }
 
@@ -2162,7 +2162,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 d1e7e86566619f32f6389b4e45f54c272abe001a..5050b82014ab9404b541237122fef6400fa1cd1c 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -309,6 +309,8 @@ function drupal_verify_install_file($file, $mask = NULL, $type = 'file', $auto_f
     }
   }
 
+  /** @var \Drupal\Core\File\FileSystemInterface $file_system */
+  $file_system = \Drupal::service('file_system');
   // Verify file permissions.
   if (isset($mask)) {
     $masks = [FILE_EXIST, FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE];
@@ -333,13 +335,13 @@ function drupal_verify_install_file($file, $mask = NULL, $type = 'file', $auto_f
             break;
 
           case FILE_WRITABLE:
-            if (!is_writable($file)) {
+            if (!$file_system->isWritable($file)) {
               $return = FALSE;
             }
             break;
 
           case FILE_EXECUTABLE:
-            if (!is_executable($file)) {
+            if (!$file_system->isExecutable($file)) {
               $return = FALSE;
             }
             break;
@@ -351,13 +353,13 @@ function drupal_verify_install_file($file, $mask = NULL, $type = 'file', $auto_f
             break;
 
           case FILE_NOT_WRITABLE:
-            if (is_writable($file)) {
+            if ($file_system->isWritable($file)) {
               $return = FALSE;
             }
             break;
 
           case FILE_NOT_EXECUTABLE:
-            if (is_executable($file)) {
+            if ($file_system->isExecutable($file)) {
               $return = FALSE;
             }
             break;
@@ -440,7 +442,8 @@ function drupal_install_fix_file($file, $mask, $message = TRUE) {
   if (!file_exists($file)) {
     return FALSE;
   }
-
+  /** @var \Drupal\Core\File\FileSystemInterface $file_system */
+  $file_system = \Drupal::service('file_system');
   $mod = fileperms($file) & 0777;
   $masks = [FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE];
 
@@ -458,13 +461,13 @@ function drupal_install_fix_file($file, $mask, $message = TRUE) {
           break;
 
         case FILE_WRITABLE:
-          if (!is_writable($file)) {
+          if (!$file_system->isWritable($file)) {
             $mod |= 0222;
           }
           break;
 
         case FILE_EXECUTABLE:
-          if (!is_executable($file)) {
+          if (!$file_system->isExecutable($file)) {
             $mod |= 0111;
           }
           break;
@@ -476,13 +479,13 @@ function drupal_install_fix_file($file, $mask, $message = TRUE) {
           break;
 
         case FILE_NOT_WRITABLE:
-          if (is_writable($file)) {
+          if ($file_system->isWritable($file)) {
             $mod &= ~0222;
           }
           break;
 
         case FILE_NOT_EXECUTABLE:
-          if (is_executable($file)) {
+          if ($file_system->isExecutable($file)) {
             $mod &= ~0111;
           }
           break;
diff --git a/core/lib/Drupal/Component/FileSystem/FileSystem.php b/core/lib/Drupal/Component/FileSystem/FileSystem.php
index b4e8389d755acd4d14a387cc5ad0fcb2ee896712..85305132d877d2476808d128c57f18a861113969 100644
--- a/core/lib/Drupal/Component/FileSystem/FileSystem.php
+++ b/core/lib/Drupal/Component/FileSystem/FileSystem.php
@@ -36,7 +36,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) && is_writable($directory) && is_executable($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/File/FileSystem.php b/core/lib/Drupal/Core/File/FileSystem.php
index 8920c3daf1e5e881eee24d5f4c2ddd7ac75d7138..7d8f05a68e9efbcd03f77ed543e3bfbd270155f2 100644
--- a/core/lib/Drupal/Core/File/FileSystem.php
+++ b/core/lib/Drupal/Core/File/FileSystem.php
@@ -531,7 +531,7 @@ public function prepareDirectory(&$directory, $options = self::MODIFY_PERMISSION
       }
     }
 
-    $writable = is_writable($directory);
+    $writable = $this->isWritable($directory);
     if (!$writable && ($options & static::MODIFY_PERMISSIONS)) {
       return $this->chmod($directory);
     }
@@ -734,4 +734,50 @@ protected function doScanDirectory($dir, $mask, array $options = [], $depth = 0)
     return array_merge(array_merge(...$files_in_sub_dirs), $files_in_this_directory);
   }
 
+  /**
+   * Determines if a directory is writable by the web server.
+   *
+   * PHP's is_writable() does not fully support stream wrappers, so this
+   * function fills that gap.
+   * 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 string $uri
+   *   A URI or pathname pointing to the directory that will be checked.
+   *
+   * @return bool
+   *   TRUE if the directory is writable and executable; FALSE otherwise.
+   */
+  public function isWritable($uri) {
+    // By converting the URI to a normal path using drupal_realpath(), we can
+    // correctly handle both stream wrappers and normal paths.
+    $realpath = $this->realpath($uri);
+    return is_writable($realpath ?: $uri) && $this->isExecutable($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 string $uri
+   *   A URI or pathname pointing to the file or directory that will be checked.
+   *
+   * @return bool
+   *   TRUE if the file or directory is executable; FALSE otherwise.
+   *
+   * @see is_executable()
+   * @ingroup php_wrappers
+   */
+  public function isExecutable($uri) {
+    // By converting the URI to a normal path using drupal_realpath(), we can
+    // correctly handle both stream wrappers and normal paths.
+    $realpath = $this->realpath($uri);
+    $filename = $realpath ?: $uri;
+    // Determine whether the URI is an executable file or a directory.
+    return is_executable($filename) || is_dir($filename);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/File/FileSystemInterface.php b/core/lib/Drupal/Core/File/FileSystemInterface.php
index f3beabfe22093d74e1329d02ddb49fdfb3148d4a..8f3967d0a2b0cf4b7dfd7a8cd4312fa84f697302 100644
--- a/core/lib/Drupal/Core/File/FileSystemInterface.php
+++ b/core/lib/Drupal/Core/File/FileSystemInterface.php
@@ -518,4 +518,38 @@ public function getTempDirectory();
    */
   public function scanDirectory($dir, $mask, array $options = []);
 
+  /**
+   * Determines if a directory is writable by the web server.
+   *
+   * PHP's is_writable() does not fully support stream wrappers, so this
+   * function fills that gap.
+   * 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 string $uri
+   *   A URI or pathname pointing to the directory that will be checked.
+   *
+   * @return bool
+   *   TRUE if the directory is writable and executable; FALSE otherwise.
+   */
+  public function isWritable($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 string $uri
+   *   A URI or pathname pointing to the file or directory that will be checked.
+   *
+   * @return bool
+   *   TRUE if the file or directory is executable; FALSE otherwise.
+   *
+   * @see is_executable()
+   * @ingroup php_wrappers
+   */
+  public function isExecutable($uri);
+
 }
diff --git a/core/lib/Drupal/Core/Updater/Updater.php b/core/lib/Drupal/Core/Updater/Updater.php
index 908fdf7616a0a87cb0ebf5169162565f5762885e..d0213ec7358b45e8f188dcc36216f7fc67ece605 100644
--- a/core/lib/Drupal/Core/Updater/Updater.php
+++ b/core/lib/Drupal/Core/Updater/Updater.php
@@ -339,7 +339,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 (!\Drupal::service('file_system')->isWritable($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.
@@ -385,7 +385,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::service('file_system')->isExecutable($path)) {
       // Set it to read + execute.
       $new_perms = fileperms($path) & 0777 | 0005;
       $filetransfer->chmod($path, $new_perms, $recursive);
diff --git a/core/modules/config/src/Form/ConfigImportForm.php b/core/modules/config/src/Form/ConfigImportForm.php
index 74c73b8d999ac5d99e442d9bac367ed8baac6e86..bcd47d4e0ed82811efa1b23c6241dee0d1d51b98 100644
--- a/core/modules/config/src/Form/ConfigImportForm.php
+++ b/core/modules/config/src/Form/ConfigImportForm.php
@@ -77,7 +77,7 @@ public function getFormId() {
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
     $directory = $this->settings->get('config_sync_directory');
-    $directory_is_writable = is_writable($directory);
+    $directory_is_writable = \Drupal::service('file_system')->isWritable($directory);
     if (!$directory_is_writable) {
       $this->messenger()->addError($this->t('The directory %directory is not writable.', ['%directory' => $directory]));
     }
diff --git a/core/modules/media/media.install b/core/modules/media/media.install
index f0acbf482da43f9231e14201e5c89803db7c507e..f3514d1128c50975e49f5e0384785f8981c5a2d9 100644
--- a/core/modules/media/media.install
+++ b/core/modules/media/media.install
@@ -23,7 +23,7 @@ function media_requirements($phase): array {
   if ($phase == 'install') {
     $destination = 'public://media-icons/generic';
     \Drupal::service('file_system')->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
-    $is_writable = is_writable($destination);
+    $is_writable = \Drupal::service('file_system')->isWritable($destination);
     $is_directory = is_dir($destination);
     if (!$is_writable || !$is_directory) {
       if (!$is_directory) {
diff --git a/core/modules/migrate/src/Plugin/migrate/process/FileCopy.php b/core/modules/migrate/src/Plugin/migrate/process/FileCopy.php
index 61d0ace73d0a796c2ecc56612fa4361c4cf9184c..030fa184b62c0c27364fef00ecb51f1afab24125 100644
--- a/core/modules/migrate/src/Plugin/migrate/process/FileCopy.php
+++ b/core/modules/migrate/src/Plugin/migrate/process/FileCopy.php
@@ -145,7 +145,7 @@ public function transform($value, MigrateExecutableInterface $migrate_executable
     // If the directory exists and is writable, avoid
     // \Drupal\Core\File\FileSystemInterface::prepareDirectory() call and write
     // the file to destination.
-    if (!is_dir($dir) || !is_writable($dir)) {
+    if (!is_dir($dir) || !$this->fileSystem->isWritable($dir)) {
       if (!$this->fileSystem->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
         throw new MigrateException("Could not create or write to directory '$dir'");
       }
diff --git a/core/modules/system/src/Form/PerformanceForm.php b/core/modules/system/src/Form/PerformanceForm.php
index 3a1cefb471bf781de365d17e474ee2c68b5c5e06..1567bc5a0a1232aec76d53613ab51b34865def63 100644
--- a/core/modules/system/src/Form/PerformanceForm.php
+++ b/core/modules/system/src/Form/PerformanceForm.php
@@ -129,7 +129,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
 
     $directory = 'assets://';
-    $is_writable = is_dir($directory) && is_writable($directory);
+    $is_writable = is_dir($directory) && \Drupal::service('file_system')->isWritable($directory);
     $disabled = !$is_writable;
     $disabled_message = '';
     if (!$is_writable) {
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 8da736101f1cb942dde3d9939c7e79f64ed5d14c..3f0e5fe2315924dcd7524edd4045225b442120ae 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -859,7 +859,7 @@ function system_requirements($phase): array {
     if ($phase == 'install') {
       \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
     }
-    $is_writable = is_writable($directory);
+    $is_writable = \Drupal::service('file_system')->isWritable($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 c7ed1808206f4ca5e21284d6e73d54b69343cdc0..cfba75fbd1065e7d0c343071ba2c4008bc24da85 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -356,7 +356,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) && !$file_system->chmod($directory)) {
+  if (is_dir($directory) && !$file_system->isWritable($directory) && !$file_system->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]);