Skip to content

Commit 4dac6d6

Browse files
authored
In MODULARIZE mode avoid modifying the incoming moduleArg. NFC (#21775)
This avoids leaking of the partially constructed module and means that only way to get access the module instance is via waiting on the promise. Previously we were attaching all our module properties to the incoming object, but not, as far as I can tell for any good reason. In case anybody was actually depending on this, inject thunks into the moduleArg that abort on access with an actionable error.
1 parent 3f15cfd commit 4dac6d6

9 files changed

+72
-25
lines changed

src/closure-externs/closure-externs.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,8 @@ var moduleArg;
245245
* Used in MODULARIZE mode.
246246
* We need to access this after the code we pass to closure so from closure's
247247
* POV this is "extern".
248-
* @suppress {duplicate}
249248
*/
250-
var readyPromise;
249+
var moduleRtn;
251250

252251
/**
253252
* This was removed from upstream closure compiler in

src/jsifier.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,10 @@ var proxiedFunctionTable = [
744744
includeFile(fileName, shouldPreprocess(fileName));
745745
}
746746

747+
if (MODULARIZE) {
748+
includeFile('postamble_modularize.js');
749+
}
750+
747751
print(
748752
'//FORWARDED_DATA:' +
749753
JSON.stringify({

src/postamble_modularize.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// In MODULARIZE mode we wrap the generated code in a factory function
2+
// and return either the Module itself, or a promise of the module.
3+
//
4+
// We assign to the `moduleRtn` global here and configure closure to see
5+
// this as and extern so it won't get minified.
6+
7+
#if WASM_ASYNC_COMPILATION
8+
9+
#if USE_READY_PROMISE
10+
moduleRtn = readyPromise;
11+
#else
12+
moduleRtn = {};
13+
#endif
14+
15+
#else // WASM_ASYNC_COMPILATION
16+
17+
moduleRtn = Module;
18+
19+
#endif // WASM_ASYNC_COMPILATION
20+
21+
#if ASSERTIONS
22+
// Assertion for attempting to access module properties on the incoming
23+
// moduleArg. In the past we used this object as the prototype of the module
24+
// and assigned properties to it, but now we return a distinct object. This
25+
// keeps the instance private until it is ready (i.e the promise has been
26+
// resolved).
27+
for (const prop of Object.keys(Module)) {
28+
if (!(prop in moduleArg)) {
29+
Object.defineProperty(moduleArg, prop, {
30+
configurable: true,
31+
get() {
32+
#if WASM_ASYNC_COMPILATION
33+
abort(`Access to module property ('${prop}') is no longer possible via the incoming module contructor argument; Instead, use the result of the module promise.`)
34+
#else
35+
abort(`Access to module property ('${prop}') is no longer possible via the module input argument; Instead, use the module constructor return value.`)
36+
#endif
37+
}
38+
});
39+
}
40+
}
41+
#endif

src/shell.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// The Module object: Our interface to the outside world. We import
1111
// and export values on it. There are various ways Module can be used:
1212
// 1. Not defined. We create it here
13-
// 2. A function parameter, function(Module) { ..generated code.. }
13+
// 2. A function parameter, function(moduleArg) => Promise<Module>
1414
// 3. pre-run appended it, var Module = {}; ..generated code..
1515
// 4. External script tag defines var Module.
1616
// We need to check if Module already exists (e.g. case 3 above).
@@ -21,7 +21,7 @@
2121
// before the code. Then that object will be used in the code, and you
2222
// can continue to use Module afterwards as well.
2323
#if MODULARIZE
24-
var Module = moduleArg;
24+
var Module = Object.assign({}, moduleArg);
2525
#elif USE_CLOSURE_COMPILER
2626
// if (!Module)` is crucial for Closure Compiler here as it will otherwise replace every `Module` occurrence with a string
2727
var /** @type {{
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
55580
1+
55579
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
31465
1+
31464
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
54468
1+
54467

test/test_other.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6416,11 +6416,11 @@ def test_modularize_sync_compilation(self):
64166416
console.log('before');
64176417
var result = Module();
64186418
// It should be an object.
6419-
console.log(typeof result);
6419+
console.log('typeof result: ' + typeof result);
64206420
// And it should have the exports that Module has, showing it is Module in fact.
6421-
console.log(typeof result._main);
6421+
console.log('typeof _main: ' + typeof result._main);
64226422
// And it should not be a Promise.
6423-
console.log(typeof result.then);
6423+
console.log('typeof result.then: ' + typeof result.then);
64246424
console.log('after');
64256425
''')
64266426
self.run_process([EMCC, test_file('hello_world.c'),
@@ -6430,12 +6430,25 @@ def test_modularize_sync_compilation(self):
64306430
self.assertContained('''\
64316431
before
64326432
hello, world!
6433-
object
6434-
function
6435-
undefined
6433+
typeof result: object
6434+
typeof _main: function
6435+
typeof result.then: undefined
64366436
after
64376437
''', self.run_js('a.out.js'))
64386438

6439+
def test_modularize_argument_misuse(self):
6440+
create_file('test.c', '''
6441+
#include <emscripten.h>
6442+
EMSCRIPTEN_KEEPALIVE int foo() { return 42; }''')
6443+
6444+
create_file('post.js', r'''
6445+
var arg = { bar: 1 };
6446+
var promise = Module(arg);
6447+
arg._foo();''')
6448+
6449+
expected = "Aborted(Access to module property ('_foo') is no longer possible via the incoming module contructor argument; Instead, use the result of the module promise"
6450+
self.do_runf('test.c', expected, assert_returncode=NON_ZERO, emcc_args=['--no-entry', '-sMODULARIZE', '--extern-post-js=post.js'])
6451+
64396452
def test_export_all_3142(self):
64406453
create_file('src.cpp', r'''
64416454
typedef unsigned int Bit32u;

tools/link.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2350,31 +2350,21 @@ def modularize(options):
23502350
shared.target_environment_may_be('web'):
23512351
async_emit = 'async '
23522352

2353-
# Return the incoming `moduleArg`. This is is equivalent to the `Module` var within the
2354-
# generated code but its not run through closure minification so we can reference it in
2355-
# the return statement.
2356-
return_value = 'moduleArg'
2357-
if settings.WASM_ASYNC_COMPILATION:
2358-
if settings.USE_READY_PROMISE:
2359-
return_value = 'readyPromise'
2360-
else:
2361-
return_value = '{}'
2362-
23632353
# TODO: Remove when https://bugs.webkit.org/show_bug.cgi?id=223533 is resolved.
23642354
if async_emit != '' and settings.EXPORT_NAME == 'config':
23652355
diagnostics.warning('emcc', 'EXPORT_NAME should not be named "config" when targeting Safari')
23662356

23672357
src = '''
23682358
%(maybe_async)sfunction(moduleArg = {}) {
2359+
var moduleRtn;
23692360
23702361
%(src)s
23712362
2372-
return %(return_value)s
2363+
return moduleRtn;
23732364
}
23742365
''' % {
23752366
'maybe_async': async_emit,
23762367
'src': src,
2377-
'return_value': return_value,
23782368
}
23792369

23802370
if settings.MINIMAL_RUNTIME and not settings.PTHREADS:

0 commit comments

Comments
 (0)