Skip to content

Commit fc0dc58

Browse files
committed
fix: use srcdoc iframe with native API protection
- Switch from external HTML file to srcdoc for about:srcdoc origin - Protect FinalizationRegistry and WeakRef before extensions can modify - Add Content-Security-Policy header for additional security
1 parent 413d188 commit fc0dc58

File tree

1 file changed

+188
-5
lines changed

1 file changed

+188
-5
lines changed

src/lib/ruby-wasm.ts

Lines changed: 188 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* T-Ruby WASM Loader for Playground
33
*
4-
* Uses a sandboxed iframe to isolate WASM execution from browser extensions
4+
* Uses a sandboxed iframe with srcdoc to isolate WASM execution from browser extensions
55
* that might interfere with FinalizationRegistry and other APIs.
66
*/
77

@@ -45,8 +45,188 @@ function generateRequestId(): string {
4545
return `req_${++requestIdCounter}_${Date.now()}`;
4646
}
4747

48+
// Worker HTML content - embedded as string to use srcdoc
49+
// This creates an about:srcdoc origin iframe which most extensions don't target
50+
const WORKER_HTML = `<!DOCTYPE html>
51+
<html>
52+
<head>
53+
<meta charset="UTF-8">
54+
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: https://cdn.jsdelivr.net;">
55+
<title>T-Ruby WASM Worker</title>
56+
</head>
57+
<body>
58+
<script>
59+
// Protect native APIs before any extension can modify them
60+
(function() {
61+
const nativeFinalizationRegistry = window.FinalizationRegistry;
62+
const nativeWeakRef = window.WeakRef;
63+
64+
// Ensure these are the native implementations
65+
Object.defineProperty(window, 'FinalizationRegistry', {
66+
value: nativeFinalizationRegistry,
67+
writable: false,
68+
configurable: false
69+
});
70+
71+
Object.defineProperty(window, 'WeakRef', {
72+
value: nativeWeakRef,
73+
writable: false,
74+
configurable: false
75+
});
76+
})();
77+
<\/script>
78+
<script type="module">
79+
// CDN URLs
80+
const RUBY_WASM_CDN = 'https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.7.0/dist/browser/+esm';
81+
const RUBY_WASM_BINARY = 'https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.7.0/dist/ruby+stdlib.wasm';
82+
83+
// Bootstrap code for T-Ruby compiler
84+
const BOOTSTRAP_CODE = \`
85+
require "json"
86+
87+
$trb_compiler = nil
88+
89+
def get_compiler
90+
$trb_compiler ||= TRuby::Compiler.new
91+
end
92+
93+
def __trb_compile__(code)
94+
compiler = get_compiler
95+
96+
begin
97+
result = compiler.compile_string(code)
98+
99+
{
100+
success: result[:errors].empty?,
101+
ruby: result[:ruby] || "",
102+
rbs: result[:rbs] || "",
103+
errors: result[:errors] || []
104+
}.to_json
105+
rescue TRuby::ParseError => e
106+
{
107+
success: false,
108+
ruby: "",
109+
rbs: "",
110+
errors: [e.message]
111+
}.to_json
112+
rescue StandardError => e
113+
{
114+
success: false,
115+
ruby: "",
116+
rbs: "",
117+
errors: ["Compilation error: " + e.message]
118+
}.to_json
119+
end
120+
end
121+
122+
def __trb_health_check__
123+
{
124+
loaded: defined?(TRuby) == "constant",
125+
version: defined?(TRuby::VERSION) ? TRuby::VERSION : "unknown",
126+
ruby_version: RUBY_VERSION
127+
}.to_json
128+
end
129+
\`;
130+
131+
let vm = null;
132+
let isReady = false;
133+
134+
// Send message to parent
135+
function sendToParent(type, data, requestId) {
136+
parent.postMessage({ type, data, requestId }, '*');
137+
}
138+
139+
// Initialize Ruby VM
140+
async function initialize() {
141+
try {
142+
console.log('[WASM Worker] Step 1: Loading Ruby WASM module...');
143+
sendToParent('progress', { message: 'Loading Ruby runtime...', progress: 10 });
144+
145+
const { DefaultRubyVM } = await import(RUBY_WASM_CDN);
146+
console.log('[WASM Worker] Step 1 complete');
147+
148+
console.log('[WASM Worker] Step 2: Fetching WASM binary...');
149+
sendToParent('progress', { message: 'Downloading Ruby WASM binary...', progress: 30 });
150+
151+
const response = await fetch(RUBY_WASM_BINARY);
152+
const wasmModule = await WebAssembly.compileStreaming(response);
153+
console.log('[WASM Worker] Step 2 complete');
154+
155+
console.log('[WASM Worker] Step 3: Initializing Ruby VM...');
156+
sendToParent('progress', { message: 'Initializing Ruby VM...', progress: 60 });
157+
158+
const result = await DefaultRubyVM(wasmModule);
159+
vm = result.vm;
160+
console.log('[WASM Worker] Step 3 complete');
161+
162+
console.log('[WASM Worker] Step 4: Loading T-Ruby bootstrap...');
163+
sendToParent('progress', { message: 'Loading T-Ruby compiler...', progress: 80 });
164+
165+
vm.eval(BOOTSTRAP_CODE);
166+
console.log('[WASM Worker] Step 4 complete');
167+
168+
// Health check
169+
const healthResult = vm.eval('__trb_health_check__');
170+
console.log('[WASM Worker] Health check:', healthResult.toString());
171+
172+
isReady = true;
173+
sendToParent('ready', { health: JSON.parse(healthResult.toString()) });
174+
175+
} catch (error) {
176+
console.error('[WASM Worker] Init error:', error);
177+
sendToParent('error', { message: error.message });
178+
}
179+
}
180+
181+
// Compile code
182+
function compile(code, requestId) {
183+
if (!isReady || !vm) {
184+
sendToParent('compile-result', {
185+
success: false,
186+
errors: ['Compiler not ready']
187+
}, requestId);
188+
return;
189+
}
190+
191+
try {
192+
console.log('[WASM Worker] Compiling:', code.substring(0, 50) + '...');
193+
const resultJson = vm.eval('__trb_compile__(' + JSON.stringify(code) + ')');
194+
const result = JSON.parse(resultJson.toString());
195+
console.log('[WASM Worker] Compile result:', result);
196+
sendToParent('compile-result', result, requestId);
197+
} catch (error) {
198+
console.error('[WASM Worker] Compile error:', error);
199+
sendToParent('compile-result', {
200+
success: false,
201+
ruby: '',
202+
rbs: '',
203+
errors: [error.message]
204+
}, requestId);
205+
}
206+
}
207+
208+
// Listen for messages from parent
209+
window.addEventListener('message', (event) => {
210+
const { type, data, requestId } = event.data || {};
211+
212+
switch (type) {
213+
case 'init':
214+
initialize();
215+
break;
216+
case 'compile':
217+
compile(data.code, requestId);
218+
break;
219+
}
220+
});
221+
222+
// Signal that worker is loaded
223+
sendToParent('loaded', {});
224+
<\/script>
225+
</body>
226+
</html>`;
227+
48228
/**
49-
* Load the T-Ruby WASM compiler using sandboxed iframe
229+
* Load the T-Ruby WASM compiler using sandboxed iframe with srcdoc
50230
* Returns cached instance if already loaded
51231
*/
52232
export async function loadTRubyCompiler(
@@ -79,18 +259,21 @@ async function doLoadCompiler(
79259
onProgress?: (progress: LoadingProgress) => void
80260
): Promise<TRubyCompiler> {
81261
return new Promise((resolve, reject) => {
82-
console.log('[T-Ruby] Creating sandboxed iframe for WASM execution...');
262+
console.log('[T-Ruby] Creating sandboxed srcdoc iframe for WASM execution...');
83263
onProgress?.({
84264
state: 'loading',
85265
message: 'Initializing compiler sandbox...',
86266
progress: 5
87267
});
88268

89-
// Create sandboxed iframe
269+
// Create sandboxed iframe with srcdoc
270+
// Using srcdoc creates an about:srcdoc origin which most extensions don't target
90271
const iframe = document.createElement('iframe');
91272
iframe.style.display = 'none';
273+
// allow-scripts: needed to run JavaScript
274+
// allow-same-origin: needed for postMessage to work properly
92275
iframe.sandbox.add('allow-scripts');
93-
iframe.src = '/wasm-worker.html';
276+
iframe.srcdoc = WORKER_HTML;
94277

95278
// Message handler
96279
const messageHandler = (event: MessageEvent) => {

0 commit comments

Comments
 (0)