Skip to content

Commit bdbf777

Browse files
authored
[WasmFS] Fix a deadlock on ProxyWorker creation (#18229)
It was previously possible for a deadlock to occur when a background thread constructed the OPFS backend then tried to use it. If the main thread was also trying to do a WasmFS operation at the same time, it could have been blocked on a lock held by the background thread and therefore would never have received the request for it to spawn the OPFS dedicated worker. Neither thread can make progress in this situation and the page will become unresponsive. To fix the problem in the simplest possible way, wait for the dedicated worker to start running before returning from the `ProxyWorker` constructor. This guarantees that the worker will already be running for subsequent OPFS backend operations, so they will not be blocked from making progress. Unfortunately this fix also means that trying to create a `ProxyWorker` from the main thread will deadlock, so in particular the OPFS backend can no longer be constructed on the main thread. This restriction should be fine for now because in practice many applications are using -sPROXY_TO_PTHREAD and are already creating their OPFS backends on background threads.
1 parent f172b22 commit bdbf777

File tree

5 files changed

+65
-5
lines changed

5 files changed

+65
-5
lines changed

system/include/emscripten/wasmfs.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,28 @@ typedef backend_t (*backend_constructor_t)(void*);
4242

4343
backend_t wasmfs_create_memory_backend(void);
4444

45+
// Note: this cannot be called on the browser main thread because it might
46+
// deadlock while waiting for its dedicated worker thread to be spawned.
47+
//
48+
// Note: This function blocks on the main browser thread returning to its event
49+
// loop. Calling this function while holding a lock the main thread is waiting
50+
// to acquire will cause a deadlock.
51+
//
52+
// TODO: Add an async version of this function that will work on the main
53+
// thread.
4554
backend_t wasmfs_create_fetch_backend(const char* base_url);
4655

4756
backend_t wasmfs_create_node_backend(const char* root);
4857

58+
// Note: this cannot be called on the browser main thread because it might
59+
// deadlock while waiting for the OPFS dedicated worker thread to be spawned.
60+
//
61+
// Note: This function blocks on the main browser thread returning to its event
62+
// loop. Calling this function while holding a lock the main thread is waiting
63+
// to acquire will cause a deadlock.
64+
//
65+
// TODO: Add an async version of this function that will work on the main
66+
// thread.
4967
backend_t wasmfs_create_opfs_backend(void);
5068

5169
backend_t wasmfs_create_icase_backend(backend_constructor_t create_backend,

system/lib/wasmfs/backends/fetch_backend.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ class FetchBackend : public ProxiedAsyncJSBackend {
8484

8585
extern "C" {
8686
backend_t wasmfs_create_fetch_backend(const char* base_url) {
87+
// ProxyWorker cannot safely be synchronously spawned from the main browser
88+
// thread. See comment in thread_utils.h for more details.
89+
assert(!emscripten_is_main_browser_thread() &&
90+
"Cannot safely create fetch backend on main browser thread");
8791
return wasmFS.addBackend(std::make_unique<FetchBackend>(
8892
base_url ? base_url : "",
8993
[](backend_t backend) { _wasmfs_create_fetch_backend_js(backend); }));

system/lib/wasmfs/backends/opfs_backend.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
// University of Illinois/NCSA Open Source License. Both these licenses can be
44
// found in the LICENSE file.
55

6+
#include <emscripten/threading.h>
7+
#include <stdlib.h>
8+
69
#include "backend.h"
710
#include "file.h"
811
#include "support.h"
912
#include "thread_utils.h"
1013
#include "wasmfs.h"
11-
#include <stdlib.h>
1214

1315
using namespace wasmfs;
1416

@@ -470,6 +472,10 @@ class OPFSBackend : public Backend {
470472
extern "C" {
471473

472474
backend_t wasmfs_create_opfs_backend() {
475+
// ProxyWorker cannot safely be synchronously spawned from the main browser
476+
// thread. See comment in thread_utils.h for more details.
477+
assert(!emscripten_is_main_browser_thread() &&
478+
"Cannot safely create OPFS backend on main browser thread");
473479
return wasmFS.addBackend(std::make_unique<OPFSBackend>());
474480
}
475481

system/lib/wasmfs/thread_utils.h

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <thread>
1212

1313
#include <emscripten/proxying.h>
14+
#include <emscripten/threading.h>
1415

1516
namespace emscripten {
1617

@@ -21,10 +22,22 @@ class ProxyWorker {
2122
ProxyingQueue queue;
2223
std::thread thread;
2324

25+
// Used to notify the calling thread once the worker has been started.
26+
bool started = false;
27+
std::mutex mutex;
28+
std::condition_variable cond;
29+
2430
public:
2531
// Spawn the worker thread.
2632
ProxyWorker()
2733
: queue(), thread([&]() {
34+
// Notify the caller that we have started.
35+
{
36+
std::unique_lock<std::mutex> lock(mutex);
37+
started = true;
38+
}
39+
cond.notify_all();
40+
2841
// Sometimes the main thread is spinning, waiting on a WasmFS lock held
2942
// by a thread trying to proxy work to this dedicated worker. In that
3043
// case, the proxying message won't be relayed by the main thread and
@@ -50,7 +63,27 @@ class ProxyWorker {
5063

5164
// Sit in the event loop performing work as it comes in.
5265
emscripten_exit_with_live_runtime();
53-
}) {}
66+
}) {
67+
68+
// Make sure the thread has actually started before returning. This allows
69+
// subsequent code to assume the thread has already been spawned and not
70+
// worry about potential deadlocks where it holds a lock while proxying an
71+
// operation and meanwhile the main thread is blocked trying to acqure the
72+
// same lock so is never able to spawn the worker thread.
73+
//
74+
// Unfortunately, this solution would cause the main thread to deadlock on
75+
// itself, so for now assert that we are not on the main thread. In the
76+
// future, we could provide an asynchronous version of this utility that
77+
// calls a user callback once the worker has been started. This asynchronous
78+
// version would be safe to use on the main thread.
79+
assert(
80+
!emscripten_is_main_browser_thread() &&
81+
"cannot safely spawn dedicated workers from the main browser thread");
82+
{
83+
std::unique_lock<std::mutex> lock(mutex);
84+
cond.wait(lock, [&]() { return started; });
85+
}
86+
}
5487

5588
// Kill the worker thread.
5689
~ProxyWorker() {

test/test_browser.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5344,8 +5344,6 @@ def test_dlmalloc_3GB(self):
53445344
})
53455345
@requires_threads
53465346
def test_wasmfs_fetch_backend(self, args):
5347-
if is_firefox() and '-sPROXY_TO_PTHREAD' not in args:
5348-
return self.skipTest('ff hangs on the main_thread version. browser bug?')
53495347
create_file('data.dat', 'hello, fetch')
53505348
create_file('small.dat', 'hello')
53515349
create_file('test.txt', 'fetch 2')
@@ -5354,7 +5352,8 @@ def test_wasmfs_fetch_backend(self, args):
53545352
create_file('subdir/backendfile', 'file 1')
53555353
create_file('subdir/backendfile2', 'file 2')
53565354
self.btest_exit(test_file('wasmfs/wasmfs_fetch.c'),
5357-
args=['-sWASMFS', '-sUSE_PTHREADS', '--js-library', test_file('wasmfs/wasmfs_fetch.js')] + args)
5355+
args=['-sWASMFS', '-sUSE_PTHREADS', '-sPROXY_TO_PTHREAD', '-sINITIAL_MEMORY=32MB',
5356+
'--js-library', test_file('wasmfs/wasmfs_fetch.js')] + args)
53585357

53595358
@requires_threads
53605359
@no_firefox('no OPFS support yet')

0 commit comments

Comments
 (0)