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