File
Blob: src/pyodide/internal/readOnlyFS.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 | type ReadFn<Info> = FSStreamOps<Info>['read']; |
| 6 | |
| 7 | // When we load shared libraries we need to ensure they come from a read only file system. |
| 8 | |
| 9 | // Map to store the original trusted read function for each read-only filesystem. We store the |
| 10 | // function itself to prevent attacks where user code modifies stream_ops.read after filesystem |
| 11 | // creation and tricks us into loading a dynamically generated so file. |
| 12 | const TRUSTED_READ_FUNCS: Map<object, ReadFn<any>> = new Map(); |
| 13 | |
| 14 | export function getTrustedReadFunc<Info>( |
| 15 | node: FSNode<Info> |
| 16 | ): ReadFn<Info> | undefined { |
| 17 | return TRUSTED_READ_FUNCS.get(node.mount.type); |
| 18 | } |
| 19 | |
| 20 | export function createReadonlyFS<Info>( |
| 21 | FSOps: FSOps<Info>, |
| 22 | Module: Module |
| 23 | ): EmscriptenFS<Info> { |
| 24 | const FS = Module.FS; |
| 25 | const ReadOnlyFS: EmscriptenFS<Info> = { |
| 26 | mount(mount) { |
| 27 | return ReadOnlyFS.createNode(null, '/', mount.opts.info); |
| 28 | }, |
| 29 | createNode(parent, name, info): FSNode<Info> { |
| 30 | // eslint-disable-next-line prefer-const |
| 31 | let { permissions: mode, isDir } = FSOps.getNodeMode(parent, name, info); |
| 32 | if (isDir) { |
| 33 | mode |= 1 << 14; // set S_IFDIR |
| 34 | } else { |
| 35 | mode |= 1 << 15; // set S_IFREG |
| 36 | } |
| 37 | const node = FS.createNode(parent, name, mode); |
| 38 | node.node_ops = ReadOnlyFS.node_ops; |
| 39 | node.stream_ops = ReadOnlyFS.stream_ops; |
| 40 | FSOps.setNodeAttributes(node, info, isDir); |
| 41 | return node; |
| 42 | }, |
| 43 | node_ops: { |
| 44 | getattr(node) { |
| 45 | const size = node.usedBytes; |
| 46 | const mode = node.mode; |
| 47 | const t = new Date(node.modtime); |
| 48 | const blksize = 4096; |
| 49 | const blocks = ((size + blksize - 1) / blksize) | 0; |
| 50 | return { |
| 51 | dev: 1, |
| 52 | ino: node.id, |
| 53 | mode, |
| 54 | nlink: 1, |
| 55 | uid: 0, |
| 56 | gid: 0, |
| 57 | rdev: 0, |
| 58 | size, |
| 59 | atime: t, |
| 60 | mtime: t, |
| 61 | ctime: t, |
| 62 | blksize, |
| 63 | blocks, |
| 64 | }; |
| 65 | }, |
| 66 | readdir(node) { |
| 67 | return FSOps.readdir(node); |
| 68 | }, |
| 69 | lookup(parent, name) { |
| 70 | const child = FSOps.lookup(parent, name); |
| 71 | if (child === undefined) { |
| 72 | throw FS.genericErrors?.[44] ?? new FS.ErrnoError(44); // ENOENT |
| 73 | } |
| 74 | return ReadOnlyFS.createNode(parent, name, child); |
| 75 | }, |
| 76 | }, |
| 77 | stream_ops: { |
| 78 | llseek(stream, offset, whence) { |
| 79 | let position = offset; |
| 80 | if (whence === 1) { |
| 81 | // SEEK_CUR |
| 82 | position += stream.position; |
| 83 | } else if (whence === 2) { |
| 84 | // SEEK_END |
| 85 | if (FS.isFile(stream.node.mode)) { |
| 86 | position += stream.node.usedBytes; |
| 87 | } |
| 88 | } |
| 89 | return position; |
| 90 | }, |
| 91 | read(stream, buffer, offset, length, position) { |
| 92 | if (position >= stream.node.usedBytes) return 0; |
| 93 | const size = Math.min(stream.node.usedBytes - position, length); |
| 94 | buffer = buffer.subarray(offset, offset + size); |
| 95 | return FSOps.read(stream, position, buffer); |
| 96 | }, |
| 97 | }, |
| 98 | }; |
| 99 | // Register this filesystem as read-only and store its trusted read function so we can load so |
| 100 | // files from it. |
| 101 | TRUSTED_READ_FUNCS.set(ReadOnlyFS, ReadOnlyFS.stream_ops.read); |
| 102 | return ReadOnlyFS; |
| 103 | } |