Summary
@spz-loader/core's generated glue calls new Function(...). Any Content-Security-Policy that doesn't include 'unsafe-eval' blocks that, so the library can't be used at all in applications with a strict policy.
Building with -s DYNAMIC_EXECUTION=0 removes the calls, with no change to the public API or behavior. I've rebuilt v0.3.1 locally with the flag and verified it — details below.
Where it comes from
packages/core/lib/spz-wasm/build.sh compiles with -lembind and doesn't set DYNAMIC_EXECUTION, so it defaults to 1. With that default, embind builds its argument-marshaling invokers by generating JavaScript source at runtime and compiling it
with new Function. Two such sites survive into the published dist/index.js (names mangled by --closure 1):
new Function(c, s + `}\n`)(...n); // craftInvokerFunction
new Function(Object.keys(e), E)(...Object.values(e)); // emval method caller
Why it matters
script-src 'self' — the ordinary starting point for a strict policy — has no 'unsafe-eval', so loadSpz() fails during initialization:
EvalError: Refused to compile or instantiate JavaScript because 'unsafe-eval'
is not an allowed source of script in the following Content-Security-Policy directive
Relaxing the policy far enough to load the library means granting eval and new Function application-wide, which is what such policies exist to prevent.
Fix
One flag added to the em++ invocation in packages/core/lib/spz-wasm/build.sh:
em++ main.cpp \
-Oz \
--closure 1 \
--llvm-lto 1 \
-fno-exceptions \
--std=c++17 \
-lembind \
--emit-tsd main.d.ts \
-o ./build/main.mjs \
-s USE_ZLIB=1 \
-s WASM=1 \
-s SINGLE_FILE=1 \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-s EXPORT_NAME="spzwasm" \
-s ALLOW_MEMORY_GROWTH \
+ -s DYNAMIC_EXECUTION=0 \
-s EXPORTED_FUNCTIONS="['_malloc', '_free']" \
-s EXPORTED_RUNTIME_METHODS="['HEAPF32', 'HEAPU8']"
Docs: https://emscripten.org/docs/tools_reference/settings_reference.html#dynamic-execution
With it, embind emits its closure-based invoker fallback instead: the same argument marshaling, destructor handling and return conversion, expressed as a closure that iterates the argument-type table at call time rather than being specialized via new Function.
Compatible with the existing -Oz, --closure 1 and --llvm-lto 1 — after the rebuild the output contains no new Function at all, so Closure isn't contributing any of its own.
Validation
I rebuilt the exact v0.3.1 source with only this flag added, using the emscripten/emsdk:5.0.3 image already pinned in packages/core/package.json's build:docker script. The resulting ESM and CommonJS distributions contain neither eval nor new Function. Decoding produced correct output, and succeeded under a policy without 'unsafe-eval' in Chromium, Firefox and WebKit.
Performance
The closure-based fallback can be slower per embind call than the runtime-generated invokers, and that's the fair objection here.
For this library the boundary is crossed roughly once per asset (a single loadSpz()) with all real work happening inside the compiled module, so I'd expect the difference to be unmeasurable in practice. To be straight though: I verified correctness and CSP
behavior, not throughput. I haven't benchmarked it, and I'm happy to produce a comparison if it would help gain confidence in this change.
How to verify
grep -c 'new Function(' packages/core/dist/index.js # expect 0
And decoding should succeed on a page served with a policy that omits 'unsafe-eval'.
Happy to open this as a PR instead if that's easier to review.
Context
Reported from CesiumGS/cesium, which is looking to make its library more ameable to tighter CSPs. These embind new Function calls are a blocker.
Related: CesiumGS/cesium#13617
AI Disclosure
I used AI to help diagnose and prove-out a fix for this issue. AI produced text and code were reviewed/edited.
Summary
@spz-loader/core's generated glue callsnew Function(...). Any Content-Security-Policy that doesn't include'unsafe-eval'blocks that, so the library can't be used at all in applications with a strict policy.Building with
-s DYNAMIC_EXECUTION=0removes the calls, with no change to the public API or behavior. I've rebuilt v0.3.1 locally with the flag and verified it — details below.Where it comes from
packages/core/lib/spz-wasm/build.shcompiles with-lembindand doesn't setDYNAMIC_EXECUTION, so it defaults to1. With that default, embind builds its argument-marshaling invokers by generating JavaScript source at runtime and compiling itwith
new Function. Two such sites survive into the publisheddist/index.js(names mangled by--closure 1):Why it matters
script-src 'self'— the ordinary starting point for a strict policy — has no'unsafe-eval', soloadSpz()fails during initialization:Relaxing the policy far enough to load the library means granting
evalandnew Functionapplication-wide, which is what such policies exist to prevent.Fix
One flag added to the
em++invocation inpackages/core/lib/spz-wasm/build.sh:em++ main.cpp \ -Oz \ --closure 1 \ --llvm-lto 1 \ -fno-exceptions \ --std=c++17 \ -lembind \ --emit-tsd main.d.ts \ -o ./build/main.mjs \ -s USE_ZLIB=1 \ -s WASM=1 \ -s SINGLE_FILE=1 \ -s MODULARIZE=1 \ -s EXPORT_ES6=1 \ -s EXPORT_NAME="spzwasm" \ -s ALLOW_MEMORY_GROWTH \ + -s DYNAMIC_EXECUTION=0 \ -s EXPORTED_FUNCTIONS="['_malloc', '_free']" \ -s EXPORTED_RUNTIME_METHODS="['HEAPF32', 'HEAPU8']"Docs: https://emscripten.org/docs/tools_reference/settings_reference.html#dynamic-execution
With it, embind emits its closure-based invoker fallback instead: the same argument marshaling, destructor handling and return conversion, expressed as a closure that iterates the argument-type table at call time rather than being specialized via
new Function.Compatible with the existing
-Oz,--closure 1and--llvm-lto 1— after the rebuild the output contains nonew Functionat all, so Closure isn't contributing any of its own.Validation
I rebuilt the exact v0.3.1 source with only this flag added, using the
emscripten/emsdk:5.0.3image already pinned inpackages/core/package.json'sbuild:dockerscript. The resulting ESM and CommonJS distributions contain neitherevalnornew Function. Decoding produced correct output, and succeeded under a policy without'unsafe-eval'in Chromium, Firefox and WebKit.Performance
The closure-based fallback can be slower per embind call than the runtime-generated invokers, and that's the fair objection here.
For this library the boundary is crossed roughly once per asset (a single
loadSpz()) with all real work happening inside the compiled module, so I'd expect the difference to be unmeasurable in practice. To be straight though: I verified correctness and CSPbehavior, not throughput. I haven't benchmarked it, and I'm happy to produce a comparison if it would help gain confidence in this change.
How to verify
And decoding should succeed on a page served with a policy that omits
'unsafe-eval'.Happy to open this as a PR instead if that's easier to review.
Context
Reported from CesiumGS/cesium, which is looking to make its library more ameable to tighter CSPs. These embind
new Functioncalls are a blocker.Related: CesiumGS/cesium#13617
AI Disclosure
I used AI to help diagnose and prove-out a fix for this issue. AI produced text and code were reviewed/edited.