Skip to content

Commit cbf4256

Browse files
authored
Add thread checks to emscripten::val (#20344)
In multithreading environment using `emscripten::val` is very error-prone and hard to debug. The reason for that is that, on one hand, `val` represents JavaScript values which cannot be (easily) shared between threads, but, on another, `val` itself is a thin wrapper around an integer handle which can be shared across threads way too easily. As a result, accessing `val` from the wrong thread sometimes succeeds by accident (such as for built-in constants like `undefined`/`false`/`true`), sometimes crashes with invalid handle, and sometimes just quietly returns completely different JS value because a handle with the same ID happened to be valid on both threads, but points to different underlying values. In the last scenario code can even keep going forward for some time, using and making changes on the incorrect JS value before it's apparent that something went very wrong. To make it easier to catch such cases, in this PR I'm adding a `pthread_t` field to the `val` class that stores the owner thread of the given handle. User can still store `val` and send it across threads, but the moment they try to operate on the underlying JS value on the wrong thread, an assertion will fire with a clear message. This check is activated only when code is compiled with pthread support, and only if user didn't disable assertions via `NDEBUG` (we can't use the `-sASSERTIONS` setting here because it's a header not compiled code). This still adds a tiny size overhead if the user is not passing `-DNDEBUG`, but typical build systems (e.g. CMake) pass it automatically in release builds, and overall I believe benefits outweigh the overhead in this scenario. In the future we might also automatically proxy all operations to the correct thread so that `emscripten::val` behaves like any other native object. However, that's out of scope of this particular PR, so for now just laying the groundwork and making those failure scenarios more visible.
1 parent 9ce7020 commit cbf4256

File tree

3 files changed

+85
-49
lines changed

3 files changed

+85
-49
lines changed

ChangeLog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ See docs/process.md for more on how version tagging works.
3131
mirrors the existing `EM_ASM_PTR`. (#20261)
3232
- Emscripten now implements default POSIX signal handlers. These can
3333
terminate or abort the program in response to `raise` cals. (#20257)
34+
- `emscripten::val` now prevents accidental access to the underlying JavaScript
35+
value from threads other than its owner. This already didn't work correctly
36+
in majority of cases, but now it will throw a clear assertion failure. (#20344)
3437

3538
3.1.46 - 09/15/23
3639
-----------------

system/include/emscripten/val.h

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -386,116 +386,119 @@ class val {
386386
explicit val(T&& value) {
387387
using namespace internal;
388388

389-
typedef internal::BindingType<T> BT;
390389
WireTypePack<T> argv(std::forward<T>(value));
391-
handle = _emval_take_value(internal::TypeID<T>::get(), argv);
390+
new (this) val(_emval_take_value(internal::TypeID<T>::get(), argv));
392391
}
393392

394-
val() : handle(EM_VAL(internal::_EMVAL_UNDEFINED)) {}
393+
val() : val(EM_VAL(internal::_EMVAL_UNDEFINED)) {}
395394

396395
explicit val(const char* v)
397-
: handle(internal::_emval_new_cstring(v))
396+
: val(internal::_emval_new_cstring(v))
398397
{}
399398

400-
val(val&& v) : handle(v.handle) {
399+
// Note: unlike other constructors, this doesn't use as_handle() because
400+
// it just moves a value and doesn't need to go via incref/decref.
401+
// This means it's safe to move values across threads - an error will
402+
// only arise if you access or free it from the wrong thread later.
403+
val(val&& v) : handle(v.handle), thread(v.thread) {
401404
v.handle = 0;
402405
}
403406

404-
val(const val& v) : handle(v.handle) {
407+
val(const val& v) : val(v.as_handle()) {
405408
internal::_emval_incref(handle);
406409
}
407410

408411
~val() {
409-
internal::_emval_decref(handle);
412+
if (EM_VAL handle = as_handle()) {
413+
internal::_emval_decref(handle);
414+
handle = 0;
415+
}
410416
}
411417

412418
EM_VAL as_handle() const {
419+
#ifdef _REENTRANT
420+
assert(pthread_equal(thread, pthread_self()) && "val accessed from wrong thread");
421+
#endif
413422
return handle;
414423
}
415424

416425
val& operator=(val&& v) & {
417-
auto v_handle = v.handle;
418-
v.handle = 0;
419-
if (handle) {
420-
internal::_emval_decref(handle);
421-
}
422-
handle = v_handle;
426+
val tmp(std::move(v));
427+
this->~val();
428+
new (this) val(std::move(tmp));
423429
return *this;
424430
}
425431

426432
val& operator=(const val& v) & {
427-
internal::_emval_incref(v.handle);
428-
internal::_emval_decref(handle);
429-
handle = v.handle;
430-
return *this;
433+
return *this = val(v);
431434
}
432435

433436
bool hasOwnProperty(const char* key) const {
434437
return val::global("Object")["prototype"]["hasOwnProperty"].call<bool>("call", *this, val(key));
435438
}
436439

437440
bool isNull() const {
438-
return handle == EM_VAL(internal::_EMVAL_NULL);
441+
return as_handle() == EM_VAL(internal::_EMVAL_NULL);
439442
}
440443

441444
bool isUndefined() const {
442-
return handle == EM_VAL(internal::_EMVAL_UNDEFINED);
445+
return as_handle() == EM_VAL(internal::_EMVAL_UNDEFINED);
443446
}
444447

445448
bool isTrue() const {
446-
return handle == EM_VAL(internal::_EMVAL_TRUE);
449+
return as_handle() == EM_VAL(internal::_EMVAL_TRUE);
447450
}
448451

449452
bool isFalse() const {
450-
return handle == EM_VAL(internal::_EMVAL_FALSE);
453+
return as_handle() == EM_VAL(internal::_EMVAL_FALSE);
451454
}
452455

453456
bool isNumber() const {
454-
return internal::_emval_is_number(handle);
457+
return internal::_emval_is_number(as_handle());
455458
}
456459

457460
bool isString() const {
458-
return internal::_emval_is_string(handle);
461+
return internal::_emval_is_string(as_handle());
459462
}
460463

461464
bool isArray() const {
462465
return instanceof(global("Array"));
463466
}
464467

465468
bool equals(const val& v) const {
466-
return internal::_emval_equals(handle, v.handle);
469+
return internal::_emval_equals(as_handle(), v.as_handle());
467470
}
468471

469472
bool operator==(const val& v) const {
470-
return internal::_emval_equals(handle, v.handle);
473+
return internal::_emval_equals(as_handle(), v.as_handle());
471474
}
472475

473476
bool operator!=(const val& v) const {
474477
return !(*this == v);
475478
}
476479

477480
bool strictlyEquals(const val& v) const {
478-
return internal::_emval_strictly_equals(handle, v.handle);
481+
return internal::_emval_strictly_equals(as_handle(), v.as_handle());
479482
}
480483

481484
bool operator>(const val& v) const {
482-
return internal::_emval_greater_than(handle, v.handle);
485+
return internal::_emval_greater_than(as_handle(), v.as_handle());
483486
}
484487

485488
bool operator>=(const val& v) const {
486489
return (*this > v) || (*this == v);
487490
}
488491

489492
bool operator<(const val& v) const {
490-
return internal::_emval_less_than(handle, v.handle);
493+
return internal::_emval_less_than(as_handle(), v.as_handle());
491494
}
492495

493496
bool operator<=(const val& v) const {
494497
return (*this < v) || (*this == v);
495498
}
496499

497500
bool operator!() const {
498-
return internal::_emval_not(handle);
501+
return internal::_emval_not(as_handle());
499502
}
500503

501504
template<typename... Args>
@@ -505,17 +508,17 @@ class val {
505508

506509
template<typename T>
507510
val operator[](const T& key) const {
508-
return val(internal::_emval_get_property(handle, val_ref(key).handle));
511+
return val(internal::_emval_get_property(as_handle(), val_ref(key).as_handle()));
509512
}
510513

511514
template<typename K, typename V>
512515
void set(const K& key, const V& value) {
513-
internal::_emval_set_property(handle, val_ref(key).handle, val_ref(value).handle);
516+
internal::_emval_set_property(as_handle(), val_ref(key).as_handle(), val_ref(value).as_handle());
514517
}
515518

516519
template<typename T>
517520
bool delete_(const T& property) const {
518-
return internal::_emval_delete(handle, val_ref(property).handle);
521+
return internal::_emval_delete(as_handle(), val_ref(property).as_handle());
519522
}
520523

521524
template<typename... Args>
@@ -527,7 +530,7 @@ class val {
527530
ReturnValue call(const char* name, Args&&... args) const {
528531
using namespace internal;
529532

530-
return MethodCaller<ReturnValue, Args...>::call(handle, name, std::forward<Args>(args)...);
533+
return MethodCaller<ReturnValue, Args...>::call(as_handle(), name, std::forward<Args>(args)...);
531534
}
532535

533536
template<typename T, typename ...Policies>
@@ -539,7 +542,7 @@ class val {
539542

540543
EM_DESTRUCTORS destructors;
541544
EM_GENERIC_WIRE_TYPE result = _emval_as(
542-
handle,
545+
as_handle(),
543546
targetType.getTypes()[0],
544547
&destructors);
545548
DestructorsRunner dr(destructors);
@@ -553,7 +556,7 @@ class val {
553556
typedef BindingType<int64_t> BT;
554557
typename WithPolicies<>::template ArgTypeList<int64_t> targetType;
555558

556-
return _emval_as_int64(handle, targetType.getTypes()[0]);
559+
return _emval_as_int64(as_handle(), targetType.getTypes()[0]);
557560
}
558561

559562
template<>
@@ -563,41 +566,41 @@ class val {
563566
typedef BindingType<uint64_t> BT;
564567
typename WithPolicies<>::template ArgTypeList<uint64_t> targetType;
565568

566-
return _emval_as_uint64(handle, targetType.getTypes()[0]);
569+
return _emval_as_uint64(as_handle(), targetType.getTypes()[0]);
567570
}
568571

569572
// If code is not being compiled with GNU extensions enabled, typeof() is not a reserved keyword, so support that as a member function.
570573
#if __STRICT_ANSI__
571574
val typeof() const {
572-
return val(internal::_emval_typeof(handle));
575+
return val(internal::_emval_typeof(as_handle()));
573576
}
574577
#endif
575578

576579
// Prefer calling val::typeOf() over val::typeof(), since this form works in both C++11 and GNU++11 build modes. "typeof" is a reserved word in GNU++11 extensions.
577580
val typeOf() const {
578-
return val(internal::_emval_typeof(handle));
581+
return val(internal::_emval_typeof(as_handle()));
579582
}
580583

581584
bool instanceof(const val& v) const {
582-
return internal::_emval_instanceof(handle, v.handle);
585+
return internal::_emval_instanceof(as_handle(), v.as_handle());
583586
}
584587

585588
bool in(const val& v) const {
586-
return internal::_emval_in(handle, v.handle);
589+
return internal::_emval_in(as_handle(), v.as_handle());
587590
}
588591

589592
[[noreturn]] void throw_() const {
590-
internal::_emval_throw(handle);
593+
internal::_emval_throw(as_handle());
591594
}
592595

593596
val await() const {
594-
return val(internal::_emval_await(handle));
597+
return val(internal::_emval_await(as_handle()));
595598
}
596599

597600
private:
598-
// takes ownership, assumes handle already incref'd
601+
// takes ownership, assumes handle already incref'd and lives on the same thread
599602
explicit val(EM_VAL handle)
600-
: handle(handle)
603+
: handle(handle), thread(pthread_self())
601604
{}
602605

603606
template<typename WrapperType>
@@ -609,7 +612,7 @@ class val {
609612

610613
WithPolicies<>::ArgTypeList<Args...> argList;
611614
WireTypePack<Args...> argv(std::forward<Args>(args)...);
612-
return val(impl(handle, argList.getCount(), argList.getTypes(), argv));
615+
return val(impl(as_handle(), argList.getCount(), argList.getTypes(), argv));
613616
}
614617

615618
template<typename T>
@@ -621,6 +624,7 @@ class val {
621624
return v;
622625
}
623626

627+
pthread_t thread;
624628
EM_VAL handle;
625629

626630
friend struct internal::BindingType<val>;
@@ -640,8 +644,9 @@ struct BindingType<T, typename std::enable_if<std::is_base_of<val, T>::value &&
640644
!std::is_const<T>::value>::type> {
641645
typedef EM_VAL WireType;
642646
static WireType toWireType(const val& v) {
643-
_emval_incref(v.handle);
644-
return v.handle;
647+
EM_VAL handle = v.as_handle();
648+
_emval_incref(handle);
649+
return handle;
645650
}
646651
static val fromWireType(WireType v) {
647652
return val::take_ownership(v);
@@ -652,11 +657,11 @@ struct BindingType<T, typename std::enable_if<std::is_base_of<val, T>::value &&
652657

653658
template <typename T, typename... Policies>
654659
std::vector<T> vecFromJSArray(const val& v, Policies... policies) {
655-
const size_t l = v["length"].as<size_t>();
660+
const uint32_t l = v["length"].as<uint32_t>();
656661

657662
std::vector<T> rv;
658663
rv.reserve(l);
659-
for (size_t i = 0; i < l; ++i) {
664+
for (uint32_t i = 0; i < l; ++i) {
660665
rv.push_back(v[i].as<T>(std::forward<Policies>(policies)...));
661666
}
662667

test/test_core.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7776,6 +7776,34 @@ def test_embind_val_assignment(self):
77767776
err = self.expect_fail([EMCC, test_file('embind/test_val_assignment.cpp'), '-lembind', '-c'])
77777777
self.assertContained('candidate function not viable: expects an lvalue for object argument', err)
77787778

7779+
@node_pthreads
7780+
def test_embind_val_cross_thread(self):
7781+
self.emcc_args += ['--bind']
7782+
self.setup_node_pthreads()
7783+
create_file('test_embind_val_cross_thread.cpp', r'''
7784+
#include <emscripten.h>
7785+
#include <emscripten/val.h>
7786+
#include <thread>
7787+
#include <stdio.h>
7788+
7789+
using emscripten::val;
7790+
7791+
int main(int argc, char **argv) {
7792+
// Store a value handle from the main thread.
7793+
val value(0);
7794+
std::thread([&] {
7795+
// Set to a value handle from a different thread.
7796+
value = val(1);
7797+
}).join();
7798+
// Try to access the stored handle from the main thread.
7799+
// Without the check (if compiled with -DNDEBUG) this will incorrectly
7800+
// print "0" instead of "1" since the handle with the same ID
7801+
// resolves to different values on different threads.
7802+
printf("%d\n", value.as<int>());
7803+
}
7804+
''')
7805+
self.do_runf('test_embind_val_cross_thread.cpp', 'val accessed from wrong thread', assert_returncode=NON_ZERO)
7806+
77797807
def test_embind_dynamic_initialization(self):
77807808
self.emcc_args += ['-lembind']
77817809
self.do_run_in_out_file_test('embind/test_dynamic_initialization.cpp')

0 commit comments

Comments
 (0)