Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client.jsonDecoderBuilder is not thread-safe #388

Open
jshier opened this issue Jun 22, 2023 · 0 comments
Open

Client.jsonDecoderBuilder is not thread-safe #388

jshier opened this issue Jun 22, 2023 · 0 comments

Comments

@jshier
Copy link

jshier commented Jun 22, 2023

Running with the thread sanitizer, occasionally multiple requests will trigger a thread sanitizer warning. For example:

==================
WARNING: ThreadSanitizer: data race (pid=7186)
  Read of size 8 at 0x000136232208 by thread T9:
    #0 Contentful.JSONDecoderBuilder.build() -> Foundation.JSONDecoder <null> (Contentful:arm64+0x6b880) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #1 Contentful.Client.fetchData(url: Foundation.URL, completion: (Swift.Result<Foundation.Data, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2aad4) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #2 Contentful.Client.fetchData(for: Contentful.AssetProtocol, with: Swift.Array<Contentful.ImageOption>, then: (Swift.Result<Foundation.Data, Swift.Error>) -> ()) -> Swift.Optional<__C.NSURLSessionDataTask> <null> (Contentful:arm64+0x34f78) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #3 Contentful.Client.fetchImage(for: Contentful.Asset, with: Swift.Array<Contentful.ImageOption>, then: (Swift.Result<__C.UIImage, Swift.Error>) -> ()) -> Swift.Optional<__C.NSURLSessionDataTask> <null> (Contentful:arm64+0x3762c) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #4 closure #1 (Swift.CheckedContinuation<__C.UIImage, Swift.Error>) -> () in (extension in App):Contentful.Client.fetchImage(from: Contentful.Asset) async throws -> __C.UIImage <null> (App:arm64+0x10026f4a8) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #5 partial apply forwarder for closure #1 (Swift.CheckedContinuation<__C.UIImage, Swift.Error>) -> () in (extension in App):Contentful.Client.fetchImage(from: Contentful.Asset) async throws -> __C.UIImage <null> (App:arm64+0x100270278) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #6 merged closure #1 (Swift.UnsafeContinuation<A, Swift.Never>) -> () in Swift.withCheckedContinuation<A>(function: Swift.String, _: (Swift.CheckedContinuation<A, Swift.Never>) -> ()) async -> A <null> (libswift_Concurrency.dylib:arm64+0x6250) (BuildId: 3aad7af116ba3c209def62b20fb04cbc32000000200000000700000000040f00)

  Previous write of size 8 at 0x000136232208 by thread T12:
    #0 Contentful.Client.localizationContext.setter : Swift.Optional<Contentful.LocalizationContext> <null> (Contentful:arm64+0x24044) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #1 closure #1 (Swift.Result<Contentful.HomogeneousArrayResponse<Contentful.Locale>, Swift.Error>) -> () in Contentful.Client.fetchCurrentSpaceLocales(then: (Swift.Result<Contentful.HomogeneousArrayResponse<Contentful.Locale>, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2f3ac) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #2 partial apply forwarder for closure #1 (Swift.Result<Contentful.HomogeneousArrayResponse<Contentful.Locale>, Swift.Error>) -> () in Contentful.Client.fetchCurrentSpaceLocales(then: (Swift.Result<Contentful.HomogeneousArrayResponse<Contentful.Locale>, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2f580) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #3 Contentful.Client.(handleJSON in _558EF92E0F1A7A570BADF83F33C4E0F3)<A where A: Swift.Decodable>(data: Foundation.Data, completion: (Swift.Result<A, Swift.Error>) -> ()) -> () <null> (Contentful:arm64+0x31a74) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #4 closure #1 (Swift.Result<Foundation.Data, Swift.Error>) -> () in Contentful.Client.fetchDecodable<A where A: Swift.Decodable>(url: Foundation.URL, completion: (Swift.Result<A, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x29eb0) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #5 partial apply forwarder for closure #1 (Swift.Result<Foundation.Data, Swift.Error>) -> () in Contentful.Client.fetchDecodable<A where A: Swift.Decodable>(url: Foundation.URL, completion: (Swift.Result<A, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2a044) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #6 closure #2 (Swift.Result<Foundation.Data, Swift.Error>) -> () in Contentful.Client.fetchDecodable<A where A: Swift.Decodable>(url: Foundation.URL, completion: (Swift.Result<A, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2a45c) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #7 partial apply forwarder for closure #2 (Swift.Result<Foundation.Data, Swift.Error>) -> () in Contentful.Client.fetchDecodable<A where A: Swift.Decodable>(url: Foundation.URL, completion: (Swift.Result<A, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2a6e8) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #8 closure #1 @Sendable (Swift.Optional<Foundation.Data>, Swift.Optional<__C.NSURLResponse>, Swift.Optional<Swift.Error>) -> () in Contentful.Client.fetchData(url: Foundation.URL, completion: (Swift.Result<Foundation.Data, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2c2d8) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #9 partial apply forwarder for closure #1 @Sendable (Swift.Optional<Foundation.Data>, Swift.Optional<__C.NSURLResponse>, Swift.Optional<Swift.Error>) -> () in Contentful.Client.fetchData(url: Foundation.URL, completion: (Swift.Result<Foundation.Data, Swift.Error>) -> ()) -> __C.NSURLSessionDataTask <null> (Contentful:arm64+0x2cc78) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #10 reabstraction thunk helper from @escaping @callee_guaranteed @Sendable (@guaranteed Swift.Optional<Foundation.Data>, @guaranteed Swift.Optional<__C.NSURLResponse>, @guaranteed Swift.Optional<Swift.Error>) -> () to @escaping @callee_unowned @convention(block) @Sendable (@unowned Swift.Optional<__C.NSData>, @unowned Swift.Optional<__C.NSURLResponse>, @unowned Swift.Optional<__C.NSError>) -> () <null> (Contentful:arm64+0x2cdf4) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #11 <null> <null> (CFNetwork:arm64+0x7ca4) (BuildId: d5424365b1e13893a3d79ba98e14b4dc32000000200000000700000000001100)
    #12 _dispatch_client_callout <null> (libdispatch.dylib:arm64+0x5978) (BuildId: 2c71a955e8ee34b580f395d8e33b36ea32000000200000000700000000001100)

  Location is heap block of size 80 at 0x0001362321e0 allocated by main thread:
    #0 __sanitizer_mz_malloc <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x53800) (BuildId: cb010fe3a00f38e3afb15e34eed35be532000000200000000700000000000e00)
    #1 _malloc_zone_malloc_instrumented_or_legacy <null> (libsystem_malloc.dylib:arm64+0x20d64) (BuildId: 41f0f02c8fca30a4905cf788fb82278332000000200000000700000000001100)
    #2 Contentful.Client.init(spaceId: Swift.String, environmentId: Swift.String, accessToken: Swift.String, host: Swift.String, clientConfiguration: Contentful.ClientConfiguration, sessionConfiguration: __C.NSURLSessionConfiguration, persistenceIntegration: Swift.Optional<Contentful.PersistenceIntegration>, contentTypeClasses: Swift.Optional<Swift.Array<Contentful.EntryDecodable.Type>>) -> Contentful.Client <null> (Contentful:arm64+0x260f8) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #3 Contentful.Client.__allocating_init(spaceId: Swift.String, environmentId: Swift.String, accessToken: Swift.String, host: Swift.String, clientConfiguration: Contentful.ClientConfiguration, sessionConfiguration: __C.NSURLSessionConfiguration, persistenceIntegration: Swift.Optional<Contentful.PersistenceIntegration>, contentTypeClasses: Swift.Optional<Swift.Array<Contentful.EntryDecodable.Type>>) -> Contentful.Client <null> (Contentful:arm64+0x25c38) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00)
    #4 App.CMS.() -> App.CMS(in _5EFF18E815206C1CAC3F3F55493FA824).init() -> App.CMS <null> (App:arm64+0x100267ab4) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #5 App.CMS.__allocating_init() -> App.CMS <null> (App:arm64+0x10026749c) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #6 one-time initialization function for shared <null> (App:arm64+0x100267414) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #7 dispatch_once <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x7c6a0) (BuildId: cb010fe3a00f38e3afb15e34eed35be532000000200000000700000000000e00)
    #8 dispatch_once_f <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x7c73c) (BuildId: cb010fe3a00f38e3afb15e34eed35be532000000200000000700000000000e00)
    #9 App.CMS.shared.unsafeMutableAddressor : App.CMS <null> (App:arm64+0x100267548) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #10 App.AppDelegate.init() -> App.AppDelegate <null> (App:arm64+0x100510410) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #11 @objc App.AppDelegate.init() -> App.AppDelegate <null> (App:arm64+0x100510724) (BuildId: e0723886cc7e3dfbb18fc97e007b4e7732000000280000000700000000000e00)
    #12 _UIApplicationMainPreparations <null> (UIKitCore:arm64+0xb856c0) (BuildId: 0732a1aeb6d8373d88a23dc5a63e7c4e32000000200000000700000000001100)
    #13 <null> <null> (0x000107b3d514)

  Thread T9 (tid=105081, running) is a GCD worker thread

  Thread T12 (tid=105154, running) is a GCD worker thread

SUMMARY: ThreadSanitizer: data race (/Users/jshier/Library/Developer/CoreSimulator/Devices/8069F768-73E0-4399-A9BF-E0491A89466F/data/Containers/Bundle/Application/F6F762D0-FB83-42B5-A7DE-DF8E79DA7827/App.app/Frameworks/Contentful.framework/Contentful:arm64+0x6b880) (BuildId: 952d92354b723ee2af700037cea82a2a32000000200000000700000000000e00) in Contentful.JSONDecoderBuilder.build() -> Foundation.JSONDecoder+0x2ac

Only modification here is that I've wrapped fetchImage in Swift concurrency, but that doesn't change the underly safety issue.

Fundamentally, the jsonDecoderBuilder property is unsafe because it's mutable yet accessible from multiple threads.

There are multiple possible solutions here but the simplest is likely a lock wrapper to provide safe access. I don't see such a construct in the project already, so I'm just reaching out to see if there's another preferred solution before I try to get the project fully building and testable locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant