Skip to content

KTX2Loader: bundled basis_transcoder.js requires CSP 'unsafe-eval', but rebuilding with -sDYNAMIC_EXECUTION=0 fixes it #34389

Description

@jjmhalew

Description

The prebuilt examples/jsm/libs/basis/basis_transcoder.js is an embind build with dynamic execution enabled: during embind registration it compiles its invoker functions with the Function constructor. (Grepping the file for new Function
finds nothing as the call is indirect, newFunc(Function, args) inside craftInvokerFunction.)

Under a CSP that allows WebAssembly but not JS eval, e.g. script-src 'self' 'wasm-unsafe-eval', every transcoder worker throws:

EvalError: Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script: "script-src 'self' 'wasm-unsafe-eval'"

once per worker (4× with the default workerLimit), and every KTX2 load fails. The only workaround today is allowing full 'unsafe-eval'. KTX2Loader itself is fine under strict CSP (its worker is covered by worker-src blob:), and the bundled DRACO decoder is fine too; only the Basis transcoder is affected.

Rebuilding the transcoder with -sDYNAMIC_EXECUTION=0 makes embind use plain closures instead, with the same API. I rebuilt from basis_universal v1_50_0_2 with the upstream webgl/transcoder/CMakeLists.txt flags plus:

  • -sDYNAMIC_EXECUTION=0 - the fix; no eval-class constructs remain in the output;
  • -sEXPORTED_RUNTIME_METHODS=HEAP8 - needed with current Emscripten (tested 6.0.8): basis_wrappers.cpp reads Module.HEAP8 via val::module_property, which newer Emscripten no longer exports by default. The current bundled build still carries an explicit Module["HEAP8"]=HEAP8 from its older toolchain.
  • -sINCOMING_MODULE_JS_API=<default list>,wasmBinary: also needed with current Emscripten: the default list dropped wasmBinary, so without this the build ignores the wasmBinary that KTX2Loader passes into the module and instead tries to fetch basis_transcoder.wasm relative to its own URL — a blob: URL inside KTX2Loader's worker, so every worker dies with "Failed to parse URL from basis_transcoder.wasm". (Note the setting replaces the default list, so the defaults — onRuntimeInitialized included — must be repeated alongside wasmBinary.) A Node smoke test does not catch this: Node's fallback finds the .wasm on disk next to the .js. Test in a worker, or with the .js isolated from the .wasm.

Tested against KTX2Loader's call sequence (initializeBasisnew KTX2FilestartTranscodingtranscodeImage) on ETC1S, UASTC, zstd-supercompressed and HDR .ktx2 files: the output is byte-identical to the current bundled build's, at every mip level. Verified in a browser behind script-src 'self' 'wasm-unsafe-eval', loading the transcoder through KTX2Loader's actual blob-worker mechanism; we run this build in production.

Two notes for a drop-in replacement:

  • The current build exports the full HEAP* family plus some embind helpers on Module; with current Emscripten only what's listed in -sEXPORTED_RUNTIME_METHODS is exported. KTX2Loader needs none of them (only the wasm-side wrapper reads HEAP8), but anyone using the file directly might — exporting the old set keeps it fully compatible.
  • Performance (Node 24, x64, median of 40 alternating runs, all mips of a 1500×939 texture): ETC1S transcoding is identical; UASTC+zstd is ~8% slower with my build. The embind call overhead itself is lower with DYNAMIC_EXECUTION=0 in my measurements, so the UASTC delta looks like wasm codegen from the newer toolchain, not the flag — worth rebenchmarking in whatever pipeline normally produces these files.

Reproduction steps

  1. Serve a page using KTX2Loader with the response header
    Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:
  2. Load any .ktx2 texture.
  3. Each worker throws EvalError; the load fails.

Code

const loader = new KTX2Loader().setTranscoderPath("jsm/libs/basis/").detectSupport(renderer);
loader.load("textures/ktx2/2d_etc1s.ktx2", console.log, undefined, console.error);

Live example

Not applicable as this needs a CSP response header, which jsfiddle/codepen can't set. Reproducible locally with any static server that sends the header above.

Screenshots

No response

Version

r185 & dev

Device

Desktop

Browser

Chrome

OS

Windows

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions