Blame SOURCES/0002-glocalfileoutputstream-Fix-CREATE_REPLACE_DESTINATIO.patch

e5da31
From 6c10e8ce6905e8fcc3466eb8af707b5d0d3bdb85 Mon Sep 17 00:00:00 2001
e5da31
From: Philip Withnall <pwithnall@endlessos.org>
e5da31
Date: Wed, 24 Feb 2021 17:36:07 +0000
e5da31
Subject: [PATCH 2/3] glocalfileoutputstream: Fix CREATE_REPLACE_DESTINATION
e5da31
 with symlinks
e5da31
MIME-Version: 1.0
e5da31
Content-Type: text/plain; charset=UTF-8
e5da31
Content-Transfer-Encoding: 8bit
e5da31
e5da31
The `G_FILE_CREATE_REPLACE_DESTINATION` flag is equivalent to unlinking
e5da31
the destination file and re-creating it from scratch. That did
e5da31
previously work, but in the process the code would call `open(O_CREAT)`
e5da31
on the file. If the file was a dangling symlink, this would create the
e5da31
destination file (empty). That’s not an intended side-effect, and has
e5da31
security implications if the symlink is controlled by a lower-privileged
e5da31
process.
e5da31
e5da31
Fix that by not opening the destination file if it’s a symlink, and
e5da31
adjusting the rest of the code to cope with
e5da31
 - the fact that `fd == -1` is not an error iff `is_symlink` is true,
e5da31
 - and that `original_stat` will contain the `lstat()` results for the
e5da31
   symlink now, rather than the `stat()` results for its target (again,
e5da31
   iff `is_symlink` is true).
e5da31
e5da31
This means that the target of the dangling symlink is no longer created,
e5da31
which was the bug. The symlink itself continues to be replaced (as
e5da31
before) with the new file — this is the intended behaviour of
e5da31
`g_file_replace()`.
e5da31
e5da31
The behaviour for non-symlink cases, or cases where the symlink was not
e5da31
dangling, should be unchanged.
e5da31
e5da31
Includes a unit test.
e5da31
e5da31
Signed-off-by: Philip Withnall <pwithnall@endlessos.org>
e5da31
e5da31
Fixes: #2325
e5da31
---
e5da31
 gio/glocalfileoutputstream.c |  63 ++++++++++++++-------
e5da31
 gio/tests/file.c             | 107 ++++++++++++++++++++++++++++++++++-
e5da31
 2 files changed, 149 insertions(+), 21 deletions(-)
e5da31
e5da31
diff --git a/gio/glocalfileoutputstream.c b/gio/glocalfileoutputstream.c
e5da31
index 6a70b2a04..4a7766f68 100644
e5da31
--- a/gio/glocalfileoutputstream.c
e5da31
+++ b/gio/glocalfileoutputstream.c
e5da31
@@ -779,16 +779,22 @@ handle_overwrite_open (const char    *filename,
e5da31
       /* Could be a symlink, or it could be a regular ELOOP error,
e5da31
        * but then the next open will fail too. */
e5da31
       is_symlink = TRUE;
e5da31
-      fd = g_open (filename, open_flags, mode);
e5da31
+      if (!replace_destination_set)
e5da31
+        fd = g_open (filename, open_flags, mode);
e5da31
     }
e5da31
-#else
e5da31
-  fd = g_open (filename, open_flags, mode);
e5da31
-  errsv = errno;
e5da31
+#else  /* if !O_NOFOLLOW */
e5da31
   /* This is racy, but we do it as soon as possible to minimize the race */
e5da31
   is_symlink = g_file_test (filename, G_FILE_TEST_IS_SYMLINK);
e5da31
+
e5da31
+  if (!is_symlink || !replace_destination_set)
e5da31
+    {
e5da31
+      fd = g_open (filename, open_flags, mode);
e5da31
+      errsv = errno;
e5da31
+    }
e5da31
 #endif
e5da31
 
