Skip to content
File

Blob: src/pyodide/internal/pool/builtin_wrappers.ts

typescript358 lines
1import type { getRandomValues as getRandomValuesType } from 'pyodide-internal:topLevelEntropy/lib';
2import { default as UnsafeEval } from 'internal:unsafe-eval';
3import { PythonWorkersInternalError } from 'pyodide-internal:util';
4import { PyodideVersion } from 'pyodide-internal:const';
5 
6if (typeof FinalizationRegistry === 'undefined') {
7 // @ts-expect-error cannot assign to globalThis
8 globalThis.FinalizationRegistry = class FinalizationRegistry {
9 register(): void {}
10 unregister(): void {}
11 };
12}
13 
14// Pyodide uses `new URL(some_url, location)` to resolve the path in `loadPackage`. Setting
15// `location = undefined` makes this throw an error if some_url is not an absolute url. Which is what
16// we want here, it doesn't make sense to load a package from a relative URL.
17export const location = undefined;
18 
19export function addEventListener(): void {}
20 
21// Mostly we use the `jsglobals` variable for everything except for:
22// * Pyodide's scheduler.ts
23// * Emscripten's implementation of syscalls etc
24//
25// These locations use the globals directly so they will get them from the pool isolate unless we
26// modify globalThis to include setTimeout etc from the main isolate.
27// We could just change scheduler.ts to use jsglobals but it won't fix Emscripten so we'll need to
28// do this either way.
29export function setSetTimeout(
30 st: typeof setTimeout,
31 ct: typeof clearTimeout,
32 si: typeof setInterval,
33 ci: typeof clearInterval
34): void {
35 globalThis.setTimeout = st;
36 globalThis.clearTimeout = ct;
37 globalThis.setInterval = si;
38 globalThis.clearInterval = ci;
39}
40 
41export function reportUndefinedSymbolsPatched(Module: Module): void {
42 if (Module.API.version === PyodideVersion.V0_26_0a2) {
43 return;
44 }
45 Module.reportUndefinedSymbols();
46}
47 
48function dynlibLookup026Helper(
49 Module: Module,
50 path: string
51): string | undefined {
52 try {
53 Module.FS.lookupPath(path);
54 } catch (e) {
55 return undefined;
56 }
57 return path;
58}
59 
60function dynlibLookup026(Module: Module, libName: string): string {
61 // This function is for 0.26.0a2 only. In newer versions, we set LD_LIBRARY_PATH instead.
62 if (Module.API.version !== PyodideVersion.V0_26_0a2) {
63 throw new PythonWorkersInternalError('Should not happen');
64 }
65 // Most libraries are loaded from /usr/lib. For scipy and similar libraries that depend on
66 // Pyodide's dynamic library deps, we may need extra "system libraries". These we'll put in
67 // python_modules/lib. So try loading system libraries from there too.
68 const result =
69 dynlibLookup026Helper(Module, '/usr/lib/' + libName) ??
70 dynlibLookup026Helper(
71 Module,
72 '/session/metadata/python_modules/lib/' + libName
73 );
74 if (!result) {
75 console.error('Failed to read ', libName);
76 throw new PythonWorkersInternalError('Should not happen');
77 }
78 return result;
79}
80 
81export function patchedLoadLibData(
82 Module: Module,
83 path: string,
84 rpath: any
85): WebAssembly.Module {
86 if (!path.startsWith('/')) {
87 if (Module.API.version === PyodideVersion.V0_26_0a2) {
88 path = dynlibLookup026(Module, path);
89 } else {
90 path = Module.findLibraryFS(path, rpath);
91 }
92 }
93 return Module.compileModuleFromReadOnlyFS(Module, path);
94}
95 
96export function patchedApplyFunc(
97 API: API,
98 func: (...params: any[]) => any,
99 this_: object,
100 args: any[]
101): any {
102 return API.config.jsglobals.Function.prototype.apply.apply(func, [
103 this_,
104 args,
105 ]);
106}
107 
108let getRandomValuesInner: typeof getRandomValuesType;
109export function setGetRandomValues(func: typeof getRandomValuesType): void {
110 getRandomValuesInner = func;
111}
112 
113export function getRandomValues(Module: Module, arr: Uint8Array): Uint8Array {
114 return getRandomValuesInner(Module, arr);
115}
116 
117let lastTime: number;
118let lastDelta = 0;
119/**
120 * Wrapper for Date.now that always advances by at least a millisecond. So that
121 * directories change their modification time when updated so that Python
122 * doesn't use stale directory contents in its import system.
123 */
124export function monotonicDateNow(): number {
125 const now = Date.now();
126 if (now === lastTime) {
127 lastDelta++;
128 } else {
129 lastTime = now;
130 lastDelta = 0;
131 }
132 return now + lastDelta;
133}
134 
135/**
136 * First check that the callee is what we expect, then use `UnsafeEval` to
137 * construct a `WasmModule`.
138 *
139 * What we expect of the callee is that:
140 * 1. it's in pyodide.asm.js
141 * 2. it's in one of the locations that are required for it to work. We can
142 * pretty easily make a whitelist of these.
143 *
144 * In particular, we specifically don't want to allow calls from places that
145 * call arbitrary functions for the user like `JsvFunction_CallBound` or
146 * `raw_call_js`; if a user somehow gets their hands on a reference to
147 * `newWasmModule` and tries to call it from Python the call would come from one
148 * of these places. Currently we only need to allow `convertJsFunctionToWasm`
149 * but if we enable JSPI we'll need to whitelist a few more locations.
150 *
151 * Some remarks:
152 * 1. I don't really think that this `builtin_wrappers.newWasmModule` function
153 * can leak from `pyodide.asm.js`, but the code for `pyodide.asm.js` is
154 * generated and so difficult to analyze. I think the correct thing to do
155 * from a security analysis perspective is to assume that unreviewed
156 * generated code leaks all permissions it receives.
157 * 2. Assuming user code somehow gets direct access to
158 * `builtin_wrappers.newWasmModule` I don't think it can spoof a call that
159 * passes this check.
160 * 3. In normal Python code, this will only be called a fixed number of times
161 * every time we load a .so file. If we ever get to the position where
162 * `checkCallee` is a performance bottleneck, that would be a great success.
163 * Using ctypes, one can arrange to call a lot more times by repeatedly
164 * allocating and discarding closures. But:
165 * - ctypes is quite slow even by Python's standards
166 * - Normally ctypes allocates all closures up front
167 */
168let finishedSetup = false;
169export function finishSetup(): void {
170 finishedSetup = true;
171}
172 
173export function newWasmModule(buffer: Uint8Array): WebAssembly.Module {
174 if (finishedSetup) {
175 checkCallee();
176 }
177 return UnsafeEval.newWasmModule(buffer);
178}
179 
180export function wasmInstantiate(
181 mod: WebAssembly.Module | Uint8Array,
182 imports: WebAssembly.Imports
183): Promise<{ module: WebAssembly.Module; instance: WebAssembly.Instance }> {
184 let module;
185 if (mod instanceof WebAssembly.Module) {
186 module = mod;
187 } else {
188 if (finishedSetup) {
189 checkCallee();
190 }
191 module = UnsafeEval.newWasmModule(mod);
192 }
193 const instance = new WebAssembly.Instance(module, imports);
194 return Promise.resolve({ module, instance });
195}
196 
197/**
198 * Check that the callee is `convertJsFunctionToWasm` by formatting a stack
199 * trace and using `prepareStackTrace` to read out the callee. It should be
200 * `convertJsFunctionToWasm` in `"pyodide-internal:generated/pyodide.asm"`,
201 * if it's anything else we'll bail.
202 */
203function checkCallee(): void {
204 const origPrepareStackTrace = Error.prepareStackTrace;
205 let isOkay, funcName;
206 try {
207 Error.prepareStackTrace = prepareStackTrace;
208 [isOkay, funcName] = new Error().stack as unknown as ReturnType<
209 typeof prepareStackTrace
210 >;
211 } finally {
212 Error.prepareStackTrace = origPrepareStackTrace;
213 }
214 if (!isOkay) {
215 console.warn('Invalid call to `WebAssembly.Module`', funcName);
216 throw new PythonWorkersInternalError(
217 'Invalid call to `WebAssembly.Module`'
218 );
219 }
220}
221 
222/**
223 * Helper function for checkCallee, returns `true` if the callee is `convertJsFunctionToWasm`,
224 * `generate`, or `getPyEMCountArgsPtr` in `pyodide.asm.js`, `false` if not. This will set the
225 * `stack` field in the error so we can read back the result there.
226 */
227function prepareStackTrace(
228 _error: Error,
229 stack: StackItem[]
230): [boolean, string] {
231 // In case a logic error is ever introduced in this function, defend against
232 // reentrant calls by setting `prepareStackTrace` to `undefined`.
233 Error.prepareStackTrace = undefined;
234 // Counting up, the bottom of the stack is `checkCallee`, then
235 // `newWasmModule`, and the third entry should be our callee.
236 if (stack.length < 3) {
237 return [false, ''];
238 }
239 try {
240 const funcName = stack[2].getFunctionName();
241 const fileName = stack[2].getFileName();
242 if (fileName !== 'pyodideRuntime-internal:emscriptenSetup') {
243 return [false, funcName];
244 }
245 return [
246 ['convertJsFunctionToWasm', 'generate', 'getPyEMCountArgsPtr'].includes(
247 funcName
248 ),
249 funcName,
250 ];
251 } catch (e) {
252 console.warn(e);
253 return [false, ''];
254 }
255}
256 
257/**
258 * This is a fix for a problem with package snapshots in 0.26.0a2. 0.26.0a2 tests if
259 * wasm-type-reflection is supported by the runtime and if so uses it to avoid function pointer
260 * casting instead of a JS trampoline. We cannot stack switch through the JS trampoline so we need
261 * to make sure that when stack switching is available, we don't use JS trampolines. When 0.26.0a2
262 * was released, wasm-stack-switching implied wasm-type-reflection.
263 *
264 * Unfortunately there is a bug (fixed in 0.26.0a3...) that made the assumption that if the runtime
265 * that we use to make a snapshot supports wasm-type-reflection, the runtime we use it in will too.
266 *
267 * Later, JSPI was rewritten not to depend on wasm-type-reflection, but the implication was left in
268 * the v8 codebase until a few weeks ago. So our snapshots expect to be able to use an implementation
269 * of `patched_PyEM_CountFuncParams` based on wasm-type-reflection, but it's not on anymore.
270 *
271 * Luckily, in the meantime there is a way to count the arguments of a webassembly function using
272 * wasm-gc. It's not exactly pretty though.
273 * This is copied from https://github.com/python/cpython/blob/main/Python/emscripten_trampoline.c
274 */
275// prettier-ignore
276function getCountFuncParams(Module: Module): (funcPtr: number) => number {
277 const code = new Uint8Array([
278 0x00, 0x61, 0x73, 0x6d, // \0asm magic number
279 0x01, 0x00, 0x00, 0x00, // version 1
280 0x01, 0x1b, // Type section, body is 0x1b bytes
281 0x05, // 6 entries
282 0x60, 0x00, 0x01, 0x7f, // (type $type0 (func (param) (result i32)))
283 0x60, 0x01, 0x7f, 0x01, 0x7f, // (type $type1 (func (param i32) (result i32)))
284 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, // (type $type2 (func (param i32 i32) (result i32)))
285 0x60, 0x03, 0x7f, 0x7f, 0x7f, 0x01, 0x7f, // (type $type3 (func (param i32 i32 i32) (result i32)))
286 0x60, 0x01, 0x7f, 0x00, // (type $blocktype (func (param i32) (result)))
287 0x02, 0x09, // Import section, 0x9 byte body
288 0x01, // 1 import (table $funcs (import "e" "t") 0 funcref)
289 0x01, 0x65, // "e"
290 0x01, 0x74, // "t"
291 0x01, // importing a table
292 0x70, // of entry type funcref
293 0x00, 0x00, // table limits: no max, min of 0
294 0x03, 0x02, // Function section
295 0x01, 0x01, // We're going to define one function of type 1 (func (param i32) (result i32))
296 0x07, 0x05, // export section
297 0x01, // 1 export
298 0x01, 0x66, // called "f"
299 0x00, // a function
300 0x00, // at index 0
301 
302 0x0a, 0x44, // Code section,
303 0x01, 0x42, // one entry of length 50
304 0x01, 0x01, 0x70, // one local of type funcref
305 // Body of the function
306 0x20, 0x00, // local.get $fptr
307 0x25, 0x00, // table.get $funcs
308 0x22, 0x01, // local.tee $fref
309 0xfb, 0x14, 0x03, // ref.test $type3
310 0x02, 0x04, // block $b (type $blocktype)
311 0x45, // i32.eqz
312 0x0d, 0x00, // br_if $b
313 0x41, 0x03, // i32.const 3
314 0x0f, // return
315 0x0b, // end block
316 
317 0x20, 0x01, // local.get $fref
318 0xfb, 0x14, 0x02, // ref.test $type2
319 0x02, 0x04, // block $b (type $blocktype)
320 0x45, // i32.eqz
321 0x0d, 0x00, // br_if $b
322 0x41, 0x02, // i32.const 2
323 0x0f, // return
324 0x0b, // end block
325 
326 0x20, 0x01, // local.get $fref
327 0xfb, 0x14, 0x01, // ref.test $type1
328 0x02, 0x04, // block $b (type $blocktype)
329 0x45, // i32.eqz
330 0x0d, 0x00, // br_if $b
331 0x41, 0x01, // i32.const 1
332 0x0f, // return
333 0x0b, // end block
334 
335 0x20, 0x01, // local.get $fref
336 0xfb, 0x14, 0x00, // ref.test $type0
337 0x02, 0x04, // block $b (type $blocktype)
338 0x45, // i32.eqz
339 0x0d, 0x00, // br_if $b
340 0x41, 0x00, // i32.const 0
341 0x0f, // return
342 0x0b, // end block
343 
344 0x41, 0x7f, // i32.const -1
345 0x0b // end function
346 ]);
347 const mod = UnsafeEval.newWasmModule(code);
348 const inst = new WebAssembly.Instance(mod, { e: { t: Module.wasmTable } });
349 return inst.exports.f as ReturnType<typeof getCountFuncParams>;
350}
351 
352let countFuncParams: (funcPtr: number) => number;
353 
354export function patched_PyEM_CountFuncParams(Module: Module, funcPtr: any) {
355 countFuncParams ??= getCountFuncParams(Module);
356 return countFuncParams(funcPtr);
357}