From 2849959079e47e281debf46e0af37b08fa3c603d Mon Sep 17 00:00:00 2001 From: Nick Strupat Date: Wed, 23 Sep 2026 18:32:26 -0400 Subject: [PATCH] fix: prevent crash in apps that weak-link MarketplaceKit on macOS 27 MarketplaceKit is only an empty stub for Mac Catalyst, so its symbols resolve to NULL in apps that weak-link it. On macOS 27 the `#available(iOS 17.4, *)` checks guarding those calls now pass, and apps such as Rocket League Sideswipe (via MarketplaceKitHelper) jump to a NULL AppDistributor function and crash with SIGSEGV on launch. Interpose _availability_version_check so that images weak-linking MarketplaceKit are told iOS 17.4+ is unavailable while MarketplaceKit is not loaded. All other availability checks are passed through unchanged. Co-Authored-By: Claude Opus 5.5 --- PlayTools/PlayLoader.m | 100 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/PlayTools/PlayLoader.m b/PlayTools/PlayLoader.m index ca928d69..1f582baa 100644 --- a/PlayTools/PlayLoader.m +++ b/PlayTools/PlayLoader.m @@ -5,6 +5,7 @@ #include #include +#include #include #import "PlayLoader.h" @@ -107,6 +108,104 @@ static int pt_sysctlbyname(const char *name, void *oldp, size_t *oldlenp, void * DYLD_INTERPOSE(pt_sysctlbyname, sysctlbyname) DYLD_INTERPOSE(pt_sysctl, sysctl) +// MarketplaceKit only exists as an empty stub for Mac Catalyst, but on macOS 27 the +// `#available(iOS 17.4, *)` checks guarding it pass. Images that weak-link it (such as +// MarketplaceKitHelper) then call NULL AppDistributor symbols and crash. Tell those +// images that iOS 17.4+ is unavailable so they take their fallback path instead. +#define MARKETPLACEKIT_PATH "/System/Library/Frameworks/MarketplaceKit.framework/MarketplaceKit" +#define MARKETPLACEKIT_MIN_IOS_VERSION 0x00110400 // 17.4.0, encoded as major << 16 | minor << 8 | patch +#define MAX_MARKETPLACEKIT_CLIENTS 8 + +typedef struct { + uint32_t platform; + uint32_t version; +} pt_build_version_t; + +bool _availability_version_check(uint32_t count, pt_build_version_t versions[]); + +static struct { + uintptr_t start; + uintptr_t end; +} marketplacekit_clients[MAX_MARKETPLACEKIT_CLIENTS]; +static _Atomic uint32_t marketplacekit_client_count = 0; + +static bool image_weak_links_marketplacekit(const struct mach_header_64 *header) { + const struct load_command *cmd = (const struct load_command *)(header + 1); + for (uint32_t i = 0; i < header->ncmds; i++) { + if (cmd->cmd == LC_LOAD_WEAK_DYLIB) { + const struct dylib_command *dylib = (const struct dylib_command *)cmd; + const char *name = (const char *)cmd + dylib->dylib.name.offset; + if (strcmp(name, MARKETPLACEKIT_PATH) == 0) { + return true; + } + } + cmd = (const struct load_command *)((const char *)cmd + cmd->cmdsize); + } + return false; +} + +// Called by dyld for every image, including those loaded before registration +static void track_marketplacekit_client(const struct mach_header *mh, intptr_t slide) { + const struct mach_header_64 *header = (const struct mach_header_64 *)mh; + if (!image_weak_links_marketplacekit(header)) { + return; + } + + uint32_t index = atomic_load(&marketplacekit_client_count); + unsigned long size = 0; + uint8_t *text = getsegmentdata(header, "__TEXT", &size); + if (index >= MAX_MARKETPLACEKIT_CLIENTS || text == NULL) { + return; + } + + marketplacekit_clients[index].start = (uintptr_t)text; + marketplacekit_clients[index].end = (uintptr_t)text + size; + atomic_store(&marketplacekit_client_count, index + 1); +} + +static bool is_marketplacekit_client(uintptr_t address) { + uint32_t count = atomic_load(&marketplacekit_client_count); + for (uint32_t i = 0; i < count; i++) { + if (address >= marketplacekit_clients[i].start && address < marketplacekit_clients[i].end) { + return true; + } + } + return false; +} + +static bool is_marketplacekit_loaded(void) { + static bool loaded = false; + static dispatch_once_t once; + dispatch_once(&once, ^{ + void *handle = dlopen(MARKETPLACEKIT_PATH, RTLD_LAZY | RTLD_NOLOAD); + loaded = handle != NULL; + if (handle != NULL) { + dlclose(handle); + } + }); + return loaded; +} + +static bool requires_marketplacekit_ios_version(uint32_t count, pt_build_version_t versions[]) { + for (uint32_t i = 0; i < count; i++) { + if (versions[i].platform == PLATFORM_IOS && versions[i].version >= MARKETPLACEKIT_MIN_IOS_VERSION) { + return true; + } + } + return false; +} + +static bool pt_availability_version_check(uint32_t count, pt_build_version_t versions[]) { + if (is_marketplacekit_client((uintptr_t)__builtin_return_address(0)) + && requires_marketplacekit_ios_version(count, versions) + && !is_marketplacekit_loaded()) { + return false; + } + return _availability_version_check(count, versions); +} + +DYLD_INTERPOSE(pt_availability_version_check, _availability_version_check) + // Interpose Apple Keychain functions (SecItemCopyMatching, SecItemAdd, SecItemUpdate, SecItemDelete) // This allows us to intercept keychain requests and return our own data @@ -330,6 +429,7 @@ static int pt_usleep(useconds_t time) { @implementation PlayLoader static void __attribute__((constructor)) initialize(void) { + _dyld_register_func_for_add_image(track_marketplacekit_client); [PlayCover launch]; if (ue_status == 0) {