From 3c5f514b3069d7a94310f2e67aa766a1f30f5b91 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Wed, 10 Sep 2025 11:51:30 +0200 Subject: [PATCH 01/14] Revert "swrap: Allow to pass 8 fds" This doesn't fit into PIPE_BUF :-( This reverts commit 89f9c3c62d17116a782728863d038d6c99d49ab4. Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- src/socket_wrapper.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c index 54eb239..bc4000c 100644 --- a/src/socket_wrapper.c +++ b/src/socket_wrapper.c @@ -5634,7 +5634,7 @@ static int swrap_sendmsg_filter_cmsg_sol_socket(const struct cmsghdr *cmsg, static const uint64_t swrap_unix_scm_right_magic = 0x8e0e13f27c42fc38; /* - * We only allow up to 8 fds at a time + * We only allow up to 6 fds at a time * as that's more than enough for Samba * and it means we can keep the logic simple * and work with fixed size arrays. @@ -5646,7 +5646,7 @@ static const uint64_t swrap_unix_scm_right_magic = 0x8e0e13f27c42fc38; #ifndef PIPE_BUF #define PIPE_BUF 4096 #endif -#define SWRAP_MAX_PASSED_FDS ((size_t)8) +#define SWRAP_MAX_PASSED_FDS ((size_t)6) #define SWRAP_MAX_PASSED_SOCKET_INFO SWRAP_MAX_PASSED_FDS struct swrap_unix_scm_rights_payload { uint8_t num_idxs; -- GitLab From b25e2eaf205e52ab65fadc52b03e30b3a3ae75db Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Mon, 24 Nov 2025 15:57:47 +0100 Subject: [PATCH 02/14] tests: Prepare test to compile with cmocka 2.0 Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- tests/CMakeLists.txt | 10 ++++++++++ tests/test_echo_tcp_get_peer_sock_name.c | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cc1b1ec..0ef0eb4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,6 +12,11 @@ if (TARGET cmocka::cmocka) set(CMOCKA_LIBRARY cmocka::cmocka) endif() +# Check for newer cmocka functions to avoid deprecated warnings +include(CheckLibraryExists) +set(CMAKE_REQUIRED_LIBRARIES ${CMOCKA_LIBRARY}) +check_library_exists("${CMOCKA_LIBRARY}" _assert_uint_in_range "" HAVE_ASSERT_UINT_IN_RANGE) + set(TORTURE_LIBRARY torture) # RFC862 echo server @@ -148,6 +153,11 @@ foreach(_SWRAP_TEST ${SWRAP_TESTS}) add_cmocka_test_environment(${_SWRAP_TEST}) endforeach() +# Add compile definition for test that needs to check for newer cmocka function +if (HAVE_ASSERT_UINT_IN_RANGE) + target_compile_definitions(test_echo_tcp_get_peer_sock_name PRIVATE HAVE_ASSERT_UINT_IN_RANGE) +endif() + if (HELGRIND_TESTING) find_program(VALGRIND_EXECUTABLE valgrind) if (VALGRIND_EXECUTABLE) diff --git a/tests/test_echo_tcp_get_peer_sock_name.c b/tests/test_echo_tcp_get_peer_sock_name.c index ec15371..47b9dd7 100644 --- a/tests/test_echo_tcp_get_peer_sock_name.c +++ b/tests/test_echo_tcp_get_peer_sock_name.c @@ -112,11 +112,15 @@ static void _assert_sockaddr_port_range_equal(struct torture_address *addr, return; } +#ifdef HAVE_ASSERT_UINT_IN_RANGE + _assert_uint_in_range(ntohs(n_port), min_port, max_port, file, line); +#else _assert_in_range(ntohs(n_port), min_port, max_port, file, line); +#endif } #define assert_sockaddr_port_range_equal(ss, a, min_prt, max_prt) \ -- GitLab From a79b0c0c34e033a0c20fd427eff75ffed11bd5bf Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Thu, 27 Nov 2025 10:41:55 +0100 Subject: [PATCH 03/14] tests: Removed unused includes from swrap_fake_uid_wrapper.c Fixes building on FreeBSD. Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- tests/swrap_fake_uid_wrapper.c | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/tests/swrap_fake_uid_wrapper.c b/tests/swrap_fake_uid_wrapper.c index fc9cf46..495572f 100644 --- a/tests/swrap_fake_uid_wrapper.c +++ b/tests/swrap_fake_uid_wrapper.c @@ -1,22 +1,7 @@ #include "config.h" -#include -#include -#include -#include - -#include -#include -#include -#include #include - -#ifdef HAVE_SYS_SYSCALL_H -#include -#endif -#ifdef HAVE_SYSCALL_H -#include -#endif +#include #include "swrap_fake_uid_wrapper.h" -- GitLab From 13e97cefa9450715fca33f5205fe8eb5014ff56d Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Tue, 25 Nov 2025 13:53:37 +0100 Subject: [PATCH 04/14] tests: Reduce runtime from 60 sec to 12 sec 200msec should be enough. Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- tests/test_echo_tcp_poll.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_echo_tcp_poll.c b/tests/test_echo_tcp_poll.c index a10ba3a..b2006c0 100644 --- a/tests/test_echo_tcp_poll.c +++ b/tests/test_echo_tcp_poll.c @@ -83,7 +83,7 @@ static void handle_poll_loop(size_t size, int s) assert_int_not_equal(ret, -1); nread += ret; /* try to delay */ - sleep(1); + usleep(200000); } if (pfds[j].revents & POLLOUT) { snprintf(send_buf, sizeof(send_buf), -- GitLab From 555d393f6e8003cc736afc5e2e6a1daa78f5b139 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Tue, 25 Nov 2025 11:17:22 +0100 Subject: [PATCH 05/14] tests: Add helper to calc the size for unix_scm_rights_payload array Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- tests/CMakeLists.txt | 18 +++++++ tests/calc_unix_scm_rights_payload.c | 75 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 tests/calc_unix_scm_rights_payload.c diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0ef0eb4..10e1d02 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -32,6 +32,24 @@ if (DEFINED DEFAULT_LINK_FLAGS) LINK_FLAGS ${DEFAULT_LINK_FLAGS}) endif() +# Helper tool to calculate optimal unix_scm_rights_payload configuration +add_executable(calc_unix_scm_rights_payload calc_unix_scm_rights_payload.c) +target_compile_options(calc_unix_scm_rights_payload + PRIVATE + ${DEFAULT_C_COMPILE_FLAGS} + -D_GNU_SOURCE) +target_include_directories(calc_unix_scm_rights_payload + PRIVATE + ${CMAKE_BINARY_DIR}) +target_link_libraries(calc_unix_scm_rights_payload + ${SWRAP_REQUIRED_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT}) +if (DEFINED DEFAULT_LINK_FLAGS) + set_target_properties(calc_unix_scm_rights_payload + PROPERTIES + LINK_FLAGS ${DEFAULT_LINK_FLAGS}) +endif() + add_library(${TORTURE_LIBRARY} STATIC torture.c) target_compile_options(${TORTURE_LIBRARY} PRIVATE diff --git a/tests/calc_unix_scm_rights_payload.c b/tests/calc_unix_scm_rights_payload.c new file mode 100644 index 0000000..443f622 --- /dev/null +++ b/tests/calc_unix_scm_rights_payload.c @@ -0,0 +1,75 @@ +/* + * Helper to calculate optimal SWRAP_MAX_PASSED_FDS and + * SWRAP_MAX_PASSED_SOCKET_INFO so that struct swrap_unix_scm_rights fits within + * PIPE_BUF + */ + +#include +#include +#include + +#include "../src/socket_wrapper.c" + +int main(void) +{ + size_t si_size = sizeof(struct socket_info); + size_t scm_rights_size = sizeof(struct swrap_unix_scm_rights); + size_t header_size; + size_t payload_base_size; + size_t available_space; + size_t max_fds; + size_t max_socket_infos; + size_t calculated_total_size; + + printf("Current configuration:\n"); + printf(" SWRAP_MAX_PASSED_FDS = %zu\n", SWRAP_MAX_PASSED_FDS); + printf(" SWRAP_MAX_PASSED_SOCKET_INFO = %zu\n", + SWRAP_MAX_PASSED_SOCKET_INFO); + printf(" sizeof(struct socket_info) = %zu bytes\n", si_size); + printf(" sizeof(struct swrap_unix_scm_rights) = %zu bytes\n", + scm_rights_size); + printf("\n"); + + /* Calculate header size and payload base size using offsetof */ + header_size = offsetof(struct swrap_unix_scm_rights, payload); + payload_base_size = offsetof(struct swrap_unix_scm_rights_payload, idxs); + + available_space = PIPE_BUF - header_size - payload_base_size; + + printf("PIPE_BUF = %d bytes\n", PIPE_BUF); + printf("Header size (offsetof payload) = %zu bytes\n", header_size); + printf("Payload base size (offsetof idxs) = %zu bytes\n", + payload_base_size); + printf("Available space for arrays = %zu bytes\n", available_space); + printf("\n"); + + /* Calculate maximum FDs and socket_infos that fit */ + /* Formula: available_space = (max_fds * sizeof(int8_t)) + + * (max_socket_infos * sizeof(socket_info)) */ + /* Assuming max_fds == max_socket_infos: available_space = max * (1 + + * sizeof(socket_info)) */ + max_fds = available_space / (sizeof(int8_t) + si_size); + max_socket_infos = max_fds; + + printf("Calculated maximum values:\n"); + printf(" SWRAP_MAX_PASSED_FDS = %zu\n", max_fds); + printf(" SWRAP_MAX_PASSED_SOCKET_INFO = %zu\n", max_socket_infos); + printf("\n"); + + /* Verify the calculation */ + calculated_total_size = header_size + payload_base_size + + (max_fds * sizeof(int8_t)) + + (max_socket_infos * si_size); + printf("Verification:\n"); + printf(" Calculated total size (struct swrap_unix_scm_rights) = %zu " + "bytes\n", + calculated_total_size); + printf(" Space used = %.1f%% of PIPE_BUF\n", + (calculated_total_size * 100.0) / PIPE_BUF); + printf(" Space remaining = %zu bytes\n", + PIPE_BUF - calculated_total_size); + printf(" Fits in PIPE_BUF: %s\n", + calculated_total_size <= PIPE_BUF ? "YES" : "NO"); + + return 0; +} -- GitLab From 2d4a3115607f76dac89765c6e676247892d285eb Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Wed, 10 Sep 2025 11:16:54 +0200 Subject: [PATCH 06/14] swrap: Use uint16_t for the family Before: sizeof(struct socket_info) = 600 bytes After: sizeof(struct socket_info) = 600 bytes Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- src/socket_wrapper.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c index bc4000c..e8a3488 100644 --- a/src/socket_wrapper.c +++ b/src/socket_wrapper.c @@ -323,7 +323,7 @@ struct socket_info * on any change. */ - int family; + uint16_t family; int type_flags; /* SOCK_CLOEXEC or SOCK_NONBLOCK */ int type; int protocol; -- GitLab From 41fece649cf3b73b062ee78a6dd429e853d1abbf Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Wed, 10 Sep 2025 11:31:03 +0200 Subject: [PATCH 07/14] swrap: Use uint16_t for pktinfo Before: sizeof(struct socket_info) = 600 bytes After: sizeof(struct socket_info) = 596 bytes Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- src/socket_wrapper.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c index e8a3488..1d03490 100644 --- a/src/socket_wrapper.c +++ b/src/socket_wrapper.c @@ -324,6 +324,7 @@ struct socket_info */ uint16_t family; + uint16_t pktinfo; int type_flags; /* SOCK_CLOEXEC or SOCK_NONBLOCK */ int type; int protocol; @@ -334,7 +335,6 @@ struct socket_info int is_server; int connected; int defer_connect; - int pktinfo; int tcp_nodelay; int listening; int fd_passed; -- GitLab From 28d77017e9b474ab2c19915c965afe8a10de3af9 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Tue, 25 Nov 2025 10:14:51 +0100 Subject: [PATCH 08/14] swrap: Use bitfields for boolean members in `struct socket_info` Before: sizeof(struct socket_info) = 596 bytes After: sizeof(struct socket_info) = 568 bytes Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- src/socket_wrapper.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c index 1d03490..2d8b0c1 100644 --- a/src/socket_wrapper.c +++ b/src/socket_wrapper.c @@ -330,15 +330,17 @@ struct socket_info int protocol; int opt_type; int opt_protocol; - int bound; - int bcast; - int is_server; - int connected; - int defer_connect; - int tcp_nodelay; - int listening; int fd_passed; + /* Use bitfields for boolean types */ + unsigned int bound:1; + unsigned int bcast:1; + unsigned int is_server:1; + unsigned int connected:1; + unsigned int defer_connect:1; + unsigned int tcp_nodelay:1; + unsigned int listening:1; + /* The unix path so we can unlink it on close() */ struct sockaddr_un un_addr; @@ -4446,6 +4448,7 @@ static int swrap_bind(int s, const struct sockaddr *myaddr, socklen_t addrlen) struct swrap_sockaddr_buf buf = {}; int ret_errno = errno; int bind_error = 0; + int bcast = 0; #if 0 /* FIXME */ bool in_use; #endif @@ -4524,12 +4527,14 @@ static int swrap_bind(int s, const struct sockaddr *myaddr, socklen_t addrlen) addrlen, &un_addr.sa.un, 1, - &si->bcast); + &bcast); if (ret == -1) { ret_errno = errno; goto out; } + si->bcast = bcast; + unlink(un_addr.sa.un.sun_path); ret = libc_bind(s, &un_addr.sa.s, un_addr.sa_socklen); -- GitLab From d23916ecd7f81ab47b9818a3d4d42f687afbc1d3 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Tue, 25 Nov 2025 13:37:02 +0100 Subject: [PATCH 09/14] swrap: Add `struct swrap_address_ip_only` and helpers Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- src/socket_wrapper.c | 150 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c index 2d8b0c1..b539ba3 100644 --- a/src/socket_wrapper.c +++ b/src/socket_wrapper.c @@ -314,6 +314,16 @@ struct swrap_address { } sa; }; +struct swrap_address_ip_only { + union { + struct sockaddr s; + struct sockaddr_in in; +#ifdef HAVE_IPV6 + struct sockaddr_in6 in6; +#endif + } sa; +}; + static int first_free; struct socket_info @@ -493,6 +503,146 @@ static void swrap_log(enum swrap_dbglvl_e dbglvl, buffer); } +/********************************************************* + * HELPER FUNCTIONS + **********************************************************/ + +/* + * Get the socklen for an address family + */ +static inline socklen_t swrap_address_ip_only_socklen(sa_family_t family) +{ + switch (family) { + case AF_UNSPEC: /* AF_UNSPEC indicates uninitialized/empty address */ + return 0; + case AF_INET: + return sizeof(struct sockaddr_in); +#ifdef HAVE_IPV6 + case AF_INET6: + return sizeof(struct sockaddr_in6); +#endif + default: + SWRAP_LOG(SWRAP_LOG_ERROR, + "BUG: unexpected address family %d", + family); + abort(); + } + + return 0; /* Never reached */ +} + +/* + * Get the socklen from a swrap_address_ip_only struct + */ +static inline socklen_t swrap_address_ip_only_len( + const struct swrap_address_ip_only *addr) +{ + return swrap_address_ip_only_socklen(addr->sa.s.sa_family); +} + +/* + * Copy a sockaddr to swrap_address_ip_only, using the actual address size + * based on the address family. + */ +static inline void swrap_address_ip_only_from_sockaddr( + struct swrap_address_ip_only *dest, + const struct sockaddr *src, + socklen_t addrlen) +{ + socklen_t copy_len = swrap_address_ip_only_socklen(src->sa_family); + + /* AF_UNSPEC means empty/unbind - zero the entire struct */ + if (copy_len == 0) { + memset(&dest->sa, 0, sizeof(dest->sa)); + return; + } + + if (addrlen < copy_len) { + copy_len = addrlen; + } + + memcpy(&dest->sa.s, src, copy_len); + + /* Zero out any remaining bytes in the union */ + if (copy_len < sizeof(dest->sa)) { + memset((char *)&dest->sa.s + copy_len, + 0, + sizeof(dest->sa) - copy_len); + } +} + +/* + * Copy from swrap_address (with larger union) to swrap_address_ip_only. + * Only allows AF_INET and AF_INET6 addresses. + */ +static inline void swrap_address_ip_only_from_swrap_address( + struct swrap_address_ip_only *dest, + const struct swrap_address *src) +{ + const struct sockaddr *sa = &src->sa.s; + size_t expected_len; + + /* Validate address family and size */ + switch (sa->sa_family) { + case AF_INET: + expected_len = sizeof(struct sockaddr_in); + if (src->sa_socklen > expected_len) { + SWRAP_LOG(SWRAP_LOG_ERROR, + "BUG: IPv4 address size=%u > " + "sizeof(sockaddr_in)=%zu", + src->sa_socklen, + expected_len); + abort(); + } + break; +#ifdef HAVE_IPV6 + case AF_INET6: + expected_len = sizeof(struct sockaddr_in6); + if (src->sa_socklen > expected_len) { + SWRAP_LOG(SWRAP_LOG_ERROR, + "BUG: IPv6 address size=%u > " + "sizeof(sockaddr_in6)=%zu", + src->sa_socklen, + expected_len); + abort(); + } + break; +#endif + default: + SWRAP_LOG(SWRAP_LOG_ERROR, + "BUG: unexpected address family %d (expected AF_INET " + "or AF_INET6)", + sa->sa_family); + abort(); + } + + memcpy(&dest->sa.s, &src->sa.s, src->sa_socklen); + + /* Zero out any remaining bytes in the union */ + if (src->sa_socklen < sizeof(dest->sa)) { + memset((char *)&dest->sa.s + src->sa_socklen, + 0, + sizeof(dest->sa) - src->sa_socklen); + } +} + +/* Copy between two swrap_address_ip_only structs */ +static inline void swrap_address_ip_only_copy( + struct swrap_address_ip_only *dest, + const struct swrap_address_ip_only *src) +{ + socklen_t copy_len = swrap_address_ip_only_socklen(src->sa.s.sa_family); + + memcpy(&dest->sa.s, &src->sa.s, copy_len); + + /* Zero out any remaining bytes in the union */ + if (copy_len < sizeof(dest->sa)) { + memset((char *)&dest->sa.s + copy_len, + 0, + sizeof(dest->sa) - copy_len); + } +} + /********************************************************* * SWRAP LOADING LIBC FUNCTIONS *********************************************************/ -- GitLab From bb61feedc10205119c31fea9ca0247a9b27be2b2 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Tue, 25 Nov 2025 13:37:34 +0100 Subject: [PATCH 10/14] swrap: Use `struct swrap_address_ip_only` in `struct socket_info` Before: sizeof(struct socket_info) = 568 bytes After: sizeof(struct socket_info) = 240 bytes Current configuration: SWRAP_MAX_PASSED_FDS = 6 SWRAP_MAX_PASSED_SOCKET_INFO = 6 sizeof(struct socket_info) = 240 bytes sizeof(struct swrap_unix_scm_rights) = 1488 bytes PIPE_BUF = 4096 bytes Header size (offsetof payload) = 40 bytes Payload base size (offsetof idxs) = 1 bytes Available space for arrays = 4055 bytes Calculated maximum values: SWRAP_MAX_PASSED_FDS = 16 SWRAP_MAX_PASSED_SOCKET_INFO = 16 Verification: Calculated total size (struct swrap_unix_scm_rights) = 3897 bytes Space used = 95.1% of PIPE_BUF Space remaining = 199 bytes Fits in PIPE_BUF: YES Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- src/socket_wrapper.c | 116 ++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 63 deletions(-) diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c index b539ba3..4409f61 100644 --- a/src/socket_wrapper.c +++ b/src/socket_wrapper.c @@ -354,9 +354,9 @@ struct socket_info /* The unix path so we can unlink it on close() */ struct sockaddr_un un_addr; - struct swrap_address bindname; - struct swrap_address myname; - struct swrap_address peername; + struct swrap_address_ip_only bindname; + struct swrap_address_ip_only myname; + struct swrap_address_ip_only peername; struct { unsigned long pck_snd; @@ -1822,7 +1822,7 @@ static const struct in6_addr *swrap_ipv6(void) } #endif -static void set_port(int family, int prt, struct swrap_address *addr) +static void set_port(int family, int prt, struct swrap_address_ip_only *addr) { switch (family) { case AF_INET: @@ -2686,15 +2686,15 @@ static int convert_in_un_alloc(struct socket_info *si, const struct sockaddr *in } /* Store the bind address for connect() */ - if (si->bindname.sa_socklen == 0) { + if (si->bindname.sa.s.sa_family == AF_UNSPEC) { struct sockaddr_in bind_in; socklen_t blen = sizeof(struct sockaddr_in); ZERO_STRUCT(bind_in); bind_in.sin_family = in->sin_family; bind_in.sin_port = in->sin_port; - bind_in.sin_addr.s_addr = htonl(swrap_ipv4_iface(iface)); - si->bindname.sa_socklen = blen; + bind_in.sin_addr.s_addr = htonl( + swrap_ipv4_iface(iface)); memcpy(&si->bindname.sa.in, &bind_in, blen); } @@ -2736,7 +2736,7 @@ static int convert_in_un_alloc(struct socket_info *si, const struct sockaddr *in } /* Store the bind address for connect() */ - if (si->bindname.sa_socklen == 0) { + if (si->bindname.sa.s.sa_family == AF_UNSPEC) { struct sockaddr_in6 bind_in; socklen_t blen = sizeof(struct sockaddr_in6); @@ -2748,7 +2748,6 @@ static int convert_in_un_alloc(struct socket_info *si, const struct sockaddr *in bind_in.sin6_addr.s6_addr[15] = iface; memcpy(&si->bindname.sa.in6, &bind_in, blen); - si->bindname.sa_socklen = blen; } break; @@ -3972,8 +3971,7 @@ static int swrap_socket(int family, int type, int protocol, int allow_quic) .sin_family = AF_INET, }; - si->myname.sa_socklen = sizeof(struct sockaddr_in); - memcpy(&si->myname.sa.in, &sin, si->myname.sa_socklen); + memcpy(&si->myname.sa.in, &sin, sizeof(struct sockaddr_in)); break; } #ifdef HAVE_IPV6 @@ -3982,8 +3980,7 @@ static int swrap_socket(int family, int type, int protocol, int allow_quic) .sin6_family = AF_INET6, }; - si->myname.sa_socklen = sizeof(struct sockaddr_in6); - memcpy(&si->myname.sa.in6, &sin6, si->myname.sa_socklen); + memcpy(&si->myname.sa.in6, &sin6, sizeof(struct sockaddr_in6)); break; } #endif @@ -4239,10 +4236,7 @@ static int swrap_accept(int s, SWRAP_UNLOCK_SI(parent_si); - child_si->peername = (struct swrap_address) { - .sa_socklen = in_addr.sa_socklen, - }; - memcpy(&child_si->peername.sa.ss, &in_addr.sa.ss, in_addr.sa_socklen); + swrap_address_ip_only_from_swrap_address(&child_si->peername, &in_addr); if (addr != NULL && addrlen != NULL) { size_t copy_len = MIN(*addrlen, in_addr.sa_socklen); @@ -4269,10 +4263,8 @@ static int swrap_accept(int s, "accept() path=%s, fd=%d", un_my_addr.sa.un.sun_path, s); - child_si->myname = (struct swrap_address) { - .sa_socklen = in_my_addr.sa_socklen, - }; - memcpy(&child_si->myname.sa.ss, &in_my_addr.sa.ss, in_my_addr.sa_socklen); + swrap_address_ip_only_from_swrap_address(&child_si->myname, + &in_my_addr); idx = swrap_create_socket(&new_si, fd); if (idx == -1) { @@ -4364,10 +4356,7 @@ static int swrap_auto_bind(int fd, struct socket_info *si, int family) in.sin_addr.s_addr = htonl(swrap_ipv4_iface( socket_wrapper_default_iface())); - si->myname = (struct swrap_address) { - .sa_socklen = sizeof(in), - }; - memcpy(&si->myname.sa.in, &in, si->myname.sa_socklen); + memcpy(&si->myname.sa.in, &in, sizeof(in)); break; } #ifdef HAVE_IPV6 @@ -4398,10 +4387,7 @@ static int swrap_auto_bind(int fd, struct socket_info *si, int family) in6.sin6_addr = *swrap_ipv6(); in6.sin6_addr.s6_addr[15] = socket_wrapper_default_iface(); - si->myname = (struct swrap_address) { - .sa_socklen = sizeof(in6), - }; - memcpy(&si->myname.sa.in6, &in6, si->myname.sa_socklen); + memcpy(&si->myname.sa.in6, &in6, sizeof(in6)); break; } #endif @@ -4539,11 +4525,9 @@ static int swrap_connect(int s, const struct sockaddr *serv_addr, } if (ret == 0) { - si->peername = (struct swrap_address) { - .sa_socklen = addrlen, - }; - - memcpy(&si->peername.sa.ss, serv_addr, addrlen); + swrap_address_ip_only_from_sockaddr(&si->peername, + serv_addr, + addrlen); si->connected = 1; /* @@ -4553,18 +4537,12 @@ static int swrap_connect(int s, const struct sockaddr *serv_addr, * but here we have to update the name so getsockname() * returns correct information. */ - if (si->bindname.sa_socklen > 0) { - si->myname = (struct swrap_address) { - .sa_socklen = si->bindname.sa_socklen, - }; - - memcpy(&si->myname.sa.ss, - &si->bindname.sa.ss, - si->bindname.sa_socklen); + if (swrap_address_ip_only_len(&si->bindname) > 0) { + swrap_address_ip_only_copy(&si->myname, &si->bindname); /* Cleanup bindname */ - si->bindname = (struct swrap_address) { - .sa_socklen = 0, + si->bindname = (struct swrap_address_ip_only){ + .sa.s.sa_family = AF_UNSPEC, }; } @@ -4669,8 +4647,7 @@ static int swrap_bind(int s, const struct sockaddr *myaddr, socklen_t addrlen) } #endif - si->myname.sa_socklen = addrlen; - memcpy(&si->myname.sa.ss, myaddr, addrlen); + swrap_address_ip_only_from_sockaddr(&si->myname, myaddr, addrlen); ret = sockaddr_convert_to_un(si, myaddr, @@ -5038,20 +5015,19 @@ static int swrap_getpeername(int s, struct sockaddr *name, socklen_t *addrlen) SWRAP_LOCK_SI(si); - if (si->peername.sa_socklen == 0) - { + if (swrap_address_ip_only_len(&si->peername) == 0) { errno = ENOTCONN; goto out; } - len = MIN(*addrlen, si->peername.sa_socklen); + len = MIN(*addrlen, swrap_address_ip_only_len(&si->peername)); if (len == 0) { ret = 0; goto out; } - memcpy(name, &si->peername.sa.ss, len); - *addrlen = si->peername.sa_socklen; + memcpy(name, &si->peername.sa.s, len); + *addrlen = swrap_address_ip_only_len(&si->peername); ret = 0; out: @@ -5085,14 +5061,14 @@ static int swrap_getsockname(int s, struct sockaddr *name, socklen_t *addrlen) SWRAP_LOCK_SI(si); - len = MIN(*addrlen, si->myname.sa_socklen); + len = MIN(*addrlen, swrap_address_ip_only_len(&si->myname)); if (len == 0) { ret = 0; goto out; } - memcpy(name, &si->myname.sa.ss, len); - *addrlen = si->myname.sa_socklen; + memcpy(name, &si->myname.sa.s, len); + *addrlen = swrap_address_ip_only_len(&si->myname); ret = 0; out: @@ -5561,10 +5537,14 @@ static int swrap_msghdr_add_pktinfo(struct socket_info *si, struct in_addr pkt; #endif - if (si->bindname.sa_socklen == sizeof(struct sockaddr_in)) { + if (swrap_address_ip_only_len(&si->bindname) == + sizeof(struct sockaddr_in)) + { sin = &si->bindname.sa.in; } else { - if (si->myname.sa_socklen != sizeof(struct sockaddr_in)) { + if (swrap_address_ip_only_len(&si->myname) != + sizeof(struct sockaddr_in)) + { return 0; } sin = &si->myname.sa.in; @@ -5591,10 +5571,14 @@ static int swrap_msghdr_add_pktinfo(struct socket_info *si, struct sockaddr_in6 *sin6; struct in6_pktinfo pkt6; - if (si->bindname.sa_socklen == sizeof(struct sockaddr_in6)) { + if (swrap_address_ip_only_len(&si->bindname) == + sizeof(struct sockaddr_in6)) + { sin6 = &si->bindname.sa.in6; } else { - if (si->myname.sa_socklen != sizeof(struct sockaddr_in6)) { + if (swrap_address_ip_only_len(&si->myname) != + sizeof(struct sockaddr_in6)) + { return 0; } sin6 = &si->myname.sa.in6; @@ -5786,7 +5770,7 @@ static int swrap_sendmsg_filter_cmsg_sol_socket(const struct cmsghdr *cmsg, return rc; } -static const uint64_t swrap_unix_scm_right_magic = 0x8e0e13f27c42fc38; +static const uint64_t swrap_unix_scm_right_magic = 0x8e0e13f27c42fc39; /* * We only allow up to 6 fds at a time @@ -6869,7 +6853,8 @@ static ssize_t swrap_sendmsg_before(int fd, ret = sockaddr_convert_to_un(si, &si->peername.sa.s, - si->peername.sa_socklen, + swrap_address_ip_only_len( + &si->peername), tmp_un, 0, NULL); @@ -7144,7 +7129,8 @@ static int swrap_recvmsg_after(int fd, msg->msg_name != NULL && msg->msg_namelen > 0) { - msg->msg_namelen = MIN(si->peername.sa_socklen, msg->msg_namelen); + msg->msg_namelen = MIN(swrap_address_ip_only_len(&si->peername), + msg->msg_namelen); memcpy(msg->msg_name, &si->peername.sa, msg->msg_namelen); } @@ -8590,11 +8576,15 @@ static int swrap_remove_wrapper(const char *__func_name, goto set_next_free; } - if (si->myname.sa_socklen > 0 && si->peername.sa_socklen > 0) { + if (swrap_address_ip_only_len(&si->myname) > 0 && + swrap_address_ip_only_len(&si->peername) > 0) + { swrap_pcap_dump_packet(si, NULL, SWRAP_CLOSE_SEND, NULL, 0); } - if (si->myname.sa_socklen > 0 && si->peername.sa_socklen > 0) { + if (swrap_address_ip_only_len(&si->myname) > 0 && + swrap_address_ip_only_len(&si->peername) > 0) + { swrap_pcap_dump_packet(si, NULL, SWRAP_CLOSE_RECV, NULL, 0); swrap_pcap_dump_packet(si, NULL, SWRAP_CLOSE_ACK, NULL, 0); } -- GitLab From 3966d40904d4ce26a86d44ff253f6d69eee4e5ea Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Mon, 24 Nov 2025 17:25:02 +0100 Subject: [PATCH 11/14] tests: Add reproducer for python >= 3.14 fd passing problem Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- tests/CMakeLists.txt | 57 +++++++++++++++++++++++++++ tests/python_concurency_reproducer.py | 32 +++++++++++++++ 2 files changed, 89 insertions(+) create mode 100755 tests/python_concurency_reproducer.py diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 10e1d02..4c39d4c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -221,3 +221,60 @@ add_cmocka_test(test_fork_thread_deadlock LINK_LIBRARIES ${TORTURE_LIBRARY} ${CMOCKA_LIBRARY} thread_deadlock LINK_OPTIONS ${DEFAULT_LINK_FLAGS}) add_cmocka_test_environment(test_fork_thread_deadlock) + +# Python concurrency reproducer test +# Skip this test when building with ThreadSanitizer as it causes issues +if (CMAKE_BUILD_TYPE) + string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER_FOR_PYTHON) +endif() + +find_package(Python3 COMPONENTS Interpreter) +if (Python3_FOUND AND NOT CMAKE_BUILD_TYPE_LOWER_FOR_PYTHON STREQUAL "threadsanitizer") + # Create wrapper script that sets up temporary directory + set(PYTHON_TEST_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/run_python_concurrency_test.sh") + file(WRITE ${PYTHON_TEST_WRAPPER} +"#!/bin/bash + +TMPDIR=\$(mktemp -d /tmp/swrap_python_XXXXXX) +trap 'rm -rf \$TMPDIR' EXIT + +export SOCKET_WRAPPER_DIR=\$TMPDIR +export SOCKET_WRAPPER_DEFAULT_IFACE=10 + +${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/python_concurency_reproducer.py +") + + add_test(NAME test_python_concurrency_reproducer + COMMAND bash ${PYTHON_TEST_WRAPPER}) + + if (ASAN_LIBRARY) + list(APPEND PYTHON_PRELOAD_LIBRARIES ${ASAN_LIBRARY}) + endif() + list(APPEND PYTHON_PRELOAD_LIBRARIES ${SWRAP_FAKE_UID_WRAPPER_LOCATION}) + list(APPEND PYTHON_PRELOAD_LIBRARIES ${SOCKET_WRAPPER_LOCATION}) + + if (OSX) + set(PYTHON_TORTURE_ENVIRONMENT "DYLD_FORCE_FLAT_NAMESPACE=1;DYLD_INSERT_LIBRARIES=${SOCKET_WRAPPER_LOCATION}") + else () + string(REPLACE ";" ":" _PYTHON_TMP_ENV "${PYTHON_PRELOAD_LIBRARIES}") + set(PYTHON_TORTURE_ENVIRONMENT "LD_PRELOAD=${_PYTHON_TMP_ENV}") + endif() + + if (CMAKE_BUILD_TYPE) + string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) + if (CMAKE_BUILD_TYPE_LOWER STREQUAL "addresssanitizer" OR + CMAKE_BUILD_TYPE_LOWER STREQUAL "threadsanitizer" OR + CMAKE_BUILD_TYPE_LOWER STREQUAL "undefinedsanitizer") + list(APPEND PYTHON_TORTURE_ENVIRONMENT "SOCKET_WRAPPER_DISABLE_DEEPBIND=1") + endif() + endif() + + set_property(TEST test_python_concurrency_reproducer + PROPERTY ENVIRONMENT "${PYTHON_TORTURE_ENVIRONMENT}") + + # Mark test as expected to fail for Python >= 3.14 due to SWRAP_MAX_PASSED_FDS limitation + if (Python3_VERSION VERSION_GREATER_EQUAL "3.14") + set_property(TEST test_python_concurrency_reproducer + PROPERTY WILL_FAIL TRUE) + endif() +endif() diff --git a/tests/python_concurency_reproducer.py b/tests/python_concurency_reproducer.py new file mode 100755 index 0000000..1b6adae --- /dev/null +++ b/tests/python_concurency_reproducer.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +""" +Simplified minimal reproducer for SOCKET_WRAPPER concurrency issue. +""" + +import subprocess +import concurrent.futures + + +def run_subprocess(): + """Run a simple subprocess.""" + # Simple command that might trigger socket operations + p = subprocess.Popen(['echo', 'test'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + return stdout.decode().strip() + + +def main(): + # Run multiple subprocesses in parallel using ProcessPoolExecutor. The pool + # executor processes do fd passing over unix socket. + with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor: + futures = [executor.submit(run_subprocess) for _ in range(10)] + + for i, future in enumerate(concurrent.futures.as_completed(futures)): + result = future.result() + print(f"Task {i}: {result}") + + +if __name__ == '__main__': + main() -- GitLab From bf12b3bb899146c311caff35dc9a5f0ebbbd1d03 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Thu, 27 Nov 2025 09:49:19 +0100 Subject: [PATCH 12/14] swrap: Add socket_wrapper_debug_level() Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- src/socket_wrapper.c | 61 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c index 4409f61..e993006 100644 --- a/src/socket_wrapper.c +++ b/src/socket_wrapper.c @@ -448,6 +448,59 @@ static const char *getprogname(void) } #endif /* HAVE_GETPROGNAME */ +static int swrap_debug_level = SWRAP_LOG_ERROR; + +static void __socket_wrapper_debug_level_once(void) +{ + const char *env = NULL; + size_t env_len; + + env = getenv("SOCKET_WRAPPER_DEBUGLEVEL"); + if (env == NULL) { + swrap_debug_level = SWRAP_LOG_ERROR; + return; + } + + env_len = strlen(env); + /* Sanity check: must not be empty and not longer than "warning" */ + if (env_len == 0 || env_len > 7) { + swrap_debug_level = SWRAP_LOG_ERROR; + return; + } + + /* Check for string values (case-insensitive) */ + if (strcasecmp(env, "error") == 0) { + swrap_debug_level = SWRAP_LOG_ERROR; + } else if (strcasecmp(env, "warn") == 0 || + strcasecmp(env, "warning") == 0) + { + swrap_debug_level = SWRAP_LOG_WARN; + } else if (strcasecmp(env, "debug") == 0) { + swrap_debug_level = SWRAP_LOG_DEBUG; + } else if (strcasecmp(env, "trace") == 0) { + swrap_debug_level = SWRAP_LOG_TRACE; + } else { + /* Numeric value */ + swrap_debug_level = atoi(env); + + /* Clamp to valid range */ + if (swrap_debug_level < SWRAP_LOG_ERROR) { + swrap_debug_level = SWRAP_LOG_ERROR; + } else if (swrap_debug_level > SWRAP_LOG_TRACE) { + swrap_debug_level = SWRAP_LOG_TRACE; + } + } +} + +static unsigned int socket_wrapper_debug_level(void) +{ + static pthread_once_t debug_level_once = PTHREAD_ONCE_INIT; + + pthread_once(&debug_level_once, __socket_wrapper_debug_level_once); + + return (unsigned int)swrap_debug_level; +} + static void swrap_log(enum swrap_dbglvl_e dbglvl, const char *func, const char *format, ...) PRINTF_ATTRIBUTE(3, 4); # define SWRAP_LOG(dbglvl, ...) swrap_log((dbglvl), __func__, __VA_ARGS__) @@ -457,15 +510,11 @@ static void swrap_log(enum swrap_dbglvl_e dbglvl, { char buffer[1024]; va_list va; - const char *d; - unsigned int lvl = 0; + unsigned int lvl; const char *prefix = "SWRAP"; const char *progname = getprogname(); - d = getenv("SOCKET_WRAPPER_DEBUGLEVEL"); - if (d != NULL) { - lvl = atoi(d); - } + lvl = socket_wrapper_debug_level(); if (lvl < dbglvl) { return; -- GitLab From 64023cd4b756ee745a5498619a6066f5e637efe2 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Mon, 24 Nov 2025 17:53:52 +0100 Subject: [PATCH 13/14] swrap: Add debugging what fd types are being passed Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- src/socket_wrapper.c | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c index e993006..33b140c 100644 --- a/src/socket_wrapper.c +++ b/src/socket_wrapper.c @@ -6002,6 +6002,52 @@ static int swrap_sendmsg_unix_scm_rights(struct cmsghdr *cmsg, } num_fds_in = size_fds_in / sizeof(int); if (num_fds_in > SWRAP_MAX_PASSED_FDS) { +#ifndef NDEBUG /* Only for debug builds */ + size_t fd_idx; + union { + uint8_t *p; + int *fds; + } __fds_preview; + + __fds_preview.p = CMSG_DATA(cmsg); + + /* Debug: log what types of fds are being passed */ + for (fd_idx = 0; + fd_idx < num_fds_in && + socket_wrapper_debug_level() >= SWRAP_LOG_DEBUG; + fd_idx++) + { + int fd = __fds_preview.fds[fd_idx]; + struct stat st; + int rc_stat; + const char *fd_type = "unknown"; + int is_socket_wrapped = 0; + + rc_stat = fstat(fd, &st); + if (rc_stat == 0) { + if (S_ISSOCK(st.st_mode)) { + fd_type = "socket"; + /* Check if it's wrapped */ + is_socket_wrapped = (find_socket_info_index(fd) != -1); + } else if (S_ISREG(st.st_mode)) { + fd_type = "regular_file"; + } else if (S_ISDIR(st.st_mode)) { + fd_type = "directory"; + } else if (S_ISFIFO(st.st_mode)) { + fd_type = "fifo"; + } else if (S_ISCHR(st.st_mode)) { + fd_type = "char_device"; + } else if (S_ISBLK(st.st_mode)) { + fd_type = "block_device"; + } + } + + SWRAP_LOG(SWRAP_LOG_DEBUG, + " fds_in[%zu]=%d type=%s wrapped=%d", + fd_idx, fd, fd_type, is_socket_wrapped); + } +#endif /* NDEBUG */ + SWRAP_LOG(SWRAP_LOG_ERROR, "cmsg->cmsg_len=%zu,size_fds_in=%zu => " "num_fds_in=%zu > " @@ -6010,6 +6056,7 @@ static int swrap_sendmsg_unix_scm_rights(struct cmsghdr *cmsg, size_fds_in, num_fds_in, SWRAP_MAX_PASSED_FDS); + errno = EINVAL; return -1; } -- GitLab From e76fb993fe424dcd96d54b9623b120c4cc5a789e Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Mon, 24 Nov 2025 17:49:37 +0100 Subject: [PATCH 14/14] swrap: Fix fd-passing for Python 3.14's ProcessPoolExecutor ./tests/calc_unix_scm_rights_payload: Current configuration: SWRAP_MAX_PASSED_FDS = 16 SWRAP_MAX_PASSED_SOCKET_INFO = 16 sizeof(struct socket_info) = 240 bytes sizeof(struct swrap_unix_scm_rights) = 3904 bytes PIPE_BUF = 4096 bytes Header size (offsetof payload) = 40 bytes Payload base size (offsetof idxs) = 1 bytes Available space for arrays = 4055 bytes Calculated maximum values: SWRAP_MAX_PASSED_FDS = 16 SWRAP_MAX_PASSED_SOCKET_INFO = 16 Verification: Calculated total size (struct swrap_unix_scm_rights) = 3897 bytes Space used = 95.1% of PIPE_BUF Space remaining = 199 bytes Fits in PIPE_BUF: YES Signed-off-by: Andreas Schneider Reviewed-by: Stefan Metzmacher --- src/socket_wrapper.c | 29 ++++++++++++++++++++++++++++- tests/CMakeLists.txt | 6 ------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c index 33b140c..34918ac 100644 --- a/src/socket_wrapper.c +++ b/src/socket_wrapper.c @@ -5834,8 +5834,35 @@ static const uint64_t swrap_unix_scm_right_magic = 0x8e0e13f27c42fc39; #ifndef PIPE_BUF #define PIPE_BUF 4096 #endif -#define SWRAP_MAX_PASSED_FDS ((size_t)6) + +/* + * Maximum number of file descriptors that can be passed via SCM_RIGHTS. + * This is limited in normally by SCM_MAX_FD (typically 253). + * + * To do the calculation, run: ./tests/calc_unix_scm_rights_payload + * + * PIPE_BUF = 4096 bytes + * Header size (offsetof payload) = 40 bytes + * Payload base size (offsetof idxs) = 1 bytes + * Available space for arrays = 4055 bytes + * + * Calculated maximum values: + * SWRAP_MAX_PASSED_FDS = 16 + * SWRAP_MAX_PASSED_SOCKET_INFO = 16 + * + * Verification: + * Calculated total size (struct swrap_unix_scm_rights) = 3897 bytes + * Space used = 95.1% of PIPE_BUF + * Space remaining = 199 bytes + */ +#define SWRAP_MAX_PASSED_FDS ((size_t)16) + +/* + * Maximum number of wrapped socket_info structures to track in the payload. + * This needs to be kept under PIPE_BUF (4096) for non-blocking writes. + */ #define SWRAP_MAX_PASSED_SOCKET_INFO SWRAP_MAX_PASSED_FDS + struct swrap_unix_scm_rights_payload { uint8_t num_idxs; int8_t idxs[SWRAP_MAX_PASSED_FDS]; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4c39d4c..3f2489e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -271,10 +271,4 @@ ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/python_concurency_reproducer.p set_property(TEST test_python_concurrency_reproducer PROPERTY ENVIRONMENT "${PYTHON_TORTURE_ENVIRONMENT}") - - # Mark test as expected to fail for Python >= 3.14 due to SWRAP_MAX_PASSED_FDS limitation - if (Python3_VERSION VERSION_GREATER_EQUAL "3.14") - set_property(TEST test_python_concurrency_reproducer - PROPERTY WILL_FAIL TRUE) - endif() endif() -- GitLab