From f131afce4a0c208528ac8ba7429f1766843743db Mon Sep 17 00:00:00 2001 From: aszlig Date: Wed, 20 May 2026 03:07:19 +0200 Subject: [PATCH] Only access non-locals after they're initialised This fixes a long-standing issue on aarch64, which is caused by mimalloc in Python 3 calling close() too early: #0 0x0000007ff75952ac in __pthread_kill_implementation () from .../lib/libc.so.6 #1 0x0000007ff753c5b8 in raise () from .../lib/libc.so.6 #2 0x0000007ff7525d38 in abort () from .../lib/libc.so.6 #3 0x0000007ff7299818 in __gnu_cxx::__verbose_terminate_handler() () from .../lib/libstdc++.so.6 #4 0x0000007ff729704c in __cxxabiv1::__terminate(void (*)()) () from .../lib/libstdc++.so.6 #5 0x0000007ff728e49c in std::terminate() () from .../lib/libstdc++.so.6 #6 0x0000007ff72973dc in __cxa_throw () from .../lib/libstdc++.so.6 #7 0x0000007ff728ed7c in std::__throw_bad_array_new_length() () from .../lib/libstdc++.so.6 #8 0x0000007ff7f10c54 in std::__new_allocator::allocate (this=0x7fffffe890, __n=18446744073709551557) at .../include/c++/15.2.0/bits/new_allocator.h:139 #9 0x0000007ff7f6e898 in std::allocator_traits >::allocate (__a=..., __n=18446744073709551557) at .../include/c++/15.2.0/bits/alloc_traits.h:614 #10 std::__detail::_Hashtable_alloc > >::_M_allocate_buckets (this=0x7ff7fb02a0 , __bkt_count=18446744073709551557) at .../include/c++/15.2.0/bits/hashtable_policy.h:1605 #11 0x0000007ff7f6e500 in std::_Hashtable, std::__detail::_Identity, std::equal_to, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits >::_M_allocate_buckets (this=0x7ff7fb02a0 , __bkt_count=18446744073709551557) at .../include/c++/15.2.0/bits/hashtable.h:413 #12 0x0000007ff7f6d944 in std::_Hashtable, std::__detail::_Identity, std::equal_to, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits >::_M_rehash (this=0x7ff7fb02a0 , __bkt_count=18446744073709551557) at .../include/c++/15.2.0/bits/hashtable.h:2754 #13 0x0000007ff7f6c0b8 in std::_Hashtable, std::__detail::_Identity, std::equal_to, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits >::_M_insert_unique_node (this=0x7ff7fb02a0 , __bkt=3, __code=3, __node=0x5555583580, __n_elt=1) at .../include/c++/15.2.0/bits/hashtable.h:2479 #14 0x0000007ff7f6a034 in std::_Hashtable, std::__detail::_Identity, std::equal_to, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits >::_M_emplace_uniq (this=0x7ff7fb02a0 ) at .../include/c++/15.2.0/bits/hashtable.h:2365 #15 0x0000007ff7f685a8 in std::_Hashtable, std::__detail::_Identity, std::equal_to, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits >::insert (this=0x7ff7fb02a0 , __v=@0x7fffffebb8: 3) at .../include/c++/15.2.0/bits/hashtable.h:1049 #16 0x0000007ff7f66b60 in std::unordered_set, std::equal_to, std::allocator >::insert (this=0x7ff7fb02a0 , __x=@0x7fffffebb8: 3) at .../include/c++/15.2.0/bits/unordered_set.h:475 #17 0x0000007ff7f64c80 in Systemd::init (rules=...) at ../src/systemd.cc:213 #18 0x0000007ff7f14bd0 in init_rules () at ../src/preload.cc:77 #19 0x0000007ff7f19edc in ip2unix_wrap_close (fd=4) at ../src/preload.cc:677 #20 0x0000007ff78cf430 in mi_process_init.part () from .../lib/libpython3.13.so.1.0 #21 0x0000007ff7894ee8 in _mi_process_init () from .../lib/libpython3.13.so.1.0 #22 0x0000007ff7fc32c4 in call_init () from .../lib/ld-linux-aarch64.so.1 #23 0x0000007ff7fc341c in _dl_init () from .../lib/ld-linux-aarch64.so.1 #24 0x0000007ff7fdbea0 in _start () from .../lib/ld-linux-aarch64.so.1 Here the non-local all_fds variable is an unordered_set which hasn't yet been initialised but is accessed because _mi_process_init has a lower priority and thus is initialised *before* all_fds. Since mi_process_init calls close(), an insert on the all_fds variable is triggered by the systemd rule initialisation, which then fails because all_fds hasn't been initialised yet. The all_fds variable isn't the only one that's affected, so in order to avoid wrapping all non-local variables in lazy initalisation wrappers, we now short-circuit all the library calls that trigger the ruleset initialisation machinery. While at it, I also fixed a potential data race that could emerge in multithreaded programs. While in practice most networking code is initialised before actually spawning threads, it's still something that could happen, so let's use std::call_once on rule initialisation. Signed-off-by: aszlig --- src/preload.cc | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/preload.cc b/src/preload.cc index 1256db4..9d9b17c 100644 --- a/src/preload.cc +++ b/src/preload.cc @@ -37,8 +37,23 @@ static std::mutex g_rules_mutex; +static std::once_flag g_rules_init_flag; static std::shared_ptr> g_rules = nullptr; +/* + * This is needed because non-local initialisation is not guaranteed to have + * happened as soon as one of our wrapper functions is called. + */ +static bool g_initialised = false; + +__attribute__((constructor(65535))) void mark_initialised(void) { + g_initialised = true; +} + +__attribute__((destructor(65535))) void mark_uninitialised(void) { + g_initialised = false; +} + using RuleMatch = std::optional>; static void init_rules(void) @@ -170,7 +185,7 @@ extern "C" int WRAP_SYM(listen)(int sockfd, int backlog) static RuleMatch match_rule(const SockAddr &addr, const Socket::Ptr sock, const RuleDir dir) { - init_rules(); + std::call_once(g_rules_init_flag, init_rules); size_t rulepos = 0; for ( @@ -318,11 +333,11 @@ static inline int bind_connect(SockFun &&sockfun, RealFun &&realfun, RuleDir dir, int fd, const struct sockaddr *addr, socklen_t addrlen) { - if ( + if (!g_initialised || ( addr->sa_family != AF_INET && addr->sa_family != AF_INET6 && addr->sa_family != AF_UNIX - ) return std::invoke(realfun, fd, addr, addrlen); + )) return std::invoke(realfun, fd, addr, addrlen); // NOLINTNEXTLINE(performance-unnecessary-value-param) return Socket::when(fd, [&](Socket::Ptr sock) { @@ -535,7 +550,7 @@ extern "C" ssize_t WRAP_SYM(sendto)(int fd, const void *buf, size_t len, { TRACE_CALL("sendto", fd, buf, len, flags, addr, addrlen); - if (addr == nullptr) + if (!g_initialised || addr == nullptr) return real::sendto(fd, buf, len, flags, addr, addrlen); // NOLINTNEXTLINE(performance-unnecessary-value-param) @@ -583,7 +598,7 @@ extern "C" ssize_t WRAP_SYM(sendmsg)(int fd, const struct msghdr *msg, { TRACE_CALL("sendmsg", fd, msg, flags); - if (msg->msg_name == nullptr) + if (!g_initialised || msg->msg_name == nullptr) return real::sendmsg(fd, msg, flags); // NOLINTNEXTLINE(performance-unnecessary-value-param) @@ -671,10 +686,13 @@ extern "C" int WRAP_SYM(close)(int fd) { TRACE_CALL("close", fd); + if (!g_initialised) + return real::close(fd); + #ifdef SYSTEMD_SUPPORT { std::scoped_lock lock(g_rules_mutex); - init_rules(); + std::call_once(g_rules_init_flag, init_rules); if (Systemd::has_fd(fd)) { LOG(DEBUG) << "Prevented socket fd " << fd << " from being closed," << " because it's a file descriptor passed by systemd.";