Skip to content

Commit bf2500e

Browse files
mhorowitzfacebook-github-bot
authored andcommitted
Switch to synchronous strategy for unprotect
Summary: Older versions of JSC (ios 11 and before) have a bug which I believe the ProtectionQueue mechanism tickles: https://bugs.webkit.org/show_bug.cgi?id=186827 This removes the ProtectionQueue and replaces it with an atomic flag to avoid calling unprotect after VM shutdown. This also fixes a race condition in shutdown. Reviewed By: danzimm Differential Revision: D12969953 fbshipit-source-id: fa3a14f3207be67a987ac3cf0fc1c9ce88837b0b
1 parent 2b01da0 commit bf2500e

File tree

2 files changed

+52
-143
lines changed

2 files changed

+52
-143
lines changed

ReactCommon/jsi/JSCRuntime.cpp

+43-141
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,19 @@
66
#include "JSCRuntime.h"
77

88
#include <JavaScriptCore/JavaScript.h>
9+
#include <atomic>
910
#include <condition_variable>
1011
#include <cstdlib>
1112
#include <mutex>
1213
#include <queue>
1314
#include <sstream>
1415
#include <thread>
1516

16-
#ifndef NDEBUG
17-
#include <atomic>
18-
#endif
19-
2017
namespace facebook {
2118
namespace jsc {
2219

2320
namespace detail {
2421
class ArgsConverter;
25-
class ProtectionQueue;
2622
} // namespace detail
2723

2824
class JSCRuntime;
@@ -65,7 +61,6 @@ class JSCRuntime : public jsi::Runtime {
6561

6662
protected:
6763
friend class detail::ArgsConverter;
68-
friend class detail::ProtectionQueue;
6964
class JSCStringValue final : public PointerValue {
7065
#ifndef NDEBUG
7166
JSCStringValue(JSStringRef str, std::atomic<intptr_t>& counter);
@@ -83,31 +78,26 @@ class JSCRuntime : public jsi::Runtime {
8378
};
8479

8580
class JSCObjectValue final : public PointerValue {
86-
#ifndef NDEBUG
8781
JSCObjectValue(
8882
JSGlobalContextRef ctx,
89-
detail::ProtectionQueue& pq,
90-
JSObjectRef obj,
91-
std::atomic<intptr_t>& counter);
92-
#else
93-
JSCObjectValue(
94-
JSGlobalContextRef context,
95-
detail::ProtectionQueue& pq,
96-
JSObjectRef obj);
83+
const std::atomic<bool>& ctxInvalid,
84+
JSObjectRef obj
85+
#ifndef NDEBUG
86+
,
87+
std::atomic<intptr_t>& counter
9788
#endif
89+
);
9890

9991
void invalidate() override;
100-
void unprotect();
10192

10293
JSGlobalContextRef ctx_;
94+
const std::atomic<bool>& ctxInvalid_;
10395
JSObjectRef obj_;
104-
detail::ProtectionQueue& protectionQueue_;
10596
#ifndef NDEBUG
10697
std::atomic<intptr_t>& counter_;
10798
#endif
10899
protected:
109100
friend class JSCRuntime;
110-
friend class detail::ProtectionQueue;
111101
};
112102

113103
PointerValue* cloneString(const Runtime::PointerValue* pv) override;
@@ -202,10 +192,8 @@ class JSCRuntime : public jsi::Runtime {
202192
void checkException(JSValueRef res, JSValueRef exc, const char* msg);
203193

204194
JSGlobalContextRef ctx_;
195+
std::atomic<bool> ctxInvalid_;
205196
std::string desc_;
206-
// We make this a pointer so that we can control explicitly when it's deleted
207-
// namely before the context is released.
208-
mutable std::unique_ptr<detail::ProtectionQueue> protectionQueue_;
209197
#ifndef NDEBUG
210198
mutable std::atomic<intptr_t> objectCounter_;
211199
mutable std::atomic<intptr_t> stringCounter_;
@@ -293,106 +281,14 @@ std::string to_string(void* value) {
293281
}
294282
} // namespace
295283

296-
// UnprotectQueue
297-
namespace detail {
298-
class ProtectionQueue {
299-
public:
300-
ProtectionQueue()
301-
: ctxInvalid_(false)
302-
, shuttingDown_(false)
303-
#ifndef NDEBUG
304-
,
305-
didShutdown_ {
306-
false
307-
}
308-
#endif
309-
, unprotectorThread_(&ProtectionQueue::unprotectThread, this) {}
310-
311-
void setContextInvalid() {
312-
std::lock_guard<std::mutex> locker(mutex_);
313-
ctxInvalid_ = true;
314-
}
315-
316-
void shutdown() {
317-
{
318-
std::lock_guard<std::mutex> locker(mutex_);
319-
assert(ctxInvalid_);
320-
shuttingDown_ = true;
321-
notEmpty_.notify_one();
322-
}
323-
unprotectorThread_.join();
324-
}
325-
326-
void push(JSCRuntime::JSCObjectValue* value) {
327-
std::lock_guard<std::mutex> locker(mutex_);
328-
assert(!didShutdown_);
329-
queue_.push(value);
330-
notEmpty_.notify_one();
331-
}
332-
333-
private:
334-
// This this the function that runs in the background deleting (and thus
335-
// unprotecting JSObjectRefs as need be). This needs to be explicitly on a
336-
// separate thread so that we don't have the API lock when `JSValueUnprotect`
337-
// is called already (i.e. if we did this on the same thread that calls
338-
// invalidate() on an Object then we might be in the middle of a GC pass, and
339-
// already have the API lock).
340-
void unprotectThread() {
341-
#if defined(__APPLE__)
342-
pthread_setname_np("jsc-protectionqueue-unprotectthread");
343-
#endif
344-
345-
std::unique_lock<std::mutex> locker(mutex_);
346-
while (!shuttingDown_ || !queue_.empty()) {
347-
if (queue_.empty()) {
348-
// This will wake up when shuttingDown_ becomes true
349-
notEmpty_.wait(locker);
350-
} else {
351-
JSCRuntime::JSCObjectValue* value = queue_.front();
352-
queue_.pop();
353-
// We need to drop the lock here since this calls JSValueUnprotect and
354-
// that may make another GC pass, which could call another finalizer
355-
// and thus attempt to push to this queue then, and deadlock.
356-
locker.unlock();
357-
if (ctxInvalid_) {
358-
value->ctx_ = nullptr;
359-
}
360-
value->unprotect();
361-
locker.lock();
362-
}
363-
}
364-
#ifndef NDEBUG
365-
didShutdown_ = true;
366-
#endif
367-
}
368-
// Used to lock the queue_/shuttingDown_ ivars
369-
std::mutex mutex_;
370-
// Used to signal queue_ empty status changing
371-
std::condition_variable notEmpty_;
372-
// The actual underlying queue
373-
std::queue<JSCRuntime::JSCObjectValue*> queue_;
374-
// A flag which is set before shutting down JSC
375-
bool ctxInvalid_;
376-
// A flag dictating whether or not we need to stop all execution
377-
bool shuttingDown_;
378-
#ifndef NDEBUG
379-
bool didShutdown_;
380-
#endif
381-
// The thread that dequeues and processes the queue. Note this is the last
382-
// member on purpose so the thread starts up after all state has been
383-
// properly initialized
384-
std::thread unprotectorThread_;
385-
};
386-
} // namespace detail
387-
388284
JSCRuntime::JSCRuntime()
389285
: JSCRuntime(JSGlobalContextCreateInGroup(nullptr, nullptr)) {
390286
JSGlobalContextRelease(ctx_);
391287
}
392288

393289
JSCRuntime::JSCRuntime(JSGlobalContextRef ctx)
394290
: ctx_(JSGlobalContextRetain(ctx)),
395-
protectionQueue_(std::make_unique<detail::ProtectionQueue>())
291+
ctxInvalid_(false)
396292
#ifndef NDEBUG
397293
,
398294
objectCounter_(0),
@@ -405,15 +301,11 @@ JSCRuntime::~JSCRuntime() {
405301
// On shutting down and cleaning up: when JSC is actually torn down,
406302
// it calls JSC::Heap::lastChanceToFinalize internally which
407303
// finalizes anything left over. But at this point,
408-
// JSUnprotectValue() can no longer be called. So there's a
409-
// multiphase shutdown here. We tell the protection queue that the
410-
// VM is invalid, which causes it not to call JSUnprotectValue. but
411-
// it will decrement its counters, if !NDEBUG. Then we shut down
412-
// the VM, which will clean everything up. Finally, we shut down
413-
// the queue itself.
414-
protectionQueue_->setContextInvalid();
304+
// JSValueUnprotect() can no longer be called. We use an
305+
// atomic<bool> to avoid unsafe unprotects happening after shutdown
306+
// has started.
307+
ctxInvalid_ = true;
415308
JSGlobalContextRelease(ctx_);
416-
protectionQueue_->shutdown();
417309
#ifndef NDEBUG
418310
assert(
419311
objectCounter_ == 0 && "JSCRuntime destroyed with a dangling API object");
@@ -473,15 +365,9 @@ JSCRuntime::JSCStringValue::JSCStringValue(JSStringRef str)
473365
#endif
474366

475367
void JSCRuntime::JSCStringValue::invalidate() {
476-
// We want to immediately JSStringRelease once a String is released,
477-
// and queue a JSObjectRef to unprotected (see comment on
478-
// ProtectionQueue::unprotectThread above).
479-
//
480368
// These JSC{String,Object}Value objects are implicitly owned by the
481369
// {String,Object} objects, thus when a String/Object is destructed
482-
// the JSC{String,Object}Value should be released (again this has
483-
// the caveat that objects must be unprotected on a separate
484-
// thread).
370+
// the JSC{String,Object}Value should be released.
485371
#ifndef NDEBUG
486372
counter_ -= 1;
487373
#endif
@@ -492,16 +378,16 @@ void JSCRuntime::JSCStringValue::invalidate() {
492378

493379
JSCRuntime::JSCObjectValue::JSCObjectValue(
494380
JSGlobalContextRef ctx,
495-
detail::ProtectionQueue& pq,
381+
const std::atomic<bool>& ctxInvalid,
496382
JSObjectRef obj
497383
#ifndef NDEBUG
498384
,
499385
std::atomic<intptr_t>& counter
500386
#endif
501387
)
502388
: ctx_(ctx),
503-
obj_(obj),
504-
protectionQueue_(pq)
389+
ctxInvalid_(ctxInvalid),
390+
obj_(obj)
505391
#ifndef NDEBUG
506392
,
507393
counter_(counter)
@@ -514,16 +400,32 @@ JSCRuntime::JSCObjectValue::JSCObjectValue(
514400
}
515401

516402
void JSCRuntime::JSCObjectValue::invalidate() {
517-
// See comment in JSCRuntime::JSCStringValue::invalidate as well as
518-
// on ProtectionQueue::unprotectThread.
519-
protectionQueue_.push(this);
520-
}
521-
522-
void JSCRuntime::JSCObjectValue::unprotect() {
523403
#ifndef NDEBUG
524404
counter_ -= 1;
525405
#endif
526-
if (ctx_) {
406+
// When shutting down the VM, if there is a HostObject which
407+
// contains or otherwise owns a jsi::Object, then the final GC will
408+
// finalize the HostObject, leading to a call to invalidate(). But
409+
// at that point, making calls to JSValueUnprotect will crash.
410+
// It is up to the application to make sure that any other calls to
411+
// invalidate() happen before VM destruction; see the comment on
412+
// jsi::Runtime.
413+
//
414+
// Another potential concern here is that in the non-shutdown case,
415+
// if a HostObject is GCd, JSValueUnprotect will be called from the
416+
// JSC finalizer. The documentation warns against this: "You must
417+
// not call any function that may cause a garbage collection or an
418+
// allocation of a garbage collected object from within a
419+
// JSObjectFinalizeCallback. This includes all functions that have a
420+
// JSContextRef parameter." However, an audit of the source code for
421+
// JSValueUnprotect in late 2018 shows that it cannot cause
422+
// allocation or a GC, and further, this code has not changed in
423+
// about two years. In the future, we may choose to reintroduce the
424+
// mechanism previously used here which uses a separate thread for
425+
// JSValueUnprotect, in order to conform to the documented API, but
426+
// use the "unsafe" synchronous version on iOS 11 and earlier.
427+
428+
if (!ctxInvalid_) {
527429
JSValueUnprotect(ctx_, obj_);
528430
}
529431
delete this;
@@ -1222,9 +1124,9 @@ jsi::Runtime::PointerValue* JSCRuntime::makeObjectValue(
12221124
objectRef = JSObjectMake(ctx_, nullptr, nullptr);
12231125
}
12241126
#ifndef NDEBUG
1225-
return new JSCObjectValue(ctx_, *protectionQueue_, objectRef, objectCounter_);
1127+
return new JSCObjectValue(ctx_, ctxInvalid_, objectRef, objectCounter_);
12261128
#else
1227-
return new JSCObjectValue(ctx_, *protectionQueue_, objectRef);
1129+
return new JSCObjectValue(ctx_, ctxInvalid_, objectRef);
12281130
#endif
12291131
}
12301132

ReactCommon/jsi/jsi.h

+9-2
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class JSI_EXPORT HostObject {
109109
};
110110

111111
/// Represents a JS runtime. Movable, but not copyable. Note that
112-
/// this object is not thread-aware, but cannot be used safely from
112+
/// this object may not be thread-aware, but cannot be used safely from
113113
/// multiple threads at once. The application is responsible for
114114
/// ensuring that it is used safely. This could mean using the
115115
/// Runtime from a single thread, using a mutex, doing all work on a
@@ -118,7 +118,14 @@ class JSI_EXPORT HostObject {
118118
/// argument. Destructors (all but ~Scope), operators, or other methods
119119
/// which do not take Runtime& as an argument are safe to call from any
120120
/// thread, but it is still forbidden to make write operations on a single
121-
/// instance of any class from more than one thread.
121+
/// instance of any class from more than one thread. In addition, to
122+
/// make shutdown safe, destruction of objects associated with the Runtime
123+
/// must be destroyed before the Runtime is destroyed, or from the
124+
/// destructor of a managed HostObject or HostFunction. Informally, this
125+
/// means that the main source of unsafe behavior is to hold a jsi object
126+
/// in a non-Runtime-managed object, and not clean it up before the Runtime
127+
/// is shut down. If your lifecycle is such that avoiding this is hard,
128+
/// you will probably need to do use your own locks.
122129
class Runtime {
123130
public:
124131
virtual ~Runtime();

0 commit comments

Comments
 (0)