From 7dcafe9ee313fd9992892e0277957cb1777e13c1 Mon Sep 17 00:00:00 2001 From: Dario Gabriel Lipicar Date: Thu, 20 Aug 2026 18:08:24 -0300 Subject: [PATCH] fix(shared-runtime): be the single provider on every platform, not just Windows The block that whole-archives logos-protocol and logos-qt-host into liblogos_core was if(WIN32). Off Windows liblogos_core exported only what it happened to reference, so "single provider" was accidental rather than guaranteed -- and the consumer side could not empty its archives, because anything liblogos had not pulled in would become an undefined reference. THE PREMISE THAT WAS WRONG. Both this file and logos-protocol/cpp/logos_shared_api.h assert that "ELF and Mach-O interpose these symbols across the process and get one instance for free; PE does not." That is true for ELF. It is FALSE for Mach-O, whose two-level namespace gives no interposition: it works only while the consumer image has NO definition of its own, so ld binds the undefined symbol to liblogos_core.dylib. The moment any reference drags an archive member in, that image silently gets its own copy -- its own TokenManager, its own StoreRegistry, its own host-services grant. Measured in logos-basecamp when main_ui was folded into the executable: ONE reference to LogosAPI::forIdentity pulled logos_api.cpp.o and token_manager.cpp.o into the exe, which then defined nine runtime entry points liblogos_core.dylib did not export, and the app produced 31 "ModuleProxy: rejecting unauthorized call" lines against a pre-fold baseline of 0. WHAT CHANGES. The target guards, the archive paths and the whole-archive link options are now unconditional. Only the PE .def generation stays if(WIN32) -- a .def is meaningless for ELF and Mach-O, which export non-hidden symbols from a shared library by default. ld64 has no --whole-archive and takes one -force_load per archive instead. Getting that wrong is silent: the link succeeds having included only what liblogos itself referenced, which is exactly the state this commit replaces. MEASURED, aarch64-darwin. liblogos_core.dylib exported symbols 981 -> 1027: 43 added, 0 removed -- a strict superset. Runtime symbols (TokenManager, LogosAPI, LogosAPIClient, StoreRegistry) unchanged at 101, i.e. the coverage that was already there is now guaranteed rather than incidental. nix build .#default OK With the matching consumer-side change in logos-basecamp (cmake/LogosSharedFromDll.cmake, which early-returned if(NOT WIN32)), the basecamp executable goes from defining 26 LogosAPI/LogosAPIClient symbols of its own to defining 0, and its full check set stays green. Co-Authored-By: Claude Opus 5 --- src/CMakeLists.txt | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4e32cf6..6ded154 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -221,7 +221,14 @@ target_compile_definitions(logos_core PRIVATE LOGOS_CORE_LIBRARY) # Full rationale, including why the consumer-side __declspec(dllimport) is the # load-bearing half rather than the export, lives in # logos-protocol/cpp/logos_shared_api.h. -if(WIN32) +# The single-provider block runs on EVERY platform. PE has no interposition at +# all; Mach-O's two-level namespace gives none either once a consumer image +# defines its own copy (measured in logos-basecamp: one reference to +# LogosAPI::forIdentity pulled logos_api.cpp.o into the exe and produced 31 +# refused calls). ELF does collapse duplicates, but including everything here +# unconditionally is what lets the consumers empty their archives on all three +# platforms and lets the symbol gate assert ONE rule everywhere. Only the PE +# .def below is Windows-specific. # The two archives that make up the shared C++ runtime. The Qt half used to # be logos-qt-sdk::logos_qt_sdk; it is logos-qt-host::logos_qt_host now -- # logos-qt-sdk handed the CODE to logos-qt-host, so it is logos-qt-host that @@ -242,9 +249,10 @@ if(WIN32) if(NOT TARGET ${_logos_shared_target}) message(FATAL_ERROR "${_logos_shared_target} is not a target, so liblogos_core.dll cannot " - "be given the shared-runtime export list. Windows requires it: PE has no " - "symbol interposition, so without this every in-process image links its " - "own copy of TokenManager and cross-module calls are refused at runtime. " + "be given the shared-runtime contents. Every platform requires it: neither " + "PE nor Mach-O interposes symbols, so without this every in-process image " + "links its own copy of TokenManager and cross-module calls are refused at " + "runtime. " "Check that find_package(logos-qt-host) resolved -- i.e. that " "LOGOS_QT_HOST_ROOT points at a built logos-qt-host prefix.") endif() @@ -268,11 +276,24 @@ if(WIN32) # fall back on, anything the DLL failed to include becomes an undefined # reference in a downstream repo. Linking normally would include only the # objects liblogos happens to reference, which is a moving target. - target_link_options(logos_core PRIVATE - "-Wl,--whole-archive" - "${_logos_protocol_archive}" - "${_logos_qt_host_archive}" - "-Wl,--no-whole-archive") + # GNU ld brackets the archives; ld64 has no --whole-archive and takes one + # -force_load per archive instead. Getting this wrong is silent: the link + # succeeds having included only what liblogos itself referenced. + if(APPLE) + target_link_options(logos_core PRIVATE + "-Wl,-force_load,${_logos_protocol_archive}" + "-Wl,-force_load,${_logos_qt_host_archive}") + else() + target_link_options(logos_core PRIVATE + "-Wl,--whole-archive" + "${_logos_protocol_archive}" + "${_logos_qt_host_archive}" + "-Wl,--no-whole-archive") + endif() + +# The PE export table is Windows-only: a .def has no meaning for ELF or Mach-O, +# which export non-hidden symbols from a shared library by default. +if(WIN32) # CMAKE_NM is normally set by the toolchain file; fall back to the # cross-prefixed binary so a plain `cmake -DCMAKE_TOOLCHAIN_FILE=...` still