From 4ce62e7a87c93cad52e48d2dfce8298863e8dbf1 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Fri, 18 Sep 2026 00:53:10 +0200 Subject: [PATCH] Fix for macOS symbol wrapping --- Modules/app-module/CMakeLists.txt | 14 +-- Modules/app-module/tests/CMakeLists.txt | 20 ++- .../source/pthread_stack_wrap.c | 11 +- Tactility/CMakeLists.txt | 7 +- Tactility/Source/AppStdioWrap.cpp | 117 ++++++++++++++++-- Tactility/Tests/CMakeLists.txt | 7 +- 6 files changed, 138 insertions(+), 38 deletions(-) diff --git a/Modules/app-module/CMakeLists.txt b/Modules/app-module/CMakeLists.txt index 442d58114..91572260b 100644 --- a/Modules/app-module/CMakeLists.txt +++ b/Modules/app-module/CMakeLists.txt @@ -12,14 +12,12 @@ tactility_add_module(app-module PRIV_REQUIRES TactilityKernelCpp ) -# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close() -# rather than calling ::read/::write/::close() directly, on every platform whose build wraps -# those symbols (ESP-IDF always; POSIX except macOS, whose linker doesn't support --wrap) - -# see Tactility/CMakeLists.txt and the top-level CMakeLists.txt for where that's applied. -if (NOT APPLE) - tactility_get_module_name(app-module MODULE_NAME) - target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO) -endif () +# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close() rather +# than calling ::read/::write/::close() directly, on every platform where those are wrapped: via +# -Wl,--wrap= on ESP-IDF and non-Apple POSIX (see Tactility/CMakeLists.txt and the top-level +# CMakeLists.txt), or via AppStdioWrap.cpp's dyld interpose on Apple (whose linker lacks --wrap). +tactility_get_module_name(app-module MODULE_NAME) +target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO) # install.cpp resolves an installed binary's fixed bin//.so path itself, so it # needs the same platform-arch string app-posix-module's own CMakeLists.txt defines for its diff --git a/Modules/app-module/tests/CMakeLists.txt b/Modules/app-module/tests/CMakeLists.txt index d10cf759a..617333680 100644 --- a/Modules/app-module/tests/CMakeLists.txt +++ b/Modules/app-module/tests/CMakeLists.txt @@ -6,22 +6,20 @@ enable_language(C CXX ASM) file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp) add_executable(AppModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES}) -if (NOT APPLE) - # Provides __wrap_read/write/close for the -Wl,--wrap= below (see Tactility/CMakeLists.txt - # for the canonical pairing of this file with those flags). - target_sources(AppModuleTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../../Tactility/Source/AppStdioWrap.cpp) -endif () +# Provides __wrap_read/write/close: paired with the -Wl,--wrap= below on non-Apple platforms (see +# Tactility/CMakeLists.txt), or self-registered via dyld interpose on Apple (see the file itself). +target_sources(AppModuleTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../../Tactility/Source/AppStdioWrap.cpp) target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC} ${CMAKE_CURRENT_LIST_DIR}/../private) add_test(NAME AppModuleTests COMMAND AppModuleTests) -# Matches app-module's own TT_APP_IO_WRAPS_STDIO (see Modules/app-module/CMakeLists.txt): -# io.cpp calls __real_read/write/close() on non-Apple POSIX, so any final link of it needs -# these wraps too, or those symbols go unresolved. The printf-family/getc-family flags are -# needed for the same reason: AppStdioWrap.cpp compiles those __wrap_* functions unconditionally -# on this platform (see its own #if guard), and they reference __real_vfprintf/fputs/fputc/ -# fgetc/fgets - those only exist once the matching --wrap flag is passed. +# Matches app-module's own TT_APP_IO_WRAPS_STDIO (see Modules/app-module/CMakeLists.txt): io.cpp +# calls __real_read/write/close() on every POSIX platform, so any final link of it needs these +# wraps too, or those symbols go unresolved. The printf-family/getc-family flags are needed for +# the same reason: AppStdioWrap.cpp compiles those __wrap_* functions unconditionally on this +# platform, and they reference __real_vfprintf/fputs/fputc/fgetc/fgets. --wrap provides all of +# these __real_* symbols on non-Apple; AppStdioWrap.cpp provides its own on Apple (see that file). if (NOT APPLE) target_link_options(AppModuleTests PRIVATE "-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close" diff --git a/Platforms/platform-posix/source/pthread_stack_wrap.c b/Platforms/platform-posix/source/pthread_stack_wrap.c index 5095a6ea1..faf71146f 100644 --- a/Platforms/platform-posix/source/pthread_stack_wrap.c +++ b/Platforms/platform-posix/source/pthread_stack_wrap.c @@ -20,8 +20,15 @@ int __wrap_pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t s } #ifdef __APPLE__ -#include -DYLD_INTERPOSE(__wrap_pthread_attr_setstack, pthread_attr_setstack) +// isn't a public SDK header (it ships with dyld's own source, not +// Xcode/Command Line Tools), so this reimplements its DYLD_INTERPOSE macro locally. +#define TT_DYLD_INTERPOSE(replacement, replacee) \ + __attribute__((used)) static struct { const void* replacement; const void* replacee; } \ + tt_interpose_##replacee __attribute__((section("__DATA,__interpose"))) = { \ + (const void*)(unsigned long)&(replacement), (const void*)(unsigned long)&(replacee) \ + }; + +TT_DYLD_INTERPOSE(__wrap_pthread_attr_setstack, pthread_attr_setstack) #endif diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index 37d05fa91..b175f2dff 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -188,7 +188,8 @@ else () # Routes every pthread_attr_setstack() call (FreeRTOS's POSIX port hands each task a stack # carved out of its own heap through this) to __wrap_pthread_attr_setstack() in # Platforms/platform-posix/source/pthread_stack_wrap.c, which no-ops it - see that file for why. - # --wrap is a GNU ld option; Apple's linker doesn't support it. + # --wrap is a GNU ld option; Apple's linker doesn't support it, so that file instead + # self-registers a dyld interpose for this symbol on Apple, needing no link flag here. if (NOT APPLE) target_link_options(Tactility PRIVATE "-Wl,--wrap=pthread_attr_setstack") # Routes every read()/write()/close() call to Tactility/Source/AppStdioWrap.cpp's @@ -196,6 +197,7 @@ else () # dispatch that lets an app's own stdio (e.g. a fileselection dialog's printf'd result # path) reach an AppStream a parent bound via app_manager_start_with_streams(). Mirrors # what ESP-IDF's build already does for the ESP32 target (see top-level CMakeLists.txt). + # On Apple, AppStdioWrap.cpp self-registers the same routing via dyld interpose instead. target_link_options(Tactility PRIVATE "-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close") # glibc's printf/fprintf/etc don't call the public write() symbol internally (they're # already compiled into libc.so, out of --wrap's reach), so the read/write/close wrap @@ -203,7 +205,8 @@ else () # wrappable syscall stubs - so this block is POSIX-only. Wrapping these symbols instead # redirects OUR OWN calls to them (the only ones --wrap can rewrite) through # AppStdioWrap.cpp's __wrap_* functions, which check the target stream (stdout/stdin) and - # fall back to the real libc function for any other FILE*. + # fall back to the real libc function for any other FILE*. Apple gets the same routing via + # dyld interpose, self-registered in that file - see its own #ifdef __APPLE__ block. target_link_options(Tactility PRIVATE "-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf" "-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc" diff --git a/Tactility/Source/AppStdioWrap.cpp b/Tactility/Source/AppStdioWrap.cpp index 9937b4b85..6227698eb 100644 --- a/Tactility/Source/AppStdioWrap.cpp +++ b/Tactility/Source/AppStdioWrap.cpp @@ -1,9 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 -// Paired with -Wl,--wrap=read/write/close - see Tactility/CMakeLists.txt (POSIX) and the -// top-level CMakeLists.txt (ESP32) for where that's applied. On a platform where it isn't -// (currently: macOS, whose linker doesn't support --wrap), these are simply never called - real -// read()/write()/close() calls go straight through unredirected. +// Paired with -Wl,--wrap=read/write/close on POSIX (Tactility/CMakeLists.txt) and ESP32 +// (top-level CMakeLists.txt); self-registered via dyld interpose below on Apple, whose linker +// doesn't support --wrap. #include #include @@ -24,21 +23,64 @@ int __wrap_close(int fd) { } +#ifdef __APPLE__ + +// --wrap also synthesizes __real_read/write/close automatically; dyld interpose doesn't, so +// io.cpp's TT_APP_IO_WRAPS_STDIO fallback needs them defined here. dlsym(RTLD_NEXT, ...) is the +// standard way to reach the true libSystem implementation despite the interpose below: a direct +// call to read/write/close from this file would just recurse into __wrap_read/write/close, since +// interpose rewrites every reference to those symbols in the process, this file included. +#include + +extern "C" { + +ssize_t __real_read(int fd, void* buffer, size_t size) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "read")); + return real(fd, buffer, size); +} + +ssize_t __real_write(int fd, const void* buffer, size_t size) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "write")); + return real(fd, buffer, size); +} + +int __real_close(int fd) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "close")); + return real(fd); +} + +} + +// isn't a public SDK header (it ships with dyld's own source, not +// Xcode/Command Line Tools), so this reimplements its DYLD_INTERPOSE macro locally; reused below +// for the printf-family interposes too. +#define TT_DYLD_INTERPOSE(replacement, replacee) \ + __attribute__((used)) static struct { const void* replacement; const void* replacee; } \ + tt_interpose_##replacee __attribute__((section("__DATA,__interpose"))) = { \ + (const void*)(unsigned long)&(replacement), (const void*)(unsigned long)&(replacee) \ + }; + +TT_DYLD_INTERPOSE(__wrap_read, read) +TT_DYLD_INTERPOSE(__wrap_write, write) +TT_DYLD_INTERPOSE(__wrap_close, close) + +#endif // __APPLE__ + // region glibc stdio wraps // -// glibc's printf/fprintf/etc are compiled into libc.so and call an internal, non-exported write() -// alias - --wrap=write (above) can't reach that call, only calls WE make to the public symbol. -// These wraps instead redirect calls WE make to printf/fprintf/etc, the same trick as read/write/ -// close above. Newlib (ESP-IDF) doesn't have this gap - its stdio does call the wrappable syscall -// stubs - so Tactility/CMakeLists.txt only applies the matching -Wl,--wrap= flags on POSIX. +// libc's printf/fprintf/etc are compiled into the C library and call an internal, non-exported +// write() alias - --wrap=write/the read/write/close interpose above can't reach that call, only +// calls WE make to the public symbol. These wraps instead redirect calls WE make to printf/ +// fprintf/etc, the same trick as read/write/close above. Newlib (ESP-IDF) doesn't have this gap - +// its stdio does call the wrappable syscall stubs - so this block is POSIX-only. // // Scoped to the printf/getc families only: fread/fwrite take an arbitrary FILE* and are already // used sitewide for real file I/O (e.g. File.cpp's readBinaryInternal), so wrapping them would // route every such call through this file's stdin/stdout check - a correctness risk for unrelated -// code that isn't worth taking here. putc/getc are excluded too since glibc defines them as +// code that isn't worth taking here. putc/getc are excluded too since libc defines them as // macros, not real calls, so wrapping those symbols wouldn't reliably intercept them. -#if !defined(ESP_PLATFORM) && !defined(__APPLE__) +#if !defined(ESP_PLATFORM) #include #include @@ -54,6 +96,43 @@ int __real_fgetc(FILE* stream); char* __real_fgets(char* buffer, int size, FILE* stream); } +#ifdef __APPLE__ + +// --wrap synthesizes these automatically elsewhere; on Apple they're defined here via +// dlsym(RTLD_NEXT, ...) instead - see the read/write/close __real_* block above for why. +#include + +extern "C" { + +int __real_vfprintf(FILE* stream, const char* format, va_list args) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "vfprintf")); + return real(stream, format, args); +} + +int __real_fputs(const char* s, FILE* stream) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "fputs")); + return real(s, stream); +} + +int __real_fputc(int c, FILE* stream) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "fputc")); + return real(c, stream); +} + +int __real_fgetc(FILE* stream) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "fgetc")); + return real(stream); +} + +char* __real_fgets(char* buffer, int size, FILE* stream) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "fgets")); + return real(buffer, size, stream); +} + +} + +#endif // __APPLE__ + namespace { void writeAllToStdout(const void* data, size_t size) { @@ -195,6 +274,20 @@ char* __wrap_fgets(char* buffer, int size, FILE* stream) { } -#endif // !ESP_PLATFORM && !__APPLE__ +#ifdef __APPLE__ +TT_DYLD_INTERPOSE(__wrap_vprintf, vprintf) +TT_DYLD_INTERPOSE(__wrap_printf, printf) +TT_DYLD_INTERPOSE(__wrap_vfprintf, vfprintf) +TT_DYLD_INTERPOSE(__wrap_fprintf, fprintf) +TT_DYLD_INTERPOSE(__wrap_puts, puts) +TT_DYLD_INTERPOSE(__wrap_fputs, fputs) +TT_DYLD_INTERPOSE(__wrap_putchar, putchar) +TT_DYLD_INTERPOSE(__wrap_fputc, fputc) +TT_DYLD_INTERPOSE(__wrap_getchar, getchar) +TT_DYLD_INTERPOSE(__wrap_fgetc, fgetc) +TT_DYLD_INTERPOSE(__wrap_fgets, fgets) +#endif // __APPLE__ + +#endif // !ESP_PLATFORM // endregion diff --git a/Tactility/Tests/CMakeLists.txt b/Tactility/Tests/CMakeLists.txt index 1a060ec24..ac2c2e7f3 100644 --- a/Tactility/Tests/CMakeLists.txt +++ b/Tactility/Tests/CMakeLists.txt @@ -20,9 +20,10 @@ target_include_directories(TactilityTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../P add_test(NAME TactilityTests COMMAND TactilityTests) # Matches Tactility/CMakeLists.txt's own set of --wrap flags: this binary also compiles -# AppStdioWrap.cpp and links app-module (whose io.cpp calls __real_read/write/close() on -# non-Apple POSIX - see TT_APP_IO_WRAPS_STDIO in Modules/app-module/CMakeLists.txt), so it -# needs the same wraps applied or those go unresolved. +# AppStdioWrap.cpp and links app-module (whose io.cpp calls __real_read/write/close() on every +# POSIX platform - see TT_APP_IO_WRAPS_STDIO in Modules/app-module/CMakeLists.txt), so it needs +# the same wraps applied on non-Apple, or those go unresolved; AppStdioWrap.cpp provides its own +# on Apple via dyld interpose (see that file). if (NOT APPLE) target_link_options(TactilityTests PRIVATE "-Wl,--wrap=pthread_attr_setstack" "-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close"