Skip to content

Build with -s DYNAMIC_EXECUTION=0 so the module works under a CSP without unsafe-eval #91

Description

@ben-polinsky

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    🆕 New

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions