4200aa
From 8fef6abe1131da0c8a7211c740a12ebe11cbcc51 Mon Sep 17 00:00:00 2001
4200aa
From: Philip Withnall <pwithnall@endlessos.org>
4200aa
Date: Wed, 10 Mar 2021 16:05:55 +0000
4200aa
Subject: [PATCH 1/3] glocalfileoutputstream: Factor out a flag check
4200aa
4200aa
This clarifies the code a little. It introduces no functional changes.
4200aa
4200aa
Signed-off-by: Philip Withnall <pwithnall@endlessos.org>
4200aa
---
4200aa
 gio/glocalfileoutputstream.c | 9 +++++----
4200aa
 1 file changed, 5 insertions(+), 4 deletions(-)
4200aa
4200aa
diff --git a/gio/glocalfileoutputstream.c b/gio/glocalfileoutputstream.c
4200aa
index 57d2d5dfe..6a70b2a04 100644
4200aa
--- a/gio/glocalfileoutputstream.c
4200aa
+++ b/gio/glocalfileoutputstream.c
4200aa
@@ -751,6 +751,7 @@ handle_overwrite_open (const char    *filename,
4200aa
   int res;
4200aa
   int mode;
4200aa
   int errsv;
4200aa
+  gboolean replace_destination_set = (flags & G_FILE_CREATE_REPLACE_DESTINATION);
4200aa
 
4200aa
   mode = mode_from_flags_or_info (flags, reference_info);
4200aa
 
4200aa
@@ -857,8 +858,8 @@ handle_overwrite_open (const char    *filename,
4200aa
    * The second strategy consist simply in copying the old file
4200aa
    * to a backup file and rewrite the contents of the file.
4200aa
    */
4200aa
-  
4200aa
-  if ((flags & G_FILE_CREATE_REPLACE_DESTINATION) ||
4200aa
+
4200aa
+  if (replace_destination_set ||
4200aa
       (!(original_stat.st_nlink > 1) && !is_symlink))
4200aa
     {
4200aa
       char *dirname, *tmp_filename;
4200aa
@@ -877,7 +878,7 @@ handle_overwrite_open (const char    *filename,
4200aa
       
4200aa
       /* try to keep permissions (unless replacing) */
4200aa
 
4200aa
-      if ( ! (flags & G_FILE_CREATE_REPLACE_DESTINATION) &&
4200aa
+      if (!replace_destination_set &&
4200aa
 	   (
4200aa
 #ifdef HAVE_FCHOWN
4200aa
 	    fchown (tmpfd, original_stat.st_uid, original_stat.st_gid) == -1 ||
4200aa
@@ -1016,7 +1017,7 @@ handle_overwrite_open (const char    *filename,
4200aa
 	}
4200aa
     }
4200aa
 
4200aa
-  if (flags & G_FILE_CREATE_REPLACE_DESTINATION)
4200aa
+  if (replace_destination_set)
4200aa
     {
4200aa
       g_close (fd, NULL);
4200aa
       
4200aa
-- 
4200aa
2.31.1
4200aa
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
4200aa
From 7f0b0d7fd744ad2f51236444005db49c80a0293d Mon Sep 17 00:00:00 2001
4200aa
From: Philip Withnall <pwithnall@endlessos.org>
4200aa
Date: Wed, 24 Feb 2021 17:42:24 +0000
4200aa
Subject: [PATCH 3/3] glocalfileoutputstream: Add a missing O_CLOEXEC flag to
4200aa
 replace()
4200aa
4200aa
Signed-off-by: Philip Withnall <pwithnall@endlessos.org>
4200aa
---
4200aa
 gio/glocalfileoutputstream.c | 15 ++++++++++++---
4200aa
 1 file changed, 12 insertions(+), 3 deletions(-)
4200aa
4200aa
diff --git a/gio/glocalfileoutputstream.c b/gio/glocalfileoutputstream.c
4200aa
index 4a7766f68..275770fa4 100644
4200aa
--- a/gio/glocalfileoutputstream.c
4200aa
+++ b/gio/glocalfileoutputstream.c
4200aa
@@ -56,6 +56,12 @@
4200aa
 #define O_BINARY 0
4200aa
 #endif
4200aa
 
4200aa
+#ifndef O_CLOEXEC
4200aa
+#define O_CLOEXEC 0
4200aa
+#else
4200aa
+#define HAVE_O_CLOEXEC 1
4200aa
+#endif
4200aa
+
4200aa
 struct _GLocalFileOutputStreamPrivate {
4200aa
   char *tmp_filename;
4200aa
   char *original_filename;
4200aa
@@ -1127,7 +1133,7 @@ _g_local_file_output_stream_replace (const char        *filename,
4200aa
   sync_on_close = FALSE;
4200aa
 
4200aa
   /* If the file doesn't exist, create it */
4200aa
-  open_flags = O_CREAT | O_EXCL | O_BINARY;
4200aa
+  open_flags = O_CREAT | O_EXCL | O_BINARY | O_CLOEXEC;
4200aa
   if (readable)
4200aa
     open_flags |= O_RDWR;
4200aa
   else
4200aa
@@ -1157,8 +1163,11 @@ _g_local_file_output_stream_replace (const char        *filename,
4200aa
       set_error_from_open_errno (filename, error);
4200aa
       return NULL;
4200aa
     }
4200aa
-  
4200aa
- 
4200aa
+#if !defined(HAVE_O_CLOEXEC) && defined(F_SETFD)
4200aa
+  else
4200aa
+    fcntl (fd, F_SETFD, FD_CLOEXEC);
4200aa
+#endif
4200aa
+
4200aa
   stream = g_object_new (G_TYPE_LOCAL_FILE_OUTPUT_STREAM, NULL);
4200aa
   stream->priv->fd = fd;
4200aa
   stream->priv->sync_on_close = sync_on_close;
4200aa
-- 
4200aa
2.31.1
4200aa