File
Blob: src/pyodide/internal/pool/builtin_wrappers.ts
| 1 | import type { getRandomValues as getRandomValuesType } from 'pyodide-internal:topLevelEntropy/lib'; |
| 2 | import { default as UnsafeEval } from 'internal:unsafe-eval'; |
| 3 | import { PythonWorkersInternalError } from 'pyodide-internal:util'; |
| 4 | import { PyodideVersion } from 'pyodide-internal:const'; |
| 5 | |
| 6 | if (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. |
| 17 | export const location = undefined; |
| 18 | |
| 19 | export 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. |
| 29 | export 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 | |
| 41 | export function reportUndefinedSymbolsPatched(Module: Module): void { |
| 42 | if (Module.API.version === PyodideVersion.V0_26_0a2) { |
| 43 | return; |
| 44 | } |
| 45 | Module.reportUndefinedSymbols(); |
| 46 | } |
| 47 | |
| 48 | function 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 | |
| 60 | function 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 | |
| 81 | export 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 | |
| 96 | export 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 | |
| 108 | let getRandomValuesInner: typeof getRandomValuesType; |
| 109 | export function setGetRandomValues(func: typeof getRandomValuesType): void { |
| 110 | getRandomValuesInner = func; |
| 111 | } |
| 112 | |
| 113 | export function getRandomValues(Module: Module, arr: Uint8Array): Uint8Array { |
| 114 | return getRandomValuesInner(Module, arr); |
| 115 | } |
| 116 | |
| 117 | let lastTime: number; |
| 118 | let 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 | */ |
| 124 | export 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 | */ |
| 168 | let finishedSetup = false; |
| 169 | export function finishSetup(): void { |
| 170 | finishedSetup = true; |
| 171 | } |
| 172 | |
| 173 | export function newWasmModule(buffer: Uint8Array): WebAssembly.Module { |
| 174 | if (finishedSetup) { |
| 175 | checkCallee(); |
| 176 | } |
| 177 | return UnsafeEval.newWasmModule(buffer); |
| 178 | } |
| 179 | |
| 180 | export 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 | */ |
| 203 | function 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 | */ |
| 227 | function 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 |
| 276 | function 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 | |
| 352 | let countFuncParams: (funcPtr: number) => number; |
| 353 | |
| 354 | export function patched_PyEM_CountFuncParams(Module: Module, funcPtr: any) { |
| 355 | countFuncParams ??= getCountFuncParams(Module); |
| 356 | return countFuncParams(funcPtr); |
| 357 | } |