diff --git a/gio/tests/socket-listener.c b/gio/tests/socket-listener.c
index 82b3e92ff..c879c8b2e 100644
--- a/gio/tests/socket-listener.c
+++ b/gio/tests/socket-listener.c
@@ -1,6 +1,7 @@
/* GLib testing framework examples and tests
*
* Copyright 2014 Red Hat, Inc.
+ * Copyright 2025 GNOME Foundation, Inc.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
@@ -19,8 +20,100 @@
* .
*/
+#include "config.h"
+
#include
+#ifdef HAVE_RTLD_NEXT
+#include
+#include
+#include
+#include
+#include
+#include
+#endif
+
+/* Override the socket(), bind(), listen() and getsockopt() functions from libc
+ * so that we can mock results from them in the tests. The libc implementations
+ * are used by default (via `dlsym()`) unless a test sets a callback
+ * deliberately.
+ *
+ * These override functions are used simply because the linker will resolve them
+ * before it finds the symbols in libc. This is effectively like `LD_PRELOAD`,
+ * except without using an external library for them.
+ *
+ * This mechanism is intended to be generic and not to force tests in this file
+ * to be written in a certain way. Tests are free to override these functions
+ * with their own implementations, or leave them as default. Different tests may
+ * need to mock these socket functions differently.
+ *
+ * If a test overrides these functions, it *must* do so at the start of the test
+ * (before starting any threads), and *must* clear them to `NULL` at the end of
+ * the test. The overrides are not thread-safe and will not be automatically
+ * cleared at the end of a test.
+ */
+#ifdef HAVE_RTLD_NEXT
+#define MOCK_SOCKET_SUPPORTED
+#endif
+
+#ifdef MOCK_SOCKET_SUPPORTED
+static int (*real_socket) (int, int, int);
+static int (*real_bind) (int, const struct sockaddr *, socklen_t);
+static int (*real_listen) (int, int);
+static int (*real_getsockopt) (int, int, int, void *, socklen_t *);
+static int (*mock_socket) (int, int, int);
+static int (*mock_bind) (int, const struct sockaddr *, socklen_t);
+static int (*mock_listen) (int, int);
+static int (*mock_getsockopt) (int, int, int, void *, socklen_t *);
+
+int
+socket (int domain,
+ int type,
+ int protocol)
+{
+ if (real_socket == NULL)
+ real_socket = dlsym (RTLD_NEXT, "socket");
+
+ return ((mock_socket != NULL) ? mock_socket : real_socket) (domain, type, protocol);
+}
+
+int
+bind (int sockfd,
+ const struct sockaddr *addr,
+ socklen_t addrlen)
+{
+ if (real_bind == NULL)
+ real_bind = dlsym (RTLD_NEXT, "bind");
+
+ return ((mock_bind != NULL) ? mock_bind : real_bind) (sockfd, addr, addrlen);
+}
+
+int
+listen (int sockfd,
+ int backlog)
+{
+ if (real_listen == NULL)
+ real_listen = dlsym (RTLD_NEXT, "listen");
+
+ return ((mock_listen != NULL) ? mock_listen : real_listen) (sockfd, backlog);
+}
+
+int
+getsockopt (int sockfd,
+ int level,
+ int optname,
+ void *optval,
+ socklen_t *optlen)
+{
+ if (real_getsockopt == NULL)
+ real_getsockopt = dlsym (RTLD_NEXT, "getsockopt");
+
+ return ((mock_getsockopt != NULL) ? mock_getsockopt : real_getsockopt) (sockfd, level, optname, optval, optlen);
+}
+
+#endif /* MOCK_SOCKET_SUPPORTED */
+
+/* Test event signals. */
static void
event_cb (GSocketListener *listener,
GSocketListenerEvent event,
@@ -82,6 +175,374 @@ test_event_signal (void)
g_object_unref (listener);
}
+/* Provide a mock implementation of socket(), listen(), bind() and getsockopt()
+ * which use a simple fixed configuration to either force a call to fail with a
+ * given errno, or allow it to pass through to the system implementation (which
+ * is assumed to succeed). Results are differentiated by protocol (IPv4 or IPv6)
+ * but nothing more complex than that.
+ *
+ * This allows the `listen()` fallback code in
+ * `g_socket_listener_add_any_inet_port()` and
+ * `g_socket_listener_add_inet_port()` to be tested for different situations
+ * where IPv4 and/or IPv6 sockets don’t work. It doesn’t allow the port
+ * allocation retry logic to be tested (as it forces all IPv6 `bind()` calls to
+ * have the same result), but this means it doesn’t have to do more complex
+ * state tracking of fully mocked-up sockets.
+ *
+ * It also means that the test won’t work on systems which don’t support IPv6,
+ * or which have a configuration which causes the first test case (which passes
+ * all syscalls through to the system) to fail. On those systems, the test
+ * should be skipped rather than the mock made more complex.
+ */
+#ifdef MOCK_SOCKET_SUPPORTED
+
+typedef struct {
+ gboolean ipv6_socket_supports_ipv4;
+ int ipv4_socket_errno; /* 0 for socket() to succeed on the IPv4 socket (i.e. IPv4 sockets are supported) */
+ int ipv6_socket_errno; /* similarly */
+ int ipv4_bind_errno; /* 0 for bind() to succeed on the IPv4 socket */
+ int ipv6_bind_errno; /* similarly */
+ int ipv4_listen_errno; /* 0 for listen() to succeed on the IPv4 socket */
+ int ipv6_listen_errno; /* similarly */
+} ListenFailuresConfig;
+
+static struct {
+ /* Input: */
+ ListenFailuresConfig config;
+
+ /* State (we only support tracking one socket of each type): */
+ int ipv4_socket_fd;
+ int ipv6_socket_fd;
+} listen_failures_mock_state;
+
+static int
+listen_failures_socket (int domain,
+ int type,
+ int protocol)
+{
+ int fd;
+
+ /* Error out if told to */
+ if (domain == AF_INET && listen_failures_mock_state.config.ipv4_socket_errno != 0)
+ {
+ errno = listen_failures_mock_state.config.ipv4_socket_errno;
+ return -1;
+ }
+ else if (domain == AF_INET6 && listen_failures_mock_state.config.ipv6_socket_errno != 0)
+ {
+ errno = listen_failures_mock_state.config.ipv6_socket_errno;
+ return -1;
+ }
+ else if (domain != AF_INET && domain != AF_INET6)
+ {
+ /* we don’t expect to support other socket types */
+ g_assert_not_reached ();
+ }
+
+ /* Pass through to the system, which we require to succeed because we’re only
+ * mocking errors and not the full socket stack state */
+ fd = real_socket (domain, type, protocol);
+ g_assert_no_errno (fd);
+
+ /* Track the returned FD for each socket type */
+ if (domain == AF_INET)
+ {
+ g_assert (listen_failures_mock_state.ipv4_socket_fd == 0);
+ listen_failures_mock_state.ipv4_socket_fd = fd;
+ }
+ else if (domain == AF_INET6)
+ {
+ g_assert (listen_failures_mock_state.ipv6_socket_fd == 0);
+ listen_failures_mock_state.ipv6_socket_fd = fd;
+ }
+
+ return fd;
+}
+
+static int
+listen_failures_bind (int sockfd,
+ const struct sockaddr *addr,
+ socklen_t addrlen)
+{
+ int retval;
+
+ /* Error out if told to */
+ if (listen_failures_mock_state.ipv4_socket_fd == sockfd &&
+ listen_failures_mock_state.config.ipv4_bind_errno != 0)
+ {
+ errno = listen_failures_mock_state.config.ipv4_bind_errno;
+ return -1;
+ }
+ else if (listen_failures_mock_state.ipv6_socket_fd == sockfd &&
+ listen_failures_mock_state.config.ipv6_bind_errno != 0)
+ {
+ errno = listen_failures_mock_state.config.ipv6_bind_errno;
+ return -1;
+ }
+ else if (listen_failures_mock_state.ipv4_socket_fd != sockfd &&
+ listen_failures_mock_state.ipv6_socket_fd != sockfd)
+ {
+ g_assert_not_reached ();
+ }
+
+ /* Pass through to the system, which we require to succeed because we’re only
+ * mocking errors and not the full socket stack state */
+ retval = real_bind (sockfd, addr, addrlen);
+ g_assert_no_errno (retval);
+
+ return retval;
+}
+
+static int
+listen_failures_listen (int sockfd,
+ int backlog)
+{
+ int retval;
+
+ /* Error out if told to */
+ if (listen_failures_mock_state.ipv4_socket_fd == sockfd &&
+ listen_failures_mock_state.config.ipv4_listen_errno != 0)
+ {
+ errno = listen_failures_mock_state.config.ipv4_listen_errno;
+ return -1;
+ }
+ else if (listen_failures_mock_state.ipv6_socket_fd == sockfd &&
+ listen_failures_mock_state.config.ipv6_listen_errno != 0)
+ {
+ errno = listen_failures_mock_state.config.ipv6_listen_errno;
+ return -1;
+ }
+ else if (listen_failures_mock_state.ipv4_socket_fd != sockfd &&
+ listen_failures_mock_state.ipv6_socket_fd != sockfd)
+ {
+ g_assert_not_reached ();
+ }
+
+ /* Pass through to the system, which we require to succeed because we’re only
+ * mocking errors and not the full socket stack state */
+ retval = real_listen (sockfd, backlog);
+ g_assert_no_errno (retval);
+
+ return retval;
+}
+
+static int
+listen_failures_getsockopt (int sockfd,
+ int level,
+ int optname,
+ void *optval,
+ socklen_t *optlen)
+{
+ /* Mock whether IPv6 sockets claim to support IPv4 */
+#if defined (IPPROTO_IPV6) && defined (IPV6_V6ONLY)
+ if (listen_failures_mock_state.ipv6_socket_fd == sockfd &&
+ level == IPPROTO_IPV6 && optname == IPV6_V6ONLY)
+ {
+ int *v6_only = optval;
+ *v6_only = !listen_failures_mock_state.config.ipv6_socket_supports_ipv4;
+ return 0;
+ }
+#endif
+
+ /* Don’t assert that the system getsockopt() succeeded, as it could be used
+ * in complex ways, and it’s incidental to what we’re actually trying to test. */
+ return real_getsockopt (sockfd, level, optname, optval, optlen);
+}
+
+#endif /* MOCK_SOCKET_SUPPORTED */
+
+static void
+test_add_any_inet_port_listen_failures (void)
+{
+#ifdef MOCK_SOCKET_SUPPORTED
+ const struct
+ {
+ ListenFailuresConfig config;
+ GQuark expected_error_domain; /* 0 if success is expected */
+ int expected_error_code; /* 0 if success is expected */
+ }
+ test_matrix[] =
+ {
+ /* If everything works, it should all work: */
+ { { TRUE, 0, 0, 0, 0, 0, 0 }, 0, 0 },
+ /* If IPv4 sockets are not supported, IPv6 should be used: */
+ { { TRUE, EAFNOSUPPORT, 0, 0, 0, 0, 0 }, 0, 0 },
+ /* If IPv6 sockets are not supported, IPv4 should be used: */
+ { { TRUE, 0, EAFNOSUPPORT, 0, 0, 0, 0, }, 0, 0 },
+ /* If no sockets are supported, everything should fail: */
+ { { TRUE, EAFNOSUPPORT, EAFNOSUPPORT, 0, 0, 0, 0 },
+ G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED },
+ /* If binding IPv4 fails, IPv6 should be used: */
+ { { TRUE, 0, 0, EADDRINUSE, 0, 0, 0 }, 0, 0 },
+ /* If binding IPv6 fails, fail overall (the algorithm is not symmetric): */
+ { { TRUE, 0, 0, 0, EADDRINUSE, 0, 0 },
+ G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
+ /* If binding them both fails, fail overall: */
+ { { TRUE, 0, 0, EADDRINUSE, EADDRINUSE, 0, 0 },
+ G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
+ /* If listening on IPv4 fails, IPv6 should be used: */
+ { { TRUE, 0, 0, 0, 0, EADDRINUSE, 0 }, 0, 0 },
+ /* If listening on IPv6 fails, IPv4 should be used:
+ * FIXME: If the IPv6 socket claims to support IPv4, this currently won’t
+ * retry with an IPv4-only socket; see https://gitlab.gnome.org/GNOME/glib/-/issues/3604 */
+ { { TRUE, 0, 0, 0, 0, 0, EADDRINUSE },
+ G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
+ /* If listening on IPv6 fails (and the IPv6 socket doesn’t claim to
+ * support IPv4), IPv4 should be used: */
+ { { FALSE, 0, 0, 0, 0, 0, EADDRINUSE }, 0, 0 },
+ /* If listening on both fails, fail overall: */
+ { { TRUE, 0, 0, 0, 0, EADDRINUSE, EADDRINUSE },
+ G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
+ };
+
+ /* Override the socket(), bind(), listen() and getsockopt() functions */
+ mock_socket = listen_failures_socket;
+ mock_bind = listen_failures_bind;
+ mock_listen = listen_failures_listen;
+ mock_getsockopt = listen_failures_getsockopt;
+
+ g_test_summary ("Test that adding a listening port succeeds if either "
+ "listening on IPv4 or IPv6 succeeds");
+
+ for (size_t i = 0; i < G_N_ELEMENTS (test_matrix); i++)
+ {
+ GSocketService *service = NULL;
+ GError *local_error = NULL;
+ uint16_t port;
+
+ g_test_message ("Test %" G_GSIZE_FORMAT, i);
+
+ /* Configure the mock socket behaviour. */
+ memset (&listen_failures_mock_state, 0, sizeof (listen_failures_mock_state));
+ listen_failures_mock_state.config = test_matrix[i].config;
+
+ /* Create a GSocketService to test. */
+ service = g_socket_service_new ();
+ port = g_socket_listener_add_any_inet_port (G_SOCKET_LISTENER (service), NULL, &local_error);
+
+ if (test_matrix[i].expected_error_domain == 0)
+ {
+ g_assert_no_error (local_error);
+ g_assert_cmpuint (port, !=, 0);
+ }
+ else
+ {
+ g_assert_error (local_error, test_matrix[i].expected_error_domain,
+ test_matrix[i].expected_error_code);
+ g_assert_cmpuint (port, ==, 0);
+ }
+
+ g_clear_error (&local_error);
+ g_socket_listener_close (G_SOCKET_LISTENER (service));
+ g_clear_object (&service);
+ }
+
+ /* Tidy up. */
+ mock_socket = NULL;
+ mock_bind = NULL;
+ mock_listen = NULL;
+ mock_getsockopt = NULL;
+ memset (&listen_failures_mock_state, 0, sizeof (listen_failures_mock_state));
+#else /* if !MOCK_SOCKET_SUPPORTED */
+ g_test_skip ("Mock socket not supported");
+#endif
+}
+
+static void
+test_add_inet_port_listen_failures (void)
+{
+#ifdef MOCK_SOCKET_SUPPORTED
+ const struct
+ {
+ ListenFailuresConfig config;
+ GQuark expected_error_domain; /* 0 if success is expected */
+ int expected_error_code; /* 0 if success is expected */
+ }
+ test_matrix[] =
+ {
+ /* If everything works, it should all work: */
+ { { TRUE, 0, 0, 0, 0, 0, 0 }, 0, 0 },
+ /* If IPv4 sockets are not supported, IPv6 should be used: */
+ { { TRUE, EAFNOSUPPORT, 0, 0, 0, 0, 0 }, 0, 0 },
+ /* If IPv6 sockets are not supported, IPv4 should be used: */
+ { { TRUE, 0, EAFNOSUPPORT, 0, 0, 0, 0, }, 0, 0 },
+ /* If no sockets are supported, everything should fail: */
+ { { TRUE, EAFNOSUPPORT, EAFNOSUPPORT, 0, 0, 0, 0 },
+ G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED },
+ /* If binding IPv4 fails, IPv6 should be used: */
+ { { TRUE, 0, 0, EADDRINUSE, 0, 0, 0 }, 0, 0 },
+ /* If binding IPv6 fails, fail overall (the algorithm is not symmetric): */
+ { { TRUE, 0, 0, 0, EADDRINUSE, 0, 0 },
+ G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
+ /* If binding them both fails, fail overall: */
+ { { TRUE, 0, 0, EADDRINUSE, EADDRINUSE, 0, 0 },
+ G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
+ /* If listening on IPv4 fails, IPv6 should be used: */
+ { { TRUE, 0, 0, 0, 0, EADDRINUSE, 0 }, 0, 0 },
+ /* If listening on IPv6 fails, IPv4 should be used: */
+ { { TRUE, 0, 0, 0, 0, 0, EADDRINUSE }, 0, 0 },
+ /* If listening on IPv6 fails (and the IPv6 socket doesn’t claim to
+ * support IPv4), IPv4 should be used: */
+ { { FALSE, 0, 0, 0, 0, 0, EADDRINUSE }, 0, 0 },
+ /* If listening on both fails, fail overall: */
+ { { TRUE, 0, 0, 0, 0, EADDRINUSE, EADDRINUSE },
+ G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE },
+ };
+
+ /* Override the socket(), bind(), listen() and getsockopt() functions */
+ mock_socket = listen_failures_socket;
+ mock_bind = listen_failures_bind;
+ mock_listen = listen_failures_listen;
+ mock_getsockopt = listen_failures_getsockopt;
+
+ g_test_summary ("Test that adding a listening port succeeds if either "
+ "listening on IPv4 or IPv6 succeeds");
+
+ for (size_t i = 0; i < G_N_ELEMENTS (test_matrix); i++)
+ {
+ GSocketService *service = NULL;
+ GError *local_error = NULL;
+ gboolean retval;
+
+ g_test_message ("Test %" G_GSIZE_FORMAT, i);
+
+ /* Configure the mock socket behaviour. */
+ memset (&listen_failures_mock_state, 0, sizeof (listen_failures_mock_state));
+ listen_failures_mock_state.config = test_matrix[i].config;
+
+ /* Create a GSocketService to test. */
+ service = g_socket_service_new ();
+ retval = g_socket_listener_add_inet_port (G_SOCKET_LISTENER (service), 4321, NULL, &local_error);
+
+ if (test_matrix[i].expected_error_domain == 0)
+ {
+ g_assert_no_error (local_error);
+ g_assert_true (retval);
+ }
+ else
+ {
+ g_assert_error (local_error, test_matrix[i].expected_error_domain,
+ test_matrix[i].expected_error_code);
+ g_assert_false (retval);
+ }
+
+ g_clear_error (&local_error);
+
+ g_socket_listener_close (G_SOCKET_LISTENER (service));
+ g_clear_object (&service);
+ }
+
+ /* Tidy up. */
+ mock_socket = NULL;
+ mock_bind = NULL;
+ mock_listen = NULL;
+ mock_getsockopt = NULL;
+ memset (&listen_failures_mock_state, 0, sizeof (listen_failures_mock_state));
+#else /* if !MOCK_SOCKET_SUPPORTED */
+ g_test_skip ("Mock socket not supported");
+#endif
+}
+
int
main (int argc,
char *argv[])
@@ -89,6 +550,8 @@ main (int argc,
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/socket-listener/event-signal", test_event_signal);
+ g_test_add_func ("/socket-listener/add-any-inet-port/listen-failures", test_add_any_inet_port_listen_failures);
+ g_test_add_func ("/socket-listener/add-inet-port/listen-failures", test_add_inet_port_listen_failures);
return g_test_run();
}