e5da31
-  if (fd == -1)
e5da31
+  if (fd == -1 &&
e5da31
+      (!is_symlink || !replace_destination_set))
e5da31
     {
e5da31
       char *display_name = g_filename_display_name (filename);
e5da31
       g_set_error (error, G_IO_ERROR,
e5da31
@@ -800,10 +806,17 @@ handle_overwrite_open (const char    *filename,
e5da31
     }
e5da31
   
e5da31
 #ifdef G_OS_WIN32
e5da31
-  res = GLIB_PRIVATE_CALL (g_win32_fstat) (fd, &original_stat);
e5da31
-#else
e5da31
-  res = fstat (fd, &original_stat);
e5da31
+#error This patch has not been ported to Windows, sorry
e5da31
 #endif
e5da31
+
e5da31
+  if (!is_symlink)
e5da31
+    {
e5da31
+      res = fstat (fd, &original_stat);
e5da31
+    }
e5da31
+  else
e5da31
+    {
e5da31
+      res = lstat (filename, &original_stat);
e5da31
+    }
e5da31
   errsv = errno;
e5da31
 
e5da31
   if (res != 0)
e5da31
@@ -821,16 +834,27 @@ handle_overwrite_open (const char    *filename,
e5da31
   if (!S_ISREG (original_stat.st_mode))
e5da31
     {
e5da31
       if (S_ISDIR (original_stat.st_mode))
e5da31
-	g_set_error_literal (error,
e5da31
-                             G_IO_ERROR,
e5da31
-                             G_IO_ERROR_IS_DIRECTORY,
e5da31
-                             _("Target file is a directory"));
e5da31
-      else
e5da31
-	g_set_error_literal (error,
e5da31
-                             G_IO_ERROR,
e5da31
-                             G_IO_ERROR_NOT_REGULAR_FILE,
e5da31
-                             _("Target file is not a regular file"));
e5da31
-      goto err_out;
e5da31
+        {
e5da31
+          g_set_error_literal (error,
e5da31
+                               G_IO_ERROR,
e5da31
+                               G_IO_ERROR_IS_DIRECTORY,
e5da31
+                               _("Target file is a directory"));
e5da31
+          goto err_out;
e5da31
+        }
e5da31
+      else if (!is_symlink ||
e5da31
+#ifdef S_ISLNK
e5da31
+               !S_ISLNK (original_stat.st_mode)
e5da31
+#else
e5da31
+               FALSE
e5da31
+#endif
e5da31
+               )
e5da31
+        {
e5da31
+          g_set_error_literal (error,
e5da31
+                               G_IO_ERROR,
e5da31
+                               G_IO_ERROR_NOT_REGULAR_FILE,
e5da31
+                               _("Target file is not a regular file"));
e5da31
+          goto err_out;
e5da31
+        }
e5da31
     }
e5da31
   
e5da31
   if (etag != NULL)
e5da31
@@ -911,7 +935,8 @@ handle_overwrite_open (const char    *filename,
e5da31
 	    }
e5da31
 	}
e5da31
 
e5da31
-      g_close (fd, NULL);
e5da31
+      if (fd >= 0)
e5da31
+        g_close (fd, NULL);
e5da31
       *temp_filename = tmp_filename;
e5da31
       return tmpfd;
e5da31
     }
e5da31
diff --git a/gio/tests/file.c b/gio/tests/file.c
e5da31
index 98eeb85d4..44db6e295 100644
e5da31
--- a/gio/tests/file.c
e5da31
+++ b/gio/tests/file.c
e5da31
@@ -671,8 +671,6 @@ test_replace_cancel (void)
e5da31
   guint count;
e5da31
   GError *error = NULL;
e5da31
 
e5da31
-  g_test_bug ("629301");
e5da31
-
e5da31
   path = g_dir_make_tmp ("g_file_replace_cancel_XXXXXX", &error);
e5da31
   g_assert_no_error (error);
e5da31
   tmpdir = g_file_new_for_path (path);
e5da31
@@ -779,6 +777,110 @@ test_replace_cancel (void)
e5da31
   g_object_unref (tmpdir);
e5da31
 }
e5da31
 
