From 393c3519d079760d70b92c7ca4aee06b3eb299e9 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 16 Oct 2026 22:19:34 +0800 Subject: [PATCH 1/2] build: statically link OpenSSL (and MinGW libgcc/winpthread) for distributable binaries The released shared libraries are supposed to be usable as-is, without the consumer installing anything. They were not: - macOS: libfakelua.dylib had absolute Homebrew paths baked in (/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib), so it only loaded on machines with that exact prefix. Any other Mac got "dyld: Library not loaded". - Windows: libfakelua.dll needed libssl-3-x64.dll, libcrypto-3-x64.dll, libgcc_s_seh-1.dll, libwinpthread-1.dll and libstdc++-6.dll, none of which shipped in the release zip. Link OpenSSL statically via OPENSSL_USE_STATIC_LIBS, behind a new FAKELUA_PREFER_STATIC_DEPS option (default ON). On MinGW also statically link libgcc and winpthread. libstdc++/libc++ stay dynamic on purpose: include/fakelua.h exposes STL types across the ABI boundary (std::string returned by value, std::vector>) and ViToString() is implemented inline in that header, so host and library must share a single copy of libstdc++. Linking it statically would give each side its own copy and corrupt the heap when a std::string crosses the boundary. A configure-time warning fires if OpenSSL resolves to shared libs anyway (e.g. an OpenSSL install without the .a files), so the regression can't come back silently. Verified on macOS: otool -L now reports only /usr/lib/libc++.1.dylib and /usr/lib/libSystem.B.dylib; test_crypto.* and test_http.tls_echo (60 tests) all pass. --- src/CMakeLists.txt | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index afc7c1e..cfc2487 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -293,6 +293,21 @@ if (WIN32) endif() # OpenSSL for TLS (libevent bufferevent_openssl, libmysqlclient). +# +# Linked statically on purpose: the released shared library must not carry a runtime +# dependency on a particular OpenSSL installation. With the shared libs, the macOS +# dylib gets an absolute path baked in (/opt/homebrew/opt/openssl@N/lib/libssl.N.dylib) +# and fails to load on any machine that does not have that exact Homebrew prefix; the +# Windows DLL likewise dragged in libssl-3-x64.dll / libcrypto-3-x64.dll. +option(FAKELUA_PREFER_STATIC_DEPS "Prefer static third-party libraries where possible" ON) +if (FAKELUA_PREFER_STATIC_DEPS) + set(OPENSSL_USE_STATIC_LIBS TRUE) + if (WIN32) + # Match the static CRT choice when consuming MSVC-built OpenSSL. + set(OPENSSL_MSVC_STATIC_RT ON) + endif () +endif () + if (WIN32 AND NOT OPENSSL_ROOT_DIR) get_filename_component(_fakelua_mingw_bin "${CMAKE_CXX_COMPILER}" DIRECTORY) get_filename_component(_fakelua_mingw_root "${_fakelua_mingw_bin}" DIRECTORY) @@ -305,6 +320,23 @@ endif() find_package(OpenSSL REQUIRED) find_package(Threads REQUIRED) +if (FAKELUA_PREFER_STATIC_DEPS) + # Guard against silently resolving to the shared libs (e.g. a Homebrew keg installed + # without the .a files), which would reintroduce the hardcoded runtime path. + set(_fl_openssl_shared "") + foreach (_fl_ssl_lib OpenSSL::SSL OpenSSL::Crypto) + get_target_property(_fl_lib_loc ${_fl_ssl_lib} IMPORTED_LOCATION) + if (_fl_lib_loc MATCHES "\\.(so|dylib)(\\..*)?$") + list(APPEND _fl_openssl_shared "${_fl_ssl_lib}=${_fl_lib_loc}") + endif () + endforeach () + if (_fl_openssl_shared) + message(WARNING + "FAKELUA_PREFER_STATIC_DEPS is ON but OpenSSL resolved to non-static libs: " + "${_fl_openssl_shared}. The shipped library will depend on OpenSSL at runtime.") + endif () +endif () + # libevent: per-State event_base, driven by runtime.tick() via EVLOOP_NONBLOCK. CPMAddPackage( NAME libevent @@ -499,6 +531,20 @@ endif() target_link_libraries(fakelua PRIVATE ${TARGET_LIB}) +# MinGW runtime: drop libgcc_s_seh-1.dll and libwinpthread-1.dll from the shipped DLL. +# +# libstdc++ is deliberately NOT statically linked. include/fakelua.h exposes STL types +# across the ABI boundary (std::string returned by value, std::vector>), +# and ViToString() is implemented inline in that header — so host and library must share +# one copy of libstdc++. Statically linking it would give each side its own copy and +# corrupt the heap whenever a std::string crosses the boundary. +# libgcc / winpthread are pure C-level runtimes with no such ABI surface, so they are safe. +if (FAKELUA_PREFER_STATIC_DEPS AND MINGW) + target_link_options(fakelua PRIVATE + -static-libgcc + -Wl,-Bstatic -lwinpthread -Wl,-Bdynamic) +endif () + target_include_directories(fakelua PUBLIC $ $ From 8f0a9897a381aa97223d785208658063f5cc3321 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 16 Oct 2026 23:59:55 +0800 Subject: [PATCH 2/2] fix: keep shared OpenSSL on MinGW so the legacy provider loads Static OpenSSL on MinGW broke 8 test_crypto cases (rc4 / blowfish / des). Those algorithms live in OpenSSL 3's "legacy" provider, a loadable module found via the MODULESDIR compiled into libcrypto; MSYS2 bakes in the MSYS path "/mingw64/lib/ossl-modules", which Win32 LoadLibrary cannot resolve. The shared libcrypto derives the module dir from its own DLL location, so it finds legacy.dll. Also warn (instead of silently ignoring) when the legacy provider fails to load, and extend the static-lib guard to catch MinGW/MSVC import libraries. --- src/CMakeLists.txt | 17 ++++++++++++++--- src/native/crypto/native_crypto.cpp | 17 ++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cfc2487..1330a40 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -300,13 +300,23 @@ endif() # and fails to load on any machine that does not have that exact Homebrew prefix; the # Windows DLL likewise dragged in libssl-3-x64.dll / libcrypto-3-x64.dll. option(FAKELUA_PREFER_STATIC_DEPS "Prefer static third-party libraries where possible" ON) -if (FAKELUA_PREFER_STATIC_DEPS) +if (FAKELUA_PREFER_STATIC_DEPS AND NOT MINGW) set(OPENSSL_USE_STATIC_LIBS TRUE) if (WIN32) # Match the static CRT choice when consuming MSVC-built OpenSSL. set(OPENSSL_MSVC_STATIC_RT ON) endif () endif () +# MinGW keeps the *shared* OpenSSL on purpose (libcrypto-3-x64.dll / libssl-3-x64.dll). +# +# RC4 / Blowfish / DES live in OpenSSL 3's "legacy" provider, which is a loadable module +# (ossl-modules/legacy.dll), not part of libcrypto itself. A static libcrypto can only find +# that module through the MODULESDIR compiled into it, and MSYS2 builds OpenSSL with a MSYS +# path there ("/mingw64/lib/ossl-modules") that Win32 LoadLibrary cannot resolve. Statically +# linking OpenSSL on MinGW therefore makes OSSL_PROVIDER_load("legacy") fail silently and +# breaks crypto.rc4 / blowfish / des (verified: BuildWithWindows, 8 test_crypto failures). +# The shared libcrypto derives its module directory from the DLL location at runtime, so it +# finds legacy.dll. Ship those two DLLs next to libfakelua.dll in the release archive. if (WIN32 AND NOT OPENSSL_ROOT_DIR) get_filename_component(_fakelua_mingw_bin "${CMAKE_CXX_COMPILER}" DIRECTORY) @@ -320,13 +330,14 @@ endif() find_package(OpenSSL REQUIRED) find_package(Threads REQUIRED) -if (FAKELUA_PREFER_STATIC_DEPS) +if (OPENSSL_USE_STATIC_LIBS) # Guard against silently resolving to the shared libs (e.g. a Homebrew keg installed # without the .a files), which would reintroduce the hardcoded runtime path. + # ".dll.a" covers the MinGW/MSVC import libraries of the OpenSSL DLLs. set(_fl_openssl_shared "") foreach (_fl_ssl_lib OpenSSL::SSL OpenSSL::Crypto) get_target_property(_fl_lib_loc ${_fl_ssl_lib} IMPORTED_LOCATION) - if (_fl_lib_loc MATCHES "\\.(so|dylib)(\\..*)?$") + if (_fl_lib_loc MATCHES "\\.(so|dylib|dll\\.a)(\\..*)?$") list(APPEND _fl_openssl_shared "${_fl_ssl_lib}=${_fl_lib_loc}") endif () endforeach () diff --git a/src/native/crypto/native_crypto.cpp b/src/native/crypto/native_crypto.cpp index 277baa6..9794f1b 100644 --- a/src/native/crypto/native_crypto.cpp +++ b/src/native/crypto/native_crypto.cpp @@ -532,13 +532,20 @@ void RegisterCryptoLibraryApi(State *s) { // On OpenSSL 3.x, RC4 / Blowfish / DES are in the "legacy" provider which // is not loaded by default. Load it once here so callers don't have to - // configure openssl.cnf manually. Failure is silently ignored: if the - // legacy provider is already loaded (via config), or if this OpenSSL build - // doesn't have it, the rest of the API still works. + // configure openssl.cnf manually. Failure only degrades the legacy ciphers: + // the rest of the API still works, so warn instead of throwing. static std::once_flag legacy_provider_init; - std::call_once(legacy_provider_init, []() { + std::call_once(legacy_provider_init, [s]() { #if OPENSSL_VERSION_MAJOR >= 3 - OSSL_PROVIDER_load(nullptr, "legacy"); + if (OSSL_PROVIDER_load(nullptr, "legacy") == nullptr) { + // Most common cause: libcrypto cannot find its provider modules + // (ossl-modules/legacy), e.g. a static OpenSSL whose MODULESDIR is wrong. + LOG_WARN(s, + "crypto", + "failed to load the OpenSSL \"legacy\" provider: crypto.rc4 / blowfish / des " + "and the matching crypto.encrypt/decrypt algorithms will fail. " + "check OPENSSL_MODULES or the OpenSSL modules directory"); + } OSSL_PROVIDER_load(nullptr, "default"); #endif });