diff --git a/glnx-backports.h b/glnx-backports.h index 904b2e095..aabf8ddb9 100644 --- a/glnx-backports.h +++ b/glnx-backports.h @@ -95,6 +95,27 @@ _glnx_clear_fd (int *fd_ptr, #endif +/* This is part of the backport of g_autofd, but we define it + * unconditionally because it's also used to implement glnx_close_fd() */ +static inline void +_glnx_clear_fd_ignore_error (int *fd_ptr) +{ + /* Don't overwrite thread-local errno if closing the fd fails */ + int errsv = errno; + + if (!g_clear_fd (fd_ptr, NULL)) + { + /* Do nothing: we ignore all errors, except for EBADF which + * is a programming error, checked for by g_close(). */ + } + + errno = errsv; +} + +#if !GLIB_CHECK_VERSION(2, 76, 0) +#define g_autofd __attribute__((cleanup(_glnx_clear_fd_ignore_error))) +#endif + #if !GLIB_CHECK_VERSION(2, 40, 0) #define g_info(...) g_log (G_LOG_DOMAIN, G_LOG_LEVEL_INFO, __VA_ARGS__) #endif diff --git a/glnx-local-alloc.h b/glnx-local-alloc.h index a6e0e9b4f..9ac8a027d 100644 --- a/glnx-local-alloc.h +++ b/glnx-local-alloc.h @@ -51,38 +51,23 @@ glnx_local_obj_unref (void *v) * glnx_close_fd: * @fdp: Pointer to fd * - * Effectively `close (g_steal_fd (&fd))`. Also - * asserts that `close()` did not raise `EBADF` - encountering - * that error is usually a critical bug in the program. + * Same as `g_clear_fd()`, but ignoring the error (if any) and making sure + * not to alter `errno`. As a result, this function can be used for cleanup + * in contexts where `errno` needs to be preserved. */ -static inline void -glnx_close_fd (int *fdp) -{ - int errsv; - - g_assert (fdp); - - int fd = g_steal_fd (fdp); - if (fd >= 0) - { - errsv = errno; - if (close (fd) < 0) - g_assert (errno != EBADF); - errno = errsv; - } -} +#define glnx_close_fd _glnx_clear_fd_ignore_error /** * glnx_fd_close: * - * Deprecated in favor of `glnx_autofd`. + * Deprecated in favor of `g_autofd`. */ -#define glnx_fd_close __attribute__((cleanup(glnx_close_fd))) +#define glnx_fd_close g_autofd /** * glnx_autofd: * - * Call close() on a variable location when it goes out of scope. + * Deprecated in favor of `g_autofd`. */ -#define glnx_autofd __attribute__((cleanup(glnx_close_fd))) +#define glnx_autofd g_autofd G_END_DECLS diff --git a/tests/libglnx-testlib.c b/tests/libglnx-testlib.c index 3eb2ba143..22c38981d 100644 --- a/tests/libglnx-testlib.c +++ b/tests/libglnx-testlib.c @@ -23,6 +23,7 @@ #include "libglnx-testlib.h" #include +#include #include @@ -72,3 +73,21 @@ _glnx_test_auto_temp_dir_leave (_GLnxTestAutoTempDir *dir) g_free (dir->old_cwd); g_free (dir); } + +void +_glnx_test_assert_fd_was_closed (int fd) +{ + /* We can't tell a fd was really closed without behaving as though it + * was still valid */ + if (g_test_undefined ()) + { + int result; + int errsv; + + errno = 0; + result = fcntl (fd, F_GETFD, 0); + errsv = errno; + g_assert_cmpint (result, <, 0); + g_assert_cmpint (errsv, ==, EBADF); + } +} diff --git a/tests/libglnx-testlib.h b/tests/libglnx-testlib.h index dccc7e558..009de74b8 100644 --- a/tests/libglnx-testlib.h +++ b/tests/libglnx-testlib.h @@ -1,7 +1,7 @@ /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright (C) 2017 Red Hat, Inc. - * Copyright 2019 Collabora Ltd. + * Copyright 2019-2022 Collabora Ltd. * SPDX-License-Identifier: LGPL-2.0-or-later * * This library is free software; you can redistribute it and/or @@ -23,6 +23,7 @@ #pragma once #include +#include #include "glnx-backport-autoptr.h" @@ -47,3 +48,5 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(_GLnxTestAutoTempDir, _glnx_test_auto_temp_dir_lea #define _GLNX_TEST_SCOPED_TEMP_DIR \ G_GNUC_UNUSED g_autoptr(_GLnxTestAutoTempDir) temp_dir = _glnx_test_auto_temp_dir_enter () + +void _glnx_test_assert_fd_was_closed (int fd); diff --git a/tests/test-libglnx-fdio.c b/tests/test-libglnx-fdio.c index cfa5b648a..e237589c9 100644 --- a/tests/test-libglnx-fdio.c +++ b/tests/test-libglnx-fdio.c @@ -29,6 +29,125 @@ #include "libglnx-testlib.h" +static void +test_close (void) +{ + g_autoptr(GError) error = NULL; + int errsv; + int fd = -2; + int fd_borrowed; + + g_test_summary ("Exercise glnx_close_fd"); + + /* Closing a non-fd is a no-op, and preserves errno. + * EILSEQ is an arbitrary valid value of errno that is unlikely + * to be set accidentally as a side-effect of I/O. */ + g_test_message ("Closing a non-fd is a no-op and preserves errno..."); + errno = EILSEQ; + glnx_close_fd (&fd); + errsv = errno; + g_assert_cmpint (fd, ==, -1); + g_assert_cmpint (errsv, ==, EILSEQ); + + /* Closing a valid fd really closes it, and preserves errno. */ + g_test_message ("Closing a valid fd preserves errno..."); + glnx_opendirat (AT_FDCWD, "/", TRUE, &fd, &error); + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + fd_borrowed = fd; + errno = EILSEQ; + glnx_close_fd (&fd); + errsv = errno; + g_assert_cmpint (fd, ==, -1); + g_assert_cmpint (errsv, ==, EILSEQ); + _glnx_test_assert_fd_was_closed (fd_borrowed); +} + +/* Exercise glnx_close_fd in the case where close() fails. + * The only convenient way we can arrange for this to happen is to use + * an invalid fd, which is a programming error. + * + * This function is only run under g_test_undefined(), and assumes the + * implementation detail that GLib responds to that programming error + * with a critical warning rather than a fatal error. */ +static void +test_close_ebadf_subprocess (void) +{ + g_autoptr(GError) error = NULL; + int errsv; + int fd = -2; + int non_fd; + + /* Preparation: Open a fd, and close it, leaving non_fd set to the + * file descriptor number. */ + glnx_opendirat (AT_FDCWD, "/", TRUE, &fd, &error); + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + close (fd); + non_fd = fd; + _glnx_test_assert_fd_was_closed (non_fd); + + /* "Closing" the non-fd provokes a critical warning. */ + g_log_set_always_fatal (G_LOG_FATAL_MASK); + g_log_set_fatal_mask ("GLib", G_LOG_FATAL_MASK); + + errno = EILSEQ; + glnx_close_fd (&non_fd); + errsv = errno; + g_assert_cmpint (non_fd, ==, -1); + + /* We preserved errno. */ + g_assert_cmpint (errsv, ==, EILSEQ); + g_print ("Closing invalid fd preserved errno\n"); +} + +static void +test_close_ebadf (void) +{ + g_test_summary ("Exercise glnx_close_fd when close() fails"); + + /* If close() fails, it still preserves errno. + * The only convenient way to make close() fail on-demand is EBADF. */ + + if (g_test_subprocess ()) + { + test_close_ebadf_subprocess (); + return; + } + + if (g_test_undefined ()) + { + g_test_message ("Closing invalid fd preserves errno..."); + +#if GLIB_CHECK_VERSION (2, 38, 0) + g_test_trap_subprocess (NULL, 0, G_TEST_SUBPROCESS_DEFAULT); +#else + if (g_test_trap_fork (0, 0)) + { + test_close_ebadf_subprocess (); + _exit (0); + } +#endif + + g_test_trap_assert_passed (); + g_test_trap_assert_stdout ("*Closing invalid fd preserved errno*"); +#if !GLIB_CHECK_VERSION(2, 76, 0) + /* We can assert that our backport emits this message */ + g_test_trap_assert_stderr ("*_glnx_close(fd:*) failed with EBADF*"); +#else + /* We can't assert anything this specific about GLib's, + * but as an implementation detail, it's currently very similar */ +# if 0 + g_test_trap_assert_stderr ("*g_close(fd:*) failed with EBADF*"); +# endif +#endif + } + else + { + g_test_skip ("Can't test this without provoking undefined behaviour"); + } +} + static gboolean renameat_test_setup (int *out_srcfd, int *out_destfd, GError **error) @@ -445,6 +564,8 @@ int main (int argc, char **argv) g_test_init (&argc, &argv, NULL); + g_test_add_func ("/close", test_close); + g_test_add_func ("/close/ebadf", test_close_ebadf); g_test_add_func ("/tmpfile", test_tmpfile); g_test_add_func ("/stdio-file", test_stdio_file); g_test_add_func ("/filecopy", test_filecopy);