e5da31
+static void
e5da31
+test_replace_symlink (void)
e5da31
+{
e5da31
+#ifdef G_OS_UNIX
e5da31
+  gchar *tmpdir_path = NULL;
e5da31
+  GFile *tmpdir = NULL, *source_file = NULL, *target_file = NULL;
e5da31
+  GFileOutputStream *stream = NULL;
e5da31
+  const gchar *new_contents = "this is a test message which should be written to source and not target";
e5da31
+  gsize n_written;
e5da31
+  GFileEnumerator *enumerator = NULL;
e5da31
+  GFileInfo *info = NULL;
e5da31
+  gchar *contents = NULL;
e5da31
+  gsize length = 0;
e5da31
+  GError *local_error = NULL;
e5da31
+
e5da31
+  /* Create a fresh, empty working directory. */
e5da31
+  tmpdir_path = g_dir_make_tmp ("g_file_replace_symlink_XXXXXX", &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+  tmpdir = g_file_new_for_path (tmpdir_path);
e5da31
+
e5da31
+  g_test_message ("Using temporary directory %s", tmpdir_path);
e5da31
+  g_free (tmpdir_path);
e5da31
+
e5da31
+  /* Create symlink `source` which points to `target`. */
e5da31
+  source_file = g_file_get_child (tmpdir, "source");
e5da31
+  target_file = g_file_get_child (tmpdir, "target");
e5da31
+  g_file_make_symbolic_link (source_file, "target", NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+
e5da31
+  /* Ensure that `target` doesn’t exist */
e5da31
+  g_assert_false (g_file_query_exists (target_file, NULL));
e5da31
+
e5da31
+  /* Replace the `source` symlink with a regular file using
e5da31
+   * %G_FILE_CREATE_REPLACE_DESTINATION, which should replace it *without*
e5da31
+   * following the symlink */
e5da31
+  stream = g_file_replace (source_file, NULL, FALSE  /* no backup */,
e5da31
+                           G_FILE_CREATE_REPLACE_DESTINATION, NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+
e5da31
+  g_output_stream_write_all (G_OUTPUT_STREAM (stream), new_contents, strlen (new_contents),
e5da31
+                             &n_written, NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+  g_assert_cmpint (n_written, ==, strlen (new_contents));
e5da31
+
e5da31
+  g_output_stream_close (G_OUTPUT_STREAM (stream), NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+
e5da31
+  g_clear_object (&stream);
e5da31
+
e5da31
+  /* At this point, there should still only be one file: `source`. It should
e5da31
+   * now be a regular file. `target` should not exist. */
e5da31
+  enumerator = g_file_enumerate_children (tmpdir,
e5da31
+                                          G_FILE_ATTRIBUTE_STANDARD_NAME ","
e5da31
+                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
e5da31
+                                          G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+
e5da31
+  info = g_file_enumerator_next_file (enumerator, NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+  g_assert_nonnull (info);
e5da31
+
e5da31
+  g_assert_cmpstr (g_file_info_get_name (info), ==, "source");
e5da31
+  g_assert_cmpint (g_file_info_get_file_type (info), ==, G_FILE_TYPE_REGULAR);
e5da31
+
e5da31
+  g_clear_object (&info;;
e5da31
+
e5da31
+  info = g_file_enumerator_next_file (enumerator, NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+  g_assert_null (info);
e5da31
+
e5da31
+  g_file_enumerator_close (enumerator, NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+  g_clear_object (&enumerator);
e5da31
+
e5da31
+  /* Double-check that `target` doesn’t exist */
e5da31
+  g_assert_false (g_file_query_exists (target_file, NULL));
e5da31
+
e5da31
+  /* Check the content of `source`. */
e5da31
+  g_file_load_contents (source_file,
e5da31
+                        NULL,
e5da31
+                        &contents,
e5da31
+                        &length,
e5da31
+                        NULL,
e5da31
+                        &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+  g_assert_cmpstr (contents, ==, new_contents);
e5da31
+  g_assert_cmpuint (length, ==, strlen (new_contents));
e5da31
+  g_free (contents);
e5da31
+
e5da31
+  /* Tidy up. */
e5da31
+  g_file_delete (source_file, NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+
e5da31
+  g_file_delete (tmpdir, NULL, &local_error);
e5da31
+  g_assert_no_error (local_error);
e5da31
+
e5da31
+  g_clear_object (&target_file);
e5da31
+  g_clear_object (&source_file);
e5da31
+  g_clear_object (&tmpdir);
e5da31
+#else  /* if !G_OS_UNIX */
e5da31
+  g_test_skip ("Symlink replacement tests can only be run on Unix")
e5da31
+#endif
e5da31
+}
e5da31
+
e5da31
 static void
e5da31
 on_file_deleted (GObject      *object,
e5da31
 		 GAsyncResult *result,
e5da31
@@ -1170,6 +1272,7 @@ main (int argc, char *argv[])
e5da31
   g_test_add_data_func ("/file/async-create-delete/4096", GINT_TO_POINTER (4096), test_create_delete);
e5da31
   g_test_add_func ("/file/replace-load", test_replace_load);
e5da31
   g_test_add_func ("/file/replace-cancel", test_replace_cancel);
e5da31
+  g_test_add_func ("/file/replace-symlink", test_replace_symlink);
e5da31
   g_test_add_func ("/file/async-delete", test_async_delete);
e5da31
 #ifdef G_OS_UNIX
e5da31
   g_test_add_func ("/file/copy-preserve-mode", test_copy_preserve_mode);
e5da31
-- 
e5da31
2.31.1
e5da31