File
Blob: src/pyodide/internal/topLevelEntropy/lib.ts
| 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 | |
| 5 | /** |
| 6 | * Handle the top level getentropy() mess. See entropy_patches.py which is the |
| 7 | * main file for the entropy patches. |
| 8 | * |
| 9 | * This file installs the relevant files and calls the exports from |
| 10 | * entropy_patches.py. setupShouldAllowBadEntropy reads out the address of the |
| 11 | * byte that we use to control calls to crypto.getRandomValues from Python. |
| 12 | */ |
| 13 | import { default as entropyPatches } from 'pyodide-internal:topLevelEntropy/entropy_patches.py'; |
| 14 | import { default as entropyImportContext } from 'pyodide-internal:topLevelEntropy/entropy_import_context.py'; |
| 15 | import { default as entropyImportContextPackages } from 'pyodide-internal:topLevelEntropy/entropy_import_context_packages.py'; |
| 16 | import { default as importPatchManager } from 'pyodide-internal:topLevelEntropy/import_patch_manager.py'; |
| 17 | import { default as allowEntropy } from 'pyodide-internal:topLevelEntropy/allow_entropy.py'; |
| 18 | import { simpleRunPython, PythonUserError } from 'pyodide-internal:util'; |
| 19 | import { CHECK_RNG_STATE, PROCESS_PTH_FILES } from 'pyodide-internal:metadata'; |
| 20 | |
| 21 | let allowed_entropy_calls_addr: number; |
| 22 | |
| 23 | /** |
| 24 | * Set up a byte for communication between JS and Python. |
| 25 | * |
| 26 | * We make an array in Python and then get its address in JavaScript so |
| 27 | * shouldAllowBadEntropy can check / write back the value |
| 28 | */ |
| 29 | function setupShouldAllowBadEntropy(Module: Module): void { |
| 30 | // get_bad_entropy_flag prints the address we want into stderr which is returned into res. |
| 31 | // We parse this as an integer. |
| 32 | const res = simpleRunPython( |
| 33 | Module, |
| 34 | 'from _cloudflare.entropy_import_context import get_bad_entropy_flag;' + |
| 35 | 'get_bad_entropy_flag();' + |
| 36 | 'del get_bad_entropy_flag' |
| 37 | ); |
| 38 | allowed_entropy_calls_addr = Number(res); |
| 39 | } |
| 40 | |
| 41 | function shouldAllowBadEntropy(Module: Module): boolean { |
| 42 | const val = Module.HEAP8[allowed_entropy_calls_addr]; |
| 43 | if (val) { |
| 44 | Module.HEAP8[allowed_entropy_calls_addr]!--; |
| 45 | return true; |
| 46 | } |
| 47 | return false; |
| 48 | } |
| 49 | |
| 50 | let IN_REQUEST_CONTEXT = false; |
| 51 | |
| 52 | /** |
| 53 | * Some packages need hash or random seeds at import time. We carefully track |
| 54 | * how much bad entropy we're giving everyone so that hopefully none of it ends |
| 55 | * up in a place where the end user needed good entropy. In particular, we think |
| 56 | * it's acceptable to give poor entropy for hash seeds but not for random seeds. |
| 57 | * The random libraries are allowed to initialize themselves with a bad seed but |
| 58 | * we disable them until we have a chance to reseed. |
| 59 | * |
| 60 | * See entropy_import_context.py where `allow_bad_entropy_calls` is used to dole |
| 61 | * out the bad entropy. |
| 62 | */ |
| 63 | export function getRandomValues( |
| 64 | Module: Module, |
| 65 | arr: Uint8Array<ArrayBuffer> |
| 66 | ): Uint8Array<ArrayBuffer> { |
| 67 | if (IN_REQUEST_CONTEXT) { |
| 68 | return crypto.getRandomValues(arr); |
| 69 | } |
| 70 | if (!shouldAllowBadEntropy(Module)) { |
| 71 | console.log('Entropy call failed'); |
| 72 | console.log('JS stack:', new Error().stack); |
| 73 | console.log('Python stack:'); |
| 74 | Module._dump_traceback(); |
| 75 | throw new PythonUserError( |
| 76 | 'Disallowed operation called within global scope' |
| 77 | ); |
| 78 | } |
| 79 | // "entropy" in the test suite is a bunch of 42's. Good to use a readily identifiable pattern |
| 80 | // here which is different than the test suite. |
| 81 | arr.fill(43); |
| 82 | return arr; |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * We call this regardless of whether we are restoring from a snapshot or not, |
| 87 | * after instantiating the Emscripten module but before restoring the snapshot. |
| 88 | * Hypothetically, we could skip it for new dedicated snapshots. |
| 89 | */ |
| 90 | export function entropyMountFiles(Module: Module): void { |
| 91 | const cloudflareDir = Module.FS.sitePackages + '/_cloudflare'; |
| 92 | Module.FS.mkdir(cloudflareDir); |
| 93 | const files: [string, ArrayBuffer][] = [ |
| 94 | ['__init__.py', new ArrayBuffer(0)], |
| 95 | ['entropy_patches.py', entropyPatches], |
| 96 | ['entropy_import_context.py', entropyImportContext], |
| 97 | ['import_patch_manager.py', importPatchManager], |
| 98 | ['allow_entropy.py', allowEntropy], |
| 99 | ]; |
| 100 | if (!PROCESS_PTH_FILES) { |
| 101 | files.push([ |
| 102 | 'entropy_import_context_packages.py', |
| 103 | entropyImportContextPackages, |
| 104 | ]); |
| 105 | } |
| 106 | |
| 107 | for (const [name, contents] of files) { |
| 108 | Module.FS.writeFile(cloudflareDir + '/' + name, new Uint8Array(contents), { |
| 109 | canOwn: true, |
| 110 | }); |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * This prepares us to execute the top level scope. It changes JS state so it |
| 116 | * needs to be called whether restoring snapshot or not. We have to call this |
| 117 | * after the runtime is ready, so after restoring the snapshot in the snapshot |
| 118 | * branch and after entropyMountFiles in the no-snapshot branch. |
| 119 | */ |
| 120 | export function entropyAfterRuntimeInit(Module: Module): void { |
| 121 | setupShouldAllowBadEntropy(Module); |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * This prepares us to execute the top level scope. It changes only Python state |
| 126 | * so it doesn't need to be called when restoring from snapshot. |
| 127 | */ |
| 128 | export function entropyBeforeTopLevel(Module: Module): void { |
| 129 | simpleRunPython( |
| 130 | Module, |
| 131 | ` |
| 132 | from _cloudflare.entropy_patches import before_top_level |
| 133 | before_top_level() |
| 134 | del before_top_level |
| 135 | ` |
| 136 | ); |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * Called to check that random number generator state was not advanced by top level calls. We |
| 141 | * manually install overlays that crash when they are called in order to prevent this situation. |
| 142 | */ |
| 143 | export function entropyAfterSnapshot(Module: Module): void { |
| 144 | if (!CHECK_RNG_STATE) { |
| 145 | return; |
| 146 | } |
| 147 | simpleRunPython( |
| 148 | Module, |
| 149 | ` |
| 150 | from _cloudflare.entropy_patches import after_snapshot |
| 151 | after_snapshot() |
| 152 | del after_snapshot |
| 153 | ` |
| 154 | ); |
| 155 | } |
| 156 | |
| 157 | let isReady = false; |
| 158 | /** |
| 159 | * Called to reseed rngs and turn off blocks that prevent access to rng APIs. |
| 160 | */ |
| 161 | export function entropyBeforeRequest(Module: Module): void { |
| 162 | if (isReady) { |
| 163 | // I think this is only ever called once, but we guard it just to be sure. |
| 164 | return; |
| 165 | } |
| 166 | IN_REQUEST_CONTEXT = true; |
| 167 | isReady = true; |
| 168 | simpleRunPython( |
| 169 | Module, |
| 170 | ` |
| 171 | from _cloudflare.entropy_patches import before_first_request |
| 172 | before_first_request() |
| 173 | del before_first_request |
| 174 | ` |
| 175 | ); |
| 176 | } |