Skip to content

Commit c411c74

Browse files
authored
Add optional primitive verification of JS library dependencies (#23855)
This is not enabled by default, and perhaps never will be. The changes in this PR are the resulting the running whole test suite with `EMCC_CHECK_DEPS=1` set in the envionment.
1 parent 5fa0954 commit c411c74

32 files changed

+236
-127
lines changed

src/jsifier.mjs

+72-13
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ const addedLibraryItems = {};
4545

4646
const extraLibraryFuncs = [];
4747

48+
// Experimental feature to check for invalid __deps entries.
49+
// See `EMCC_CHECK_DEPS` in in the environment to try it out.
50+
const CHECK_DEPS = process.env.EMCC_CHECK_DEPS;
51+
4852
// Some JS-implemented library functions are proxied to be called on the main
4953
// browser thread, if the Emscripten runtime is executing in a Web Worker.
5054
// Each such proxied function is identified via an ordinal number (this is not
@@ -184,6 +188,60 @@ function preJS() {
184188
return result;
185189
}
186190

191+
// Certain library functions have specific indirect dependencies. See the
192+
// comments alongside eaach of these.
193+
const checkDependenciesSkip = new Set([
194+
'_mmap_js',
195+
'_emscripten_throw_longjmp',
196+
'_emscripten_receive_on_main_thread_js',
197+
'emscripten_start_fetch',
198+
'emscripten_start_wasm_audio_worklet_thread_async',
199+
]);
200+
201+
const checkDependenciesIgnore = new Set([
202+
// These are added in bulk to whole library files are so are not precise
203+
'$PThread',
204+
'$WebGPU',
205+
'$SDL',
206+
'$GLUT',
207+
'$GLEW',
208+
'$Browser',
209+
'$AL',
210+
'$GL',
211+
'$IDBStore',
212+
// These are added purely for their side effects
213+
'$polyfillWaitAsync',
214+
'$GLImmediateSetup',
215+
'$emscriptenGetAudioObject',
216+
// These get conservatively injected via i53ConversionDeps
217+
'$bigintToI53Checked',
218+
'$convertI32PairToI53Checked',
219+
'setTempRet0',
220+
]);
221+
222+
/**
223+
* Hacky attempt to find unused `__deps` entries. This is not enabled by default
224+
* but can be enabled by setting CHECK_DEPS above.
225+
* TODO: Use a more precise method such as tokenising using acorn.
226+
*/
227+
function checkDependencies(symbol, snippet, deps, postset) {
228+
if (checkDependenciesSkip.has(symbol)) {
229+
return;
230+
}
231+
for (const dep of deps) {
232+
if (typeof dep === 'function') {
233+
continue;
234+
}
235+
if (checkDependenciesIgnore.has(dep)) {
236+
continue;
237+
}
238+
const mangled = mangleCSymbolName(dep);
239+
if (!snippet.includes(mangled) && (!postset || !postset.includes(mangled))) {
240+
error(`${symbol}: unused dependency: ${dep}`);
241+
}
242+
}
243+
}
244+
187245
function addImplicitDeps(snippet, deps) {
188246
// There are some common dependencies that we inject automatically by
189247
// conservatively scanning the input functions for their usage.
@@ -576,6 +634,18 @@ function(${args}) {
576634
let isFunction = false;
577635
let aliasTarget;
578636

637+
const postsetId = symbol + '__postset';
638+
const postset = LibraryManager.library[postsetId];
639+
if (postset) {
640+
// A postset is either code to run right now, or some text we should emit.
641+
// If it's code, it may return some text to emit as well.
642+
const postsetString = typeof postset == 'function' ? postset() : postset;
643+
if (postsetString && !addedLibraryItems[postsetId]) {
644+
addedLibraryItems[postsetId] = true;
645+
postSets.push(postsetString + ';');
646+
}
647+
}
648+
579649
if (typeof snippet == 'string') {
580650
if (snippet[0] != '=') {
581651
if (LibraryManager.library[snippet]) {
@@ -594,19 +664,8 @@ function(${args}) {
594664
isFunction = true;
595665
snippet = processLibraryFunction(snippet, symbol, mangled, deps, isStub);
596666
addImplicitDeps(snippet, deps);
597-
}
598-
599-
const postsetId = symbol + '__postset';
600-
let postset = LibraryManager.library[postsetId];
601-
if (postset) {
602-
// A postset is either code to run right now, or some text we should emit.
603-
// If it's code, it may return some text to emit as well.
604-
if (typeof postset == 'function') {
605-
postset = postset();
606-
}
607-
if (postset && !addedLibraryItems[postsetId]) {
608-
addedLibraryItems[postsetId] = true;
609-
postSets.push(postset + ';');
667+
if (CHECK_DEPS && !isUserSymbol) {
668+
checkDependencies(symbol, snippet, deps, postset?.toString());
610669
}
611670
}
612671

src/lib/libaddfunction.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ addToLibrary({
8484
}
8585
},
8686
// Wraps a JS function as a wasm function with a given signature.
87+
#if !WASM2JS
8788
$convertJsFunctionToWasm__deps: ['$uleb128Encode', '$sigToWasmTypes', '$generateFuncType'],
89+
#endif
8890
$convertJsFunctionToWasm: (func, sig) => {
8991
#if WASM2JS
9092
// return func;
@@ -192,8 +194,12 @@ addToLibrary({
192194
$addFunction__docs: '/** @param {string=} sig */',
193195
$addFunction__deps: ['$convertJsFunctionToWasm', '$getFunctionAddress',
194196
'$functionsInTableMap', '$getEmptyTableSlot',
195-
'$getWasmTableEntry', '$setWasmTableEntry',
196-
'$wasmTable'],
197+
'$setWasmTableEntry',
198+
#if ASSERTIONS >= 2
199+
'$getWasmTableEntry', '$wasmTable',
200+
#endif
201+
],
202+
197203
$addFunction: (func, sig) => {
198204
#if ASSERTIONS
199205
assert(typeof func != 'undefined');

src/lib/libautodebug.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ addToLibrary({
1818
$get_i64__deps: ['setTempRet0'],
1919
$get_i64: (loc, index, low, high) => {
2020
dbg('get_i64 ' + [loc, index, low, high]);
21-
setTempRet0(high);
21+
_setTempRet0(high);
2222
return low;
2323
},
2424
$get_f32: (loc, index, value) => {
@@ -52,7 +52,7 @@ addToLibrary({
5252
$set_i64__deps: ['setTempRet0'],
5353
$set_i64: (loc, index, low, high) => {
5454
dbg('set_i64 ' + [loc, index, low, high]);
55-
setTempRet0(high);
55+
_setTempRet0(high);
5656
return low;
5757
},
5858
$set_f32: (loc, index, value) => {
@@ -90,7 +90,7 @@ addToLibrary({
9090
$load_val_i64__deps: ['setTempRet0'],
9191
$load_val_i64: (loc, low, high) => {
9292
dbg('load_val_i64 ' + [loc, low, high]);
93-
setTempRet0(high);
93+
_setTempRet0(high);
9494
return low;
9595
},
9696
$load_val_f32: (loc, value) => {
@@ -112,7 +112,7 @@ addToLibrary({
112112
$store_val_i64__deps: ['setTempRet0'],
113113
$store_val_i64: (loc, low, high) => {
114114
dbg('store_val_i64 ' + [loc, low, high]);
115-
setTempRet0(high);
115+
_setTempRet0(high);
116116
return low;
117117
},
118118
$store_val_f32: (loc, value) => {

src/lib/libccall.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ addToLibrary({
130130
* @param {Array=} argTypes
131131
* @param {Object=} opts
132132
*/`,
133-
$cwrap__deps: ['$getCFunc', '$ccall'],
133+
$cwrap__deps: [ '$ccall',
134+
#if !ASSERTIONS
135+
'$getCFunc',
136+
#endif
137+
],
134138
$cwrap: (ident, returnType, argTypes, opts) => {
135139
#if !ASSERTIONS
136140
// When the function takes numbers and returns a number, we can just return

src/lib/libcore.js

+20-19
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,15 @@ addToLibrary({
202202
},
203203

204204
emscripten_resize_heap__deps: [
205-
'$getHeapMax',
206-
'$alignMemory',
207-
#if ASSERTIONS == 2
208-
'emscripten_get_now',
209-
#endif
210205
#if ABORTING_MALLOC
211206
'$abortOnCannotGrowMemory',
212207
#endif
213208
#if ALLOW_MEMORY_GROWTH
209+
#if ASSERTIONS == 2
210+
'emscripten_get_now',
211+
#endif
212+
'$getHeapMax',
213+
'$alignMemory',
214214
'$growMemory',
215215
#endif
216216
],
@@ -791,7 +791,7 @@ addToLibrary({
791791
return str;
792792
},
793793

794-
$readSockaddr__deps: ['$Sockets', '$inetNtop4', '$inetNtop6', 'ntohs'],
794+
$readSockaddr__deps: ['$inetNtop4', '$inetNtop6', 'ntohs'],
795795
$readSockaddr: (sa, salen) => {
796796
// family / port offsets are common to both sockaddr_in and sockaddr_in6
797797
var family = {{{ makeGetValue('sa', C_STRUCTS.sockaddr_in.sin_family, 'i16') }}};
@@ -825,7 +825,7 @@ addToLibrary({
825825
return { family: family, addr: addr, port: port };
826826
},
827827
$writeSockaddr__docs: '/** @param {number=} addrlen */',
828-
$writeSockaddr__deps: ['$Sockets', '$inetPton4', '$inetPton6', '$zeroMemory', 'htons'],
828+
$writeSockaddr__deps: ['$inetPton4', '$inetPton6', '$zeroMemory', 'htons'],
829829
$writeSockaddr: (sa, family, addr, port, addrlen) => {
830830
switch (family) {
831831
case {{{ cDefs.AF_INET }}}:
@@ -914,7 +914,7 @@ addToLibrary({
914914
return inetPton4(DNS.lookup_name(nameString));
915915
},
916916

917-
getaddrinfo__deps: ['$Sockets', '$DNS', '$inetPton4', '$inetNtop4', '$inetPton6', '$inetNtop6', '$writeSockaddr', 'malloc', 'htonl'],
917+
getaddrinfo__deps: ['$DNS', '$inetPton4', '$inetNtop4', '$inetPton6', '$inetNtop6', '$writeSockaddr', 'malloc', 'htonl'],
918918
getaddrinfo__proxy: 'sync',
919919
getaddrinfo: (node, service, hint, out) => {
920920
// Note getaddrinfo currently only returns a single addrinfo with ai_next defaulting to NULL. When NULL
@@ -1085,7 +1085,7 @@ addToLibrary({
10851085
return 0;
10861086
},
10871087

1088-
getnameinfo__deps: ['$Sockets', '$DNS', '$readSockaddr', '$stringToUTF8'],
1088+
getnameinfo__deps: ['$DNS', '$readSockaddr', '$stringToUTF8'],
10891089
getnameinfo: (sa, salen, node, nodelen, serv, servlen, flags) => {
10901090
var info = readSockaddr(sa, salen);
10911091
if (info.errno) {
@@ -1251,8 +1251,7 @@ addToLibrary({
12511251
// Timers always fire on the main thread, either directly from JS (here) or
12521252
// or when the main thread is busy waiting calling _emscripten_yield.
12531253
_setitimer_js__proxy: 'sync',
1254-
_setitimer_js__deps: ['$timers', '$callUserCallback',
1255-
'_emscripten_timeout', 'emscripten_get_now'],
1254+
_setitimer_js__deps: ['$timers', '$callUserCallback', '_emscripten_timeout', 'emscripten_get_now'],
12561255
_setitimer_js: (which, timeout_ms) => {
12571256
#if RUNTIME_DEBUG
12581257
dbg(`setitimer_js ${which} timeout=${timeout_ms}`);
@@ -1506,12 +1505,7 @@ addToLibrary({
15061505
#endif
15071506

15081507
$readEmAsmArgsArray: [],
1509-
$readEmAsmArgs__deps: [
1510-
'$readEmAsmArgsArray',
1511-
#if MEMORY64
1512-
'$readI53FromI64',
1513-
#endif
1514-
],
1508+
$readEmAsmArgs__deps: ['$readEmAsmArgsArray'],
15151509
$readEmAsmArgs: (sigPtr, buf) => {
15161510
#if ASSERTIONS
15171511
// Nobody should have mutated _readEmAsmArgsArray underneath us to be something else than an array.
@@ -1778,7 +1772,11 @@ addToLibrary({
17781772
#endif
17791773
return f(ptr, ...args);
17801774
},
1781-
$dynCall__deps: ['$dynCallLegacy', '$getWasmTableEntry'],
1775+
#if DYNCALLS
1776+
$dynCall__deps: ['$dynCallLegacy'],
1777+
#else
1778+
$dynCall__deps: ['$getWasmTableEntry'],
1779+
#endif
17821780
#endif
17831781

17841782
// Used in library code to get JS function from wasm function pointer.
@@ -2101,7 +2099,7 @@ addToLibrary({
21012099
#if PTHREADS
21022100
'_emscripten_thread_exit',
21032101
#endif
2104-
#if RUNTIME_DEBUG
2102+
#if RUNTIME_DEBUG >= 2
21052103
'$runtimeKeepaliveCounter',
21062104
#endif
21072105
],
@@ -2159,7 +2157,9 @@ addToLibrary({
21592157

21602158
// Allocate memory for an mmap operation. This allocates space of the right
21612159
// page-aligned size, and clears the allocated space.
2160+
#if hasExportedSymbol('emscripten_builtin_memalign')
21622161
$mmapAlloc__deps: ['$zeroMemory', '$alignMemory'],
2162+
#endif
21632163
$mmapAlloc: (size) => {
21642164
#if hasExportedSymbol('emscripten_builtin_memalign')
21652165
size = alignMemory(size, {{{ WASM_PAGE_SIZE }}});
@@ -2416,6 +2416,7 @@ function wrapSyscallFunction(x, library, isWasi) {
24162416
// has disabled the filesystem or we have proven some other way that this will
24172417
// not be called in practice, and do not need that code.
24182418
if (!SYSCALLS_REQUIRE_FILESYSTEM && t.includes('FS.')) {
2419+
library[x + '__deps'] = [];
24192420
t = modifyJSFunction(t, (args, body) => {
24202421
return `(${args}) => {\n` +
24212422
(ASSERTIONS ? "abort('it should not be possible to operate on streams when !SYSCALLS_REQUIRE_FILESYSTEM');\n" : '') +

src/lib/libdylink.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ var LibraryDylink = {
203203
},
204204

205205
$updateGOT__internal: true,
206-
$updateGOT__deps: ['$GOT', '$isInternalSym', '$addFunction', '$getFunctionAddress'],
206+
$updateGOT__deps: ['$GOT', '$isInternalSym', '$addFunction'],
207207
$updateGOT: (exports, replace) => {
208208
#if DYLINK_DEBUG
209209
dbg("updateGOT: adding " + Object.keys(exports).length + " symbols");
@@ -945,7 +945,7 @@ var LibraryDylink = {
945945
// flags.global and flags.nodelete are handled every time a load request is made.
946946
// Once a library becomes "global" or "nodelete", it cannot be removed or unloaded.
947947
$loadDynamicLibrary__deps: ['$LDSO', '$loadWebAssemblyModule',
948-
'$isInternalSym', '$mergeLibSymbols', '$newDSO',
948+
'$mergeLibSymbols', '$newDSO',
949949
'$asyncLoad',
950950
#if FILESYSTEM
951951
'$preloadedWasm',
@@ -1119,7 +1119,7 @@ var LibraryDylink = {
11191119
},
11201120

11211121
// void* dlopen(const char* filename, int flags);
1122-
$dlopenInternal__deps: ['$ENV', '$dlSetError', '$PATH'],
1122+
$dlopenInternal__deps: ['$dlSetError', '$PATH'],
11231123
$dlopenInternal: (handle, jsflags) => {
11241124
// void *dlopen(const char *file, int mode);
11251125
// http://pubs.opengroup.org/onlinepubs/009695399/functions/dlopen.html

0 commit comments

Comments
 (0)