From 9e2a884f48cec809f1609b9c2161dacb1805d6b0 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 18 Oct 2026 08:41:54 -0700 Subject: [PATCH] fix(ffi/objc): key thread_local bridge caches by a per-bridge id The property-getter, round-trip and expando lookup caches identified a bridge by its address. After a JS reload a new bridge can be allocated where a destroyed one lived and match its entries: the getter cache then returned a raw pointer into the freed bridge (EXC_BAD_ACCESS in preparedObjCInvocationIsInit on the next launch), and cached misses could hide values that exist. --- .../ffi/objc/shared/bridge/ObjCBridge.mm | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm b/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm index 5baecf609..57acf87fa 100644 --- a/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm +++ b/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm @@ -708,7 +708,7 @@ Value findRoundTripValue(Runtime& runtime, const void* native, ? validationKey : (nativeIsObject ? nativeObjectClassKey(native) : 0); struct RoundTripCacheEntry { - const NativeApiBridge* bridge = nullptr; + uint64_t bridgeId = 0; uintptr_t key = 0; uint64_t generation = 0; std::weak_ptr value; @@ -722,7 +722,7 @@ Value findRoundTripValue(Runtime& runtime, const void* native, const size_t firstSlot = (key >> 4) & 3; for (size_t i = 0; i < 4; i++) { RoundTripCacheEntry& entry = cache[(firstSlot + i) & 3]; - if (entry.bridge == this && entry.key == key && + if (entry.bridgeId == instanceId_ && entry.key == key && entry.generation == generation) { if (entry.validationKey != expectedValidationKey) { break; @@ -777,13 +777,13 @@ Value findRoundTripValue(Runtime& runtime, const void* native, } if (entry == nullptr) { cache[firstSlot] = RoundTripCacheEntry{ - this, key, generation, {}, true, false, expectedValidationKey}; + instanceId_, key, generation, {}, true, false, expectedValidationKey}; return Value::undefined(); } storedValue = entry->value; cachedStringLike = entry->stringLikeNative; cache[firstSlot] = RoundTripCacheEntry{ - this, key, generation, storedValue, false, cachedStringLike, + instanceId_, key, generation, storedValue, false, cachedStringLike, entry->validationKey}; } if (stringLikeNative != nullptr) { @@ -973,7 +973,7 @@ Value findObjectExpando(Runtime& runtime, const void* native, return Value::undefined(); } struct ObjectExpandoCacheEntry { - const NativeApiBridge* bridge = nullptr; + uint64_t bridgeId = 0; uintptr_t key = 0; uint64_t generation = 0; std::string property; @@ -988,7 +988,7 @@ Value findObjectExpando(Runtime& runtime, const void* native, const uint64_t generation = objectExpandosGeneration_.load(std::memory_order_acquire); for (auto& entry : cache) { - if (entry.bridge == this && entry.key == key && + if (entry.bridgeId == instanceId_ && entry.key == key && entry.generation == generation && entry.property == property) { if (entry.miss) { return Value::undefined(); @@ -1004,17 +1004,17 @@ Value findObjectExpando(Runtime& runtime, const void* native, const size_t slot = nextSlot++ & 7; if (objectIt == objectExpandos_.end()) { cache[slot] = - ObjectExpandoCacheEntry{this, key, generation, property, {}, true}; + ObjectExpandoCacheEntry{instanceId_, key, generation, property, {}, true}; return Value::undefined(); } auto propertyIt = objectIt->second.find(property); if (propertyIt == objectIt->second.end() || propertyIt->second == nullptr) { cache[slot] = - ObjectExpandoCacheEntry{this, key, generation, property, {}, true}; + ObjectExpandoCacheEntry{instanceId_, key, generation, property, {}, true}; return Value::undefined(); } cache[slot] = ObjectExpandoCacheEntry{ - this, key, generation, property, propertyIt->second, false}; + instanceId_, key, generation, property, propertyIt->second, false}; return Value(runtime, *propertyIt->second); } @@ -1045,7 +1045,7 @@ void forgetObjectExpandos(const void* native) { return nullptr; } struct PropertyGetterCacheEntry { - const NativeApiBridge* bridge = nullptr; + uint64_t bridgeId = 0; Class cls = Nil; uint64_t generation = 0; std::string property; @@ -1058,7 +1058,7 @@ void forgetObjectExpandos(const void* native) { const uint64_t generation = propertyGetterCacheGeneration_.load(std::memory_order_acquire); for (auto& entry : cache) { - if (entry.bridge == this && entry.cls == cls && + if (entry.bridgeId == instanceId_ && entry.cls == cls && entry.generation == generation && entry.property == property) { return entry.miss ? nullptr : entry.getter; } @@ -1068,17 +1068,17 @@ void forgetObjectExpandos(const void* native) { const size_t slot = nextSlot++ & 7; if (classIt == propertyGetterCache_.end()) { cache[slot] = PropertyGetterCacheEntry{ - this, cls, generation, property, nullptr, true}; + instanceId_, cls, generation, property, nullptr, true}; return nullptr; } auto propIt = classIt->second.find(property); if (propIt == classIt->second.end()) { cache[slot] = PropertyGetterCacheEntry{ - this, cls, generation, property, nullptr, true}; + instanceId_, cls, generation, property, nullptr, true}; return nullptr; } cache[slot] = - PropertyGetterCacheEntry{this, cls, generation, property, + PropertyGetterCacheEntry{instanceId_, cls, generation, property, &propIt->second, false}; return &propIt->second; } @@ -2133,6 +2133,13 @@ static void appendSurfaceMember( objectExpandos_; std::unordered_map objectExpandoOwnerCounts_; std::atomic objectExpandosGeneration_{1}; + // Identifies this bridge in the thread_local lookup caches. Its address + // cannot: a bridge allocated where a destroyed one lived would match that + // one's entries, and the property-getter cache holds raw pointers into it. + const uint64_t instanceId_ = [] { + static std::atomic next{1}; + return next.fetch_add(1, std::memory_order_relaxed); + }(); std::unordered_map> propertyGetterCache_; std::atomic propertyGetterCacheGeneration_{1};