Skip to content
File

Blob: src/pyodide/internal/topLevelEntropy/lib.ts

typescript177 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 
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 */
13import { default as entropyPatches } from 'pyodide-internal:topLevelEntropy/entropy_patches.py';
14import { default as entropyImportContext } from 'pyodide-internal:topLevelEntropy/entropy_import_context.py';
15import { default as entropyImportContextPackages } from 'pyodide-internal:topLevelEntropy/entropy_import_context_packages.py';
16import { default as importPatchManager } from 'pyodide-internal:topLevelEntropy/import_patch_manager.py';
17import { default as allowEntropy } from 'pyodide-internal:topLevelEntropy/allow_entropy.py';
18import { simpleRunPython, PythonUserError } from 'pyodide-internal:util';
19import { CHECK_RNG_STATE, PROCESS_PTH_FILES } from 'pyodide-internal:metadata';
20 
21let 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 */
29function 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 
41function 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 
50let 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 */
63export 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 */
90export 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 */
120export 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 */
128export function entropyBeforeTopLevel(Module: Module): void {
129 simpleRunPython(
130 Module,
131 `
132from _cloudflare.entropy_patches import before_top_level
133before_top_level()
134del 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 */
143export function entropyAfterSnapshot(Module: Module): void {
144 if (!CHECK_RNG_STATE) {
145 return;
146 }
147 simpleRunPython(
148 Module,
149 `
150from _cloudflare.entropy_patches import after_snapshot
151after_snapshot()
152del after_snapshot
153 `
154 );
155}
156 
157let isReady = false;
158/**
159 * Called to reseed rngs and turn off blocks that prevent access to rng APIs.
160 */
161export 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 `
171from _cloudflare.entropy_patches import before_first_request
172before_first_request()
173del before_first_request
174 `
175 );
176}