Skip to content
File

Blob: src/workerd/io/tracked-wasm-instance-test.js

javascript266 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4// Tests for the WASM shutdown signal registration shim.
5//
6// These tests verify that the shimWebAssemblyInstantiate() code in worker.c++ correctly
7// detects __instance_signal / __instance_terminated exports, handles various memory
8// configurations, and rejects out-of-bounds addresses.
9 
10import basicModule from 'signal-basic.wasm';
11import partialModule from 'signal-partial-exports.wasm';
12import terminatedOnlyModule from 'signal-terminated-only.wasm';
13import noGlobalsModule from 'signal-no-globals.wasm';
14import overflowModule from 'signal-bounds-check-overflow.wasm';
15import edgeModule from 'signal-bounds-check-edge.wasm';
16import validModule from 'signal-bounds-check-valid.wasm';
17import decoyModule from 'signal-decoy-memory.wasm';
18import externrefMemoryModule from 'signal-externref-memory.wasm';
19import importedMemoryModule from 'signal-imported-memory.wasm';
20import reclaimModule from 'signal-memory-reclaim.wasm';
21import preinitModule from 'signal-preinit.wasm';
22 
23// ---------------------------------------------------------------------------
24// Export permutation tests
25//
26// At least one of __instance_terminated or __instance_signal must be present for registration.
27// The four permutations:
28// 1. Both present → registers (signal + terminated)
29// 2. Only terminated → registers (terminated only)
30// 3. Only signal → registers (signal only, GC-based cleanup)
31// 4. Neither → NOT registered
32// ---------------------------------------------------------------------------
33 
34// Permutation 1: both __instance_signal and __instance_terminated present.
35// The module should be registered and both addresses are functional.
36export let bothGlobalsRegisters = {
37 async test() {
38 const instance = await WebAssembly.instantiate(basicModule);
39 // Registration should zero the signal field.
40 if (instance.exports.get_signal() !== 0) {
41 throw new Error('Expected signal to be 0 initially');
42 }
43 },
44};
45 
46// Permutation 1 (sync): same test via the sync WebAssembly.Instance constructor.
47export let syncBothGlobalsRegisters = {
48 test() {
49 const instance = new WebAssembly.Instance(basicModule);
50 if (instance.exports.get_signal() !== 0) {
51 throw new Error('Expected signal to be 0 initially');
52 }
53 },
54};
55 
56// Permutation 2: only __instance_terminated present (no __instance_signal).
57// The module should be registered — __instance_signal is optional.
58export let terminatedOnlyRegisters = {
59 async test() {
60 const instance = await WebAssembly.instantiate(terminatedOnlyModule);
61 if (instance.exports.get_terminated() !== 0) {
62 throw new Error('Expected terminated to be 0 initially');
63 }
64 },
65};
66 
67// Permutation 2 (sync): same test via the sync WebAssembly.Instance constructor.
68export let syncTerminatedOnlyRegisters = {
69 test() {
70 const instance = new WebAssembly.Instance(terminatedOnlyModule);
71 if (instance.exports.get_terminated() !== 0) {
72 throw new Error('Expected terminated to be 0 initially');
73 }
74 },
75};
76 
77// Permutation 3: only __instance_signal present (no __instance_terminated).
78// The module should be registered — either signal is sufficient.
79export let signalOnlyRegisters = {
80 async test() {
81 const instance = await WebAssembly.instantiate(partialModule);
82 // Registration should zero the signal field.
83 if (instance.exports.get_signal() !== 0) {
84 throw new Error('Expected signal to be 0 initially');
85 }
86 },
87};
88 
89// Permutation 3 (sync): same test via the sync WebAssembly.Instance constructor.
90export let syncSignalOnlyRegisters = {
91 test() {
92 const instance = new WebAssembly.Instance(partialModule);
93 // Registration should zero the signal field.
94 if (instance.exports.get_signal() !== 0) {
95 throw new Error('Expected signal to be 0 initially');
96 }
97 },
98};
99 
100// Permutation 4: neither __instance_signal nor __instance_terminated present.
101// The module should NOT be registered and should instantiate without error.
102export let noGlobalsSkipped = {
103 async test() {
104 const instance = await WebAssembly.instantiate(noGlobalsModule);
105 // Module has a simple add function — verify it works.
106 if (instance.exports.add(2, 3) !== 5) {
107 throw new Error('Expected add(2, 3) to return 5');
108 }
109 },
110};
111 
112// Permutation 4 (sync): same test via the sync WebAssembly.Instance constructor.
113export let syncNoGlobalsSkipped = {
114 test() {
115 const instance = new WebAssembly.Instance(noGlobalsModule);
116 if (instance.exports.add(2, 3) !== 5) {
117 throw new Error('Expected add(2, 3) to return 5');
118 }
119 },
120};
121 
122// Memory at the signal address is pre-initialized to 0xDEADBEEF via a data segment.
123// Registration should zero the signal field.
124export let registrationZerosPreinitMemory = {
125 async test() {
126 const instance = await WebAssembly.instantiate(preinitModule);
127 if (instance.exports.get_signal() !== 0) {
128 throw new Error(
129 'Expected signal to be zeroed, got ' + instance.exports.get_signal()
130 );
131 }
132 },
133};
134 
135// Same test via the sync WebAssembly.Instance constructor.
136export let syncRegistrationZerosPreinitMemory = {
137 test() {
138 const instance = new WebAssembly.Instance(preinitModule);
139 if (instance.exports.get_signal() !== 0) {
140 throw new Error(
141 'Expected signal to be zeroed, got ' + instance.exports.get_signal()
142 );
143 }
144 },
145};
146 
147// ---------------------------------------------------------------------------
148// Bounds checking tests
149// ---------------------------------------------------------------------------
150 
151// __instance_signal beyond memory bounds — registration is silently skipped.
152export let boundsCheckOverflow = {
153 async test() {
154 // Should instantiate without error; the module simply won't receive shutdown signals.
155 await WebAssembly.instantiate(overflowModule);
156 },
157};
158 
159// __instance_signal at 65533 leaves only 3 bytes but needs 4 — silently skipped.
160export let boundsCheckEdge = {
161 async test() {
162 await WebAssembly.instantiate(edgeModule);
163 },
164};
165 
166// Both addresses exactly at the boundary — should succeed.
167export let boundsCheckValid = {
168 async test() {
169 const instance = await WebAssembly.instantiate(validModule);
170 if (instance.exports.get_signal() !== 0) {
171 throw new Error('Expected signal to be 0 initially');
172 }
173 },
174};
175 
176// ---------------------------------------------------------------------------
177// Memory detection tests
178// ---------------------------------------------------------------------------
179 
180// A module that imports memory (not exports it) should still register.
181export let importedMemoryDetected = {
182 async test() {
183 const memory = new WebAssembly.Memory({ initial: 1 });
184 const instance = await WebAssembly.instantiate(importedMemoryModule, {
185 env: { memory },
186 });
187 // If registration threw, we wouldn't get here.
188 if (instance.exports.get_signal() !== 0) {
189 throw new Error('Expected signal to be 0 initially');
190 }
191 },
192};
193 
194// A WebAssembly.Memory passed as a non-memory import must not be used as the
195// module's linear memory. The module has internal memory but doesn't export it.
196export let decoyMemoryIgnored = {
197 async test() {
198 const decoyMemory = new WebAssembly.Memory({ initial: 1 });
199 // The module imports (func "env" "log") only — decoy_memory is extra.
200 const _instance = await WebAssembly.instantiate(decoyModule, {
201 env: {
202 log: () => {},
203 decoy_memory: decoyMemory,
204 },
205 });
206 // The shim should have found no memory (internal memory is inaccessible).
207 // Verify decoy memory is untouched (we can't trigger writeShutdownSignal
208 // in workerd, but we can verify instantiation didn't blow up).
209 const view = new Uint32Array(decoyMemory.buffer);
210 if (view[0] !== 0) {
211 throw new Error('Decoy memory was modified during instantiation');
212 }
213 },
214};
215 
216// A module that imports a global named "memory" as externref must not be
217// confused for a linear memory import. The shim checks Module.imports() kind
218// and should skip registration because the import's kind is 'global', not 'memory'.
219export let externrefMemoryIgnored = {
220 async test() {
221 const instance = await WebAssembly.instantiate(externrefMemoryModule, {
222 env: { memory: null },
223 });
224 // Should instantiate fine — shim just doesn't register it.
225 if (instance.exports.get_value() !== 42) {
226 throw new Error('Expected get_value() to return 42');
227 }
228 },
229};
230 
231// The sync Instance constructor should also detect imported memory.
232export let syncInstanceImportedMemory = {
233 test() {
234 const memory = new WebAssembly.Memory({ initial: 1 });
235 const instance = new WebAssembly.Instance(importedMemoryModule, {
236 env: { memory },
237 });
238 if (instance.exports.get_signal() !== 0) {
239 throw new Error('Expected signal to be 0 initially');
240 }
241 },
242};
243 
244// ---------------------------------------------------------------------------
245// GC reclamation tests
246// ---------------------------------------------------------------------------
247 
248// Instantiate many large (16MB) WASM modules, let them go out of scope so V8 can
249// GC the instances. The weak instanceRef becomes empty when V8 collects the
250// unreachable instance, and the GC prologue filter removes the entry, releasing
251// the strong reference to linear memory. If entries aren't cleaned up, this OOMs.
252//
253// There is a one-cycle delay: V8 resets the weak handle during GC, but our prologue
254// (which runs before marking) detects it in the *next* cycle. V8's external memory
255// tracking (16MB per BackingStore) should trigger GC frequently enough to prevent OOM.
256export let gcReclaimsModules = {
257 async test() {
258 for (let i = 0; i < 20; i++) {
259 // Each instance goes out of scope at the end of the loop iteration.
260 // The `await` yields to the event loop, giving V8 opportunities to trigger GC.
261 await WebAssembly.instantiate(reclaimModule);
262 }
263 // If we get here without OOM, reclamation is working.
264 },
265};