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