The runtime EvalError is the definitive signal that the Manifest V3 (MV3) security model has failed you. Developers migrating to Manifest V3 are often promised that wasm-unsafe-eval restores WebAssembly functionality, yet high-performance tools like magick-wasm continue to throw EvalError at runtime. This isn’t a simple configuration oversight. It is the direct result of a fundamental re-engineering of the browser’s internal security architecture.
Official documentation describes a progression toward safety and performance. Forensic traces in internal bug trackers reveal a messier reality: a “Shadow CSP” that governs isolated worlds and an undocumented “5-Minute Kill Switch” that abruptly severs service worker connections. The primitives introduced to replace legacy features possess critical failure modes that the platform’s marketing fails to address. These boundaries collapse when high-performance extensions attempt to handle complex object graphs or dynamic execution. You aren’t just losing legacy features; you are losing the ability to predict how code will behave once it leaves your development environment. Reliability is being traded for an opaque enforcement layer that the manifest cannot fully override.
MV3 is a fundamental shift that sacrifices execution reliability for an opaque security model, creating a platform that is safe on paper but prone to critical leaks under runtime pressure
The JIT Boundary: Why ‘wasm-unsafe-eval’ is a Semantic Decoy
Stop treating wasm-unsafe-eval as a 1:1 replacement for Manifest V2’s unsafe-eval. It is not. You are navigating a “Secure Airship” where the hull is technically intact on paper but functionally porous under the pressure of runtime execution. This directive is a semantic decoy.
This directive is a semantic decoy. It protects the binary path while leaving the JS interoperability layer completely broken.
The technical disconnect lies in the granular scoping of the execution path within the browser core. Forensic tracing of the execution path within the V8 engine and the Blink renderer indicates that this directive is strictly scoped to Wasm bytecode compilation and does not extend to the JavaScript-based ‘glue code’ or Just-In-Time (JIT) optimizations required by modern Wasm toolchains.
The browser recognizes the .wasm binary as a privileged guest, but it treats the mandatory JavaScript wrappers as hostile intruders. Modern Wasm toolchains do not produce isolated binaries; they generate complex, dynamic JS-based “glue” to manage memory, imports, and exports. Under the Manifest V3 CSP, the V8 engine permits the WebAssembly.instantiate call for the bytecode itself while simultaneously triggering an EvalError for the dynamic JavaScript stubs required for interop.
The security model allows the engine room to run but locks the doors to the cockpit. You are left with a valid, compiled Wasm module that is functionally paralyzed. It cannot interface with the DOM or the extension’s background context because the JIT-optimized bridge is severed. The Blink renderer enforces this through a whitelist that specifically excludes the dynamic evaluation of script strings, even when those strings are essential for the Wasm-to-JS interoperability layer.
The Emscripten Trap: DYNAMIC_EXECUTION and EvalErrors
You hit the wall the moment you attempt to ship high-performance image processing like magick-wasm. The build finishes. The manifest validates. Then, the runtime crashes with a definitive EvalError. This failure is not a bug in your code; it is a structural leak in the “Secure Airship” of Manifest V3.
This is a hard boundary where the MV3 security model prioritizes the elimination of dynamic JS execution over the functional requirements of Wasm-JS bridging.
The conflict resides in how toolchains bridge the gap between binary logic and the browser environment. Tools like Emscripten frequently utilize the Function constructor or eval() to generate optimized wrapper functions for Embind or to handle relocatable symbols. While ‘wasm-unsafe-eval’ permits WebAssembly.instantiate() and WebAssembly.compile(), it does not authorize the underlying JavaScript engine to perform string-to-code conversions.
By default, Emscripten operates with:
-s DYNAMIC_EXECUTION=1This default is fatal in the MV3 environment. It relies on the exact dynamic execution patterns that the Blink renderer is now hard-coded to block. Relocatable builds—the industry standard for modular, high-performance applications—are effectively dead in this ecosystem. They require the runtime to resolve symbols using logic that the MV3 CSP classifies as a security violation.
To bypass the crash, you can force the toolchain to avoid dynamic code generation:
-s DYNAMIC_EXECUTION=0But this is a pyrrhic victory. Setting this flag strips away the performance optimizations and features that justify using WebAssembly in the first place. You are left with a neutered binary and a broken bridge. The security model has achieved its goal of “safety” by rendering the interoperability layer non-functional.
The Sandbox Bridge: Structured Clone Latency and the 1MB Ceiling
“My extension feels sluggish since the migration.” You hear it constantly. This isn’t a coding error; it’s the architectural tax of the Manifest V3 Sandbox Bridge. You are operating within a “Secure Airship” where the structural integrity is theoretically impenetrable, yet the hull buckles under the pressure of actual data transfer.
The Structured Clone Algorithm is the platform’s mechanism for transferring complex JavaScript objects across execution contexts. In MV3, it replaces the shared-memory benefits of persistent pages with an $O(n)$ deep-copy requirement that scales poorly with data volume.
The efficiency of Manifest V2 relied on persistent background pages that allowed for direct memory sharing. That is gone. Forensic benchmarks in high-load environments indicate that when datasets approach or exceed 1MB, the serialization latency becomes a significant bottleneck. Unlike the highly efficient memory sharing available in MV2’s persistent background pages, the MV3 sandbox requires a complete deep copy of the object graph for every message.
When your extension handles large-scale data transfers—specifically payloads exceeding 1MB—the latency incurred by the Structured Clone Algorithm degrades the user experience and threatens process stability. The “breaking point” is rarely a hard memory limit. Instead, it is the cumulative overhead of serialization combined with the memory pressure it exerts on the renderer process. This creates a “latency trap” where serialization time scales linearly with object complexity. Your high-performance Wasm logic might execute quickly, but the cost of moving data across the bridge nullifies those gains. You are fighting a platform that prioritizes isolation over throughput.
This architectural friction is merely the preliminary hurdle before the volatility of the parent context takes hold.
The 5-Minute Kill Switch: Ephemeral Service Worker Failures
The ‘5-Minute Kill Switch’ is the single greatest threat to computational reliability in MV3. If your Wasm module takes 301 seconds to process a local file, the platform will terminate the bridge, leaving the user with a broken UI and a lost state
Your background process is dying mid-execution. This is not a crash or a memory leak. It is a scheduled execution. In the “Secure Airship” of Manifest V3, the life support systems operate on a timer you cannot override.
The Chromium lifecycle is merciless. ServiceWorker-backed extensions shut down the Native Messaging Host after 5 minutes [Chromium Issue 40755584]. This 300-second limit is a hard architectural constraint of the Chromium Service Worker lifecycle, regardless of whether the sandboxed iframe is still actively processing data.
This creates a fatal architectural disconnect. While your Wasm module is deep in a heavy computational task within the sandboxed iframe, the parent Service Worker—the only entity capable of relaying that result back to the UI—is terminated by the browser engine. The sandbox becomes an orphaned process. It continues to consume CPU cycles on a result that can no longer be delivered because the communication bridge is severed.
WebAssembly modules often maintain complex, ephemeral states in linear memory. These states are not easily serialized. Consequently, this termination is catastrophic. You cannot simply “wake up” the Service Worker and resume execution. The memory bridge is gone. The state is purged. Any data not hardened to indexedDB or persistent storage before the 300-second mark is effectively deleted. You are building on a foundation designed to disappear.
This volatility is an intended feature of the resource-constrained model, yet the lack of engine parity remains the final hurdle.
Cross-Browser Friction: Blink vs. Gecko Disparities
Your manifest validates in Chrome. You load the exact same package into Firefox, and it fails instantly. This is the structural collapse of the Manifest V3 “Secure Airship.” While the schema appears unified, the engine-level enforcement is a fractured landscape of divergent interpretations.
In Manifest V3, ‘cross-browser compatibility’ is a misnomer. While the manifest schema is unified, the CSP validators in Blink and Gecko are functionally distinct, often resulting in silent failures when moving from Chrome-based development to Firefox testing.
The friction is most visible in how engines handle the transition to local development and Wasm support. Chrome 103 integrated wasm-unsafe-eval as a core pillar of its MV3 strategy. Firefox adopted a more cautious, opt-in timeline, creating a versioning gap that forces developers to maintain engine-specific manifest forks.
The disparity extends to basic protocol handling. Firefox, adhering to a stricter interpretation of the ‘no remote code’ rule, initially threw a ‘forbidden http: protocol source’ error for localhost sources, even in development mode. This was tracked in Firefox Bug 1864284, revealing that the ‘minimum CSP’ is not a standard but a browser-specific implementation detail that can block entire development ecosystems. Blink prioritizes a pragmatic approach to local testing; Gecko prioritizes the absolute exclusion of non-secure protocols, regardless of the developer’s intent. You are not writing for one platform. You are writing for two distinct gatekeepers that disagree on the definition of “safe.” This divergence turns the promise of a unified extension API into a series of silent, engine-specific blocks.
These visible disparities are only the surface. The true danger lies in how these engines handle their hidden enforcement layers.
Shadow CSP: The Hidden Enforcement in Isolated Worlds
You encounter the “Refused to execute script” error in the console. You check your manifest. The policy is valid. The logic is sound. Yet, the browser kills the execution. This is the structural collapse of your “Secure Airship.” While the outer hull—your manifest—looks intact, a hidden interior valve is venting your permissions.
In Manifest V3, the manifest is often merely a “suggestion” of security. Runtime behavior reveals a “Shadow” layer—an underlying, immutable baseline policy acting as a second, higher-priority filter that overrides your declarations.
This is the Shadow CSP. It represents a clinical reality where manifest-defined policies are systematically ignored within content scripts. The browser does not care what you declared in your JSON. Content scripts operate in an ‘Isolated World’ where the CSP is hardcoded to a default value that cannot be relaxed by the developer.
Every execution request triggers a Dual-CSP Violation check. The Blink engine first evaluates your code against your extension’s manifest-defined policy. If it passes, it immediately subjects the request to an immutable baseline policy hardcoded into the browser core. This secondary filter is invisible and absolute. It ensures that the “Isolated World” remains a restricted environment, regardless of your attempts to use dynamic logic or external dependencies. You are no longer the architect of your extension’s security; you are a tenant in a space governed by hardcoded defaults. The manifest has become a suggestion rather than a rule.
The Final Verdict: High-Performance Architecture in 2026
The “Secure Airship” of Manifest V3 has launched. Its hull is technically sound but fundamentally porous under the pressure of runtime execution. You cannot patch these leaks with legacy workarounds. You must rebuild.
Audit your Wasm toolchain for -s DYNAMIC_EXECUTION=0 and evaluate Offscreen Documents as a potential (though limited) alternative to the Sandbox Bridge.
Success in 2026 requires a strategic retreat from the hybrid Wasm-JS interoperability that defined the previous decade. The browser engine now treats dynamic JavaScript stubs as hostile intruders. Move toward “pure” Wasm implementations that function independently of dynamic JS logic. This shift is mandatory. If your architecture still relies on runtime eval() or Function constructors for its bridge, it is obsolete.
Transition your heavy computational tasks to Offscreen Documents. This API replaces the brittle, high-latency Sandbox Bridge with a more stable environment for complex logic. It is your only path to maintaining functional reliability when the Service Worker lifecycle threatens to sever your execution mid-task. Hardening your state against sudden termination is no longer optional.
Accept the clinical reality of the new ecosystem. Manifest V3 is not a simple upgrade; it is a fundamental shift that sacrifices execution reliability for a more opaque, engine-enforced security model. Align your architecture with the engine’s hardcoded constraints or prepare for terminal runtime failure.
FAQs
WebAssembly.instantiate() and WebAssembly.compile(). Standard JavaScript eval() and new Function() remain strictly blocked. extension_pages settings. Related Citations & References
- GIIs it possible avoid binding js to use unsafe-eval syntax? · Issue #20994 · emscripten-core/emscripten · GitHub
- GISupport Web Extension Manifest V3 · Issue #203 · dlemstra/magick-wasm · GitHub
- BUDetail
- BU1766027 – Decide on including 'wasm-unsafe-eval' in the default CSP of MV3 extensions
- ISChromium
- DELlms Full.Txt
- GIopen-webui/CHANGELOG.md at main · open-webui/open-webui · GitHub
- BUMy App
- ISChromium
- GIInconsistency: Default `content_security_policy` · Issue #98 · w3c/webextensions · GitHub
- DEManifest – Content Security Policy | Chrome Extensions | Chrome for Developers
- BU1864284 – Allow localhost in MV3 CSP to allow connecting to local dev servers
- ISChromium




