Skip to content

Commit 7ef9c79

Browse files
committed
Fix: Make it impossible to load dynamically generated shared libraries
1 parent 65a8274 commit 7ef9c79

File tree

8 files changed

+110
-7
lines changed

8 files changed

+110
-7
lines changed

src/pyodide/helpers.bzl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,17 @@ _REPLACEMENTS = [
239239
"var tableBase=metadata.tableSize?wasmTable.length:0;" +
240240
"Module.snapshotDebug && console.log('loadWebAssemblyModule', libName, memoryBase, tableBase);",
241241
],
242+
[
243+
"function loadLibData(){",
244+
"""
245+
function loadLibData(){
246+
var f = findLibraryFS(libName, flags.rpath);
247+
var libData = Module.patched_loadLibData(Module, f);
248+
return flags.loadAsync ? Promise.resolve(libData) : libData;
249+
}
250+
function loadLibData1(){
251+
""",
252+
],
242253
]
243254

244255
def _python_bundle(version, *, pyodide_asm_wasm = None, pyodide_asm_js = None, python_stdlib_zip = None, emscripten_setup_override = None):

src/pyodide/internal/pool/builtin_wrappers.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,12 +226,9 @@ function prepareStackTrace(
226226
return [false, funcName];
227227
}
228228
return [
229-
[
230-
'loadModule',
231-
'convertJsFunctionToWasm',
232-
'generate',
233-
'getPyEMCountArgsPtr',
234-
].includes(funcName),
229+
['convertJsFunctionToWasm', 'generate', 'getPyEMCountArgsPtr'].includes(
230+
funcName
231+
),
235232
funcName,
236233
];
237234
} catch (e) {

src/pyodide/internal/python.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
import { loadPackages } from 'pyodide-internal:loadPackage';
3939
import { default as MetadataReader } from 'pyodide-internal:runtime-generated/metadata';
4040
import { TRANSITIVE_REQUIREMENTS } from 'pyodide-internal:metadata';
41+
import { getTrustedReadFunc } from 'pyodide-internal:readOnlyFS';
4142

4243
/**
4344
* After running `instantiateEmscriptenModule` but before calling into any C
@@ -206,6 +207,24 @@ export function clearSignals(Module: Module): void {
206207
}
207208
}
208209

210+
function patched_loadLibData(Module: Module, path: string): WebAssembly.Module {
211+
const { node } = Module.FS.lookupPath(path);
212+
// Get the trusted read function from our private Map, not from the node
213+
// or filesystem object (which could have been tampered with by user code)
214+
const trustedRead = getTrustedReadFunc(node);
215+
if (!trustedRead) {
216+
throw new Error(
217+
'Can only load shared libraries from read only file systems.'
218+
);
219+
}
220+
const stat = node.node_ops.getattr(node);
221+
const buffer = new Uint8Array(stat.size);
222+
// Create a minimal stream object and read using trusted read function
223+
const stream = { node, position: 0 };
224+
trustedRead(stream, buffer, 0, stat.size, 0);
225+
return UnsafeEval.newWasmModule(buffer);
226+
}
227+
209228
export function loadPyodide(
210229
isWorkerd: boolean,
211230
lockfile: PackageLock,
@@ -216,6 +235,7 @@ export function loadPyodide(
216235
const Module = enterJaegerSpan('instantiate_emscripten', () =>
217236
SetupEmscripten.getModule()
218237
);
238+
Module.patched_loadLibData = patched_loadLibData;
219239
Module.API.config.jsglobals = globalThis;
220240
if (isWorkerd) {
221241
Module.API.config.indexURL = indexURL;

src/pyodide/internal/readOnlyFS.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
type ReadFn<Info> = FSStreamOps<Info>['read'];
2+
3+
// When we load shared libraries we need to ensure they come from a read only file system.
4+
5+
// Map to store the original trusted read function for each read-only filesystem. We store the
6+
// function itself to prevent attacks where user code modifies stream_ops.read after filesystem
7+
// creation and tricks us into loading a dynamically generated so file.
8+
const TRUSTED_READ_FUNCS: Map<object, ReadFn<any>> = new Map();
9+
10+
export function getTrustedReadFunc<Info>(
11+
node: FSNode<Info>
12+
): ReadFn<Info> | undefined {
13+
return TRUSTED_READ_FUNCS.get(node.mount.type);
14+
}
15+
116
export function createReadonlyFS<Info>(
217
FSOps: FSOps<Info>,
318
Module: Module
@@ -77,5 +92,8 @@ export function createReadonlyFS<Info>(
7792
},
7893
},
7994
};
95+
// Register this filesystem as read-only and store its trusted read function so we can load so
96+
// files from it.
97+
TRUSTED_READ_FUNCS.set(ReadOnlyFS, ReadOnlyFS.stream_ops.read);
8098
return ReadOnlyFS;
8199
}

src/pyodide/types/emscripten.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,5 @@ interface Module {
135135
Py_EmscriptenSignalBuffer: Uint8Array;
136136
_Py_EMSCRIPTEN_SIGNAL_HANDLING: number;
137137
___memory_base: WebAssembly.Global<'i32'>;
138+
patched_loadLibData: (Module: Module, path: string) => WebAssembly.Module;
138139
}

src/pyodide/types/filesystem.d.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,31 @@ interface TarFSInfo {
1818
declare type MetadataDirInfo = Map<string, MetadataFSInfo>;
1919
declare type MetadataFSInfo = MetadataDirInfo | number; // file infos are numbers and dir infos are maps
2020

21+
interface FSLookupResult<Info> {
22+
node: FSNode<Info>;
23+
}
24+
2125
interface FS {
2226
mkdir: (dirname: string) => void;
2327
mkdirTree: (dirname: string) => void;
2428
writeFile: (fname: string, contents: Uint8Array, options: object) => void;
25-
readFile: (fname: string) => Uint8Array;
29+
readFile: (fname: string, options?: { encoding?: string }) => Uint8Array;
2630
mount(fs: object, options: { info?: any }, path: string): void;
2731
createNode<Info>(
2832
parent: FSNode<Info> | null,
2933
name: string,
3034
mode: number
3135
): FSNode<Info>;
36+
lookupPath<Info>(path: string): FSLookupResult<Info>;
37+
open<Info>(nodeOrPath: FSNode<Info> | string, flags?: number): FSStream<Info>;
38+
read<Info>(
39+
stream: FSStream<Info>,
40+
buffer: Uint8Array,
41+
offset: number,
42+
length: number,
43+
position: number
44+
): number;
45+
close<Info>(stream: FSStream<Info>): void;
3246
isFile: (mode: number) => boolean;
3347
readdir: (path: string) => string[];
3448
genericErrors: { 44: Error };
@@ -86,13 +100,18 @@ interface FSStreamOps<Info> {
86100
) => number;
87101
}
88102

103+
interface FSMount {
104+
type: EmscriptenFS<any>;
105+
}
106+
89107
interface FSNode<Info> {
90108
id: number;
91109
usedBytes: number;
92110
mode: number;
93111
modtime: number;
94112
node_ops: FSNodeOps<Info>;
95113
stream_ops: FSStreamOps<Info>;
114+
mount: FSMount;
96115
info: Info;
97116
contentsOffset?: number | undefined;
98117
tree?: MetadataDirInfo;

src/workerd/server/tests/python/pytest/pytest.wd-test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const unitTests :Workerd.Config = (
99
(name = "tests/test_env.py", pythonModule = embed "pytest/tests/test_env.py"),
1010
(name = "tests/test_fs.py", pythonModule = embed "pytest/tests/test_fs.py"),
1111
(name = "tests/test_import_from_javascript.py", pythonModule = embed "pytest/tests/test_import_from_javascript.py"),
12+
(name = "tests/test_dynlib_loading.py", pythonModule = embed "pytest/tests/test_dynlib_loading.py"),
1213
%PYTHON_VENDORED_MODULES%
1314
],
1415
compatibilityFlags = [
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
6+
def use(x):
7+
pass
8+
9+
10+
def test_dynlib_loading(tmp_path, monkeypatch):
11+
# fmt: off
12+
Path(tmp_path / "a.so").write_bytes(
13+
bytes(
14+
[
15+
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x08, 0x64,
16+
0x79, 0x6c, 0x69, 0x6e, 0x6b, 0x2e, 0x30, 0x01, 0x04, 0x00, 0x00, 0x00,
17+
0x00, 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, 0x02, 0x38, 0x03, 0x03, 0x65,
18+
0x6e, 0x76, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x00,
19+
0x03, 0x65, 0x6e, 0x76, 0x0d, 0x5f, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72,
20+
0x79, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x03, 0x7f, 0x00, 0x03, 0x65, 0x6e,
21+
0x76, 0x0c, 0x5f, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x62, 0x61,
22+
0x73, 0x65, 0x03, 0x7f, 0x00, 0x03, 0x02, 0x01, 0x00, 0x07, 0x15, 0x01,
23+
0x11, 0x5f, 0x5f, 0x77, 0x61, 0x73, 0x6d, 0x5f, 0x63, 0x61, 0x6c, 0x6c,
24+
0x5f, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x00, 0x00, 0x0a, 0x04, 0x01, 0x02,
25+
0x00, 0x0b
26+
]
27+
)
28+
)
29+
# fmt: on
30+
monkeypatch.syspath_prepend(tmp_path)
31+
with pytest.raises(
32+
ImportError, match="Can only load shared libraries from read only file systems"
33+
):
34+
import a
35+
36+
use(a)

0 commit comments

Comments
 (0)