Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ The release run heads these entries with the version and opens a fresh
`pop()` and `depth()` do what they say instead of nothing.
- A directory a document or an archive only implies is one: `exists()` and
`is_directory()` answer for `/` and for a path that files sit under.
- `Odr.load()` works under a Content-Security-Policy without `'unsafe-eval'`:
the wasm module is linked with `-sDYNAMIC_EXECUTION=0`, so embind builds its
invokers without `new Function`. `script-src 'self' 'wasm-unsafe-eval'` is
now enough.

## v6.9.0 - 2026-08-18

Expand Down
5 changes: 5 additions & 0 deletions wasm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ target_link_options(odr_wasm PRIVATE
# failure at 64 KB does not look like a stack overflow.
-sSTACK_SIZE=8388608
-sFILESYSTEM=1
# embind otherwise builds its invokers with `new Function`, which CSP
# treats as eval, so loading needed `script-src 'unsafe-eval'`. The
# closure-based path costs `emscripten_run_script`, `dlopen`, `ccall`
# and `cwrap`, none of which this uses.
-sDYNAMIC_EXECUTION=0
)

# The hand-written half of the package sits beside the generated glue, so the
Expand Down
3 changes: 3 additions & 0 deletions wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ where `Symbol.dispose` is supported.
## Hosting

- Serve `.wasm` as `application/wasm`, or the browser cannot stream-compile it.
- `script-src 'self' 'wasm-unsafe-eval'` is enough to load the module. It is
linked with `-sDYNAMIC_EXECUTION=0`, so embind builds its invokers without
`new Function` and no `'unsafe-eval'` is needed.
- **Enable brotli.** It takes the module from 2.9 M to about 830 K β€” worth more
than every code-size flag put together. Hosts that only gzip land at ~1.2 M.
- No COOP/COEP headers needed. The build is deliberately single-threaded so
Expand Down
36 changes: 36 additions & 0 deletions wasm/tests/csp.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, it } from 'node:test';

import { dist } from './helper.mjs';

// `wasm-unsafe-eval` allows `WebAssembly.instantiate` but not `new Function`,
// which embind uses for its invokers unless linked with
// `-sDYNAMIC_EXECUTION=0`.
describe('content security policy', () => {
it('loads where dynamic code construction is blocked', () => {
// A child process, because the stand-in below replaces a global the test
// runner itself uses.
const script = `
globalThis.Function = new Proxy(Function, {
construct() { throw new EvalError('blocked by the stand-in CSP'); },
apply() { throw new EvalError('blocked by the stand-in CSP'); },
});
const { Odr } = await import(${JSON.stringify(join(dist, 'index.js'))});
await Odr.load();
`;

execFileSync(process.execPath, ['--input-type=module', '-e', script], {
stdio: 'pipe',
});
});

it('ships glue that builds no code at run time', () => {
const glue = readFileSync(join(dist, 'odr-core.mjs'), 'utf8');

assert.doesNotMatch(glue, /new Function\b/);
assert.doesNotMatch(glue, /[^\w.$]eval\(/);
});
});
2 changes: 1 addition & 1 deletion wasm/tests/helper.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const here = dirname(fileURLToPath(import.meta.url));

// `ODR_WASM_DIST` is set by ctest; the fallback is where a by-hand cmake build
// puts it.
const dist = process.env.ODR_WASM_DIST ?? join(here, '..', '..', 'dist');
export const dist = process.env.ODR_WASM_DIST ?? join(here, '..', '..', 'dist');

// A static `export ... from` needs a literal specifier, and the package's
// location is only known at run time, so the module is loaded once up front.
Expand Down
Loading