From c67a8186660b9b09b74c3e8b01792cf82d05a257 Mon Sep 17 00:00:00 2001 From: Vyacheslav Titov Date: Mon, 19 Oct 2026 17:37:37 +0400 Subject: [PATCH] network: Accept loopback that is already configured Some runtimes bring up lo with 127.0.0.1/8 in newly created network namespaces. Verify that state before issuing exclusive address creation, which otherwise fails with EEXIST under gVisor. Keep the original setup and failure handling for incomplete state or query errors. Add a TAP regression covering the verified fast path, failed queries, incomplete loopback state and descriptor cleanup. Fixes: https://github.com/containers/bubblewrap/issues/745 Signed-off-by: Vyacheslav Titov --- network.c | 30 ++++++++ tests/meson.build | 11 +++ tests/test-network.c | 172 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 tests/test-network.c diff --git a/network.c b/network.c index 373d606a..047afb2f 100644 --- a/network.c +++ b/network.c @@ -25,6 +25,7 @@ #include #include #include +#include #include "utils.h" #include "network.h" @@ -133,6 +134,31 @@ rtnl_setup_request (char *buffer, return header; } +static bool +loopback_is_configured (void) +{ + cleanup_fd int fd = socket (AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + struct ifreq flags = { .ifr_name = "lo" }; + struct ifreq address_query = { .ifr_name = "lo", .ifr_addr.sa_family = AF_INET }; + struct ifreq mask_query = { .ifr_name = "lo", .ifr_netmask.sa_family = AF_INET }; + struct sockaddr_in *address; + + if (fd < 0 || ioctl (fd, SIOCGIFFLAGS, &flags) != 0 || + (flags.ifr_flags & (IFF_UP | IFF_LOOPBACK)) != (IFF_UP | IFF_LOOPBACK)) + return false; + + if (ioctl (fd, SIOCGIFADDR, &address_query) != 0 || address_query.ifr_addr.sa_family != AF_INET) + return false; + address = (struct sockaddr_in *) &address_query.ifr_addr; + if (address->sin_addr.s_addr != htonl (INADDR_LOOPBACK)) + return false; + + if (ioctl (fd, SIOCGIFNETMASK, &mask_query) != 0 || mask_query.ifr_netmask.sa_family != AF_INET) + return false; + address = (struct sockaddr_in *) &mask_query.ifr_netmask; + return address->sin_addr.s_addr == htonl (0xff000000); +} + void loopback_setup (void) { @@ -145,6 +171,10 @@ loopback_setup (void) struct ifinfomsg *infomsg; struct in_addr *ip_addr; + /* Some runtimes, such as gVisor, bring up lo in a fresh network namespace. */ + if (loopback_is_configured ()) + return; + src_addr.nl_pid = getpid (); if_loopback = (int) if_nametoindex ("lo"); diff --git a/tests/meson.build b/tests/meson.build index fe45af0a..e776ec84 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,4 +1,15 @@ test_programs = [ + ['test-network', executable( + 'test-network', + 'test-network.c', + '../network.c', + include_directories : common_include_directories, + link_args : [ + '-Wl,--wrap=socket', + '-Wl,--wrap=ioctl', + '-Wl,--wrap=if_nametoindex', + ], + )], ['test-utils', executable( 'test-utils', 'test-utils.c', diff --git a/tests/test-network.c b/tests/test-network.c new file mode 100644 index 00000000..9018d797 --- /dev/null +++ b/tests/test-network.c @@ -0,0 +1,172 @@ +/* SPDX-License-Identifier: LGPL-2.0-or-later */ + +#include +#include +#include +#include +#include +#include + +#include "network.h" +#include "utils.h" + +enum condition +{ + READY, + SOCKET_ERROR, + FLAGS_ERROR, + DOWN, + NOT_LOOPBACK, + ADDRESS_ERROR, + WRONG_ADDRESS_FAMILY, + WRONG_ADDRESS, + MISSING_ADDRESS, + MASK_ERROR, + WRONG_MASK_FAMILY, + WRONG_MASK, + MISSING_MASK +}; + +static const char * const case_names[] = { + "already configured", + "socket error", + "flags query error", + "interface down", + "not a loopback interface", + "address query error", + "wrong address family", + "wrong address", + "missing address", + "netmask query error", + "wrong netmask family", + "wrong netmask", + "missing netmask" +}; + +static enum condition current; +static int control = -1; + +int __wrap_socket (int domain, int type, int protocol); +int __wrap_ioctl (int fd, unsigned long operation, ...); +unsigned int __wrap_if_nametoindex (const char *name); + +static bool +closed (void) +{ + return control == -1 || (fcntl (control, F_GETFD) == -1 && errno == EBADF); +} + +int +__wrap_socket (int domain, int type, int protocol) +{ + (void) protocol; + /* Stop at the original setup path: errors and mismatches must reach it. */ + if (domain == AF_NETLINK) + _exit (current != READY && closed () ? 0 : 1); + if (domain != AF_INET || type != (SOCK_DGRAM | SOCK_CLOEXEC)) + _exit (2); + if (current == SOCKET_ERROR) + { + errno = EMFILE; + return -1; + } + control = open ("/dev/null", O_RDONLY | O_CLOEXEC); + if (control < 0) + _exit (3); + return control; +} + +static int +address_query (struct ifreq *query, bool mask) +{ + struct sockaddr_in *address = (struct sockaddr_in *) &query->ifr_addr; + if (address->sin_family != AF_INET || address->sin_addr.s_addr != 0) + _exit (4); + if (current == (mask ? MASK_ERROR : ADDRESS_ERROR)) + { + errno = EIO; + return -1; + } + if (current == (mask ? MISSING_MASK : MISSING_ADDRESS)) + return 0; + if (current == (mask ? WRONG_MASK_FAMILY : WRONG_ADDRESS_FAMILY)) + address->sin_family = AF_INET6; + /* gVisor can write only the address, preserving the input family. */ + address->sin_addr.s_addr = htonl (mask ? 0xff000000 : INADDR_LOOPBACK); + if (current == (mask ? WRONG_MASK : WRONG_ADDRESS)) + address->sin_addr.s_addr = htonl (mask ? 0xffffff00 : 0x7f000002); + return 0; +} + +int +__wrap_ioctl (int fd, unsigned long operation, ...) +{ + va_list arguments; + struct ifreq *query; + if (fd != control) + _exit (5); + va_start (arguments, operation); + query = va_arg (arguments, struct ifreq *); + va_end (arguments); + if (strcmp (query->ifr_name, "lo") != 0) + _exit (6); + if (operation == SIOCGIFADDR || operation == SIOCGIFNETMASK) + return address_query (query, operation == SIOCGIFNETMASK); + if (operation != SIOCGIFFLAGS) + _exit (7); + if (current == FLAGS_ERROR) + { + errno = EIO; + return -1; + } + query->ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; + if (current == DOWN) + query->ifr_flags &= ~IFF_UP; + if (current == NOT_LOOPBACK) + query->ifr_flags &= ~IFF_LOOPBACK; + return 0; +} + +unsigned int +__wrap_if_nametoindex (const char *name) +{ + return strcmp (name, "lo") == 0 ? 1 : 0; +} + +void +die_with_error (const char *format, ...) +{ + (void) format; + _exit (8); +} + +int +main (void) +{ + puts ("1..13"); + fflush (stdout); + + for (current = READY; current <= MISSING_MASK; current++) + { + pid_t child = fork (); + int status; + if (child == -1) + { + perror ("fork"); + return 9; + } + if (child == 0) + { + loopback_setup (); + _exit (current == READY && closed () ? 0 : 10); + } + if (waitpid (child, &status, 0) != child || !WIFEXITED (status) || WEXITSTATUS (status) != 0) + { + printf ("not ok %u - %s\n", (unsigned int) current + 1, case_names[current]); + return 11; + } + printf ("ok %u - %s; query descriptor closed\n", (unsigned int) current + 1, case_names[current]); + fflush (stdout); + } + return 0; +}