glib2/SOURCES/0002-glocalfileoutputstream-Fix-CREATE_REPLACE_DESTINATIO.patch
2021-09-09 17:38:49 +00:00

279 lines
9.6 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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