just-bash 2.11.9 → 2.11.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,6 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
9
9
  import { createRequire } from "node:module";
10
10
  import { dirname } from "node:path";
11
11
  import { parentPort, workerData } from "node:worker_threads";
12
- import { loadPyodide } from "pyodide";
13
12
 
14
13
  // src/security/blocked-globals.ts
15
14
  function getBlockedGlobals() {
@@ -1536,21 +1535,60 @@ var SyncFsBackend = class {
1536
1535
  };
1537
1536
 
1538
1537
  // src/commands/python3/worker.ts
1539
- var pyodideInstance = null;
1540
- var pyodideLoading = null;
1538
+ import { readFileSync } from "node:fs";
1541
1539
  var require2 = createRequire(import.meta.url);
1542
- var pyodideIndexURL = `${dirname(require2.resolve("pyodide/pyodide.mjs"))}/`;
1543
- async function getPyodide() {
1544
- if (pyodideInstance) {
1545
- return pyodideInstance;
1546
- }
1547
- if (pyodideLoading) {
1548
- return pyodideLoading;
1540
+ try {
1541
+ const NodeModule = require2("node:module");
1542
+ if (typeof NodeModule._load === "function") {
1543
+ const originalLoad = NodeModule._load;
1544
+ const blockedModules = /* @__PURE__ */ new Set([
1545
+ "child_process",
1546
+ "node:child_process",
1547
+ "cluster",
1548
+ "node:cluster",
1549
+ "dgram",
1550
+ "node:dgram",
1551
+ "dns",
1552
+ "node:dns",
1553
+ "net",
1554
+ "node:net",
1555
+ "tls",
1556
+ "node:tls",
1557
+ "vm",
1558
+ "node:vm",
1559
+ "v8",
1560
+ "node:v8",
1561
+ "inspector",
1562
+ "node:inspector",
1563
+ "inspector/promises",
1564
+ "node:inspector/promises",
1565
+ "trace_events",
1566
+ "node:trace_events",
1567
+ "perf_hooks",
1568
+ "node:perf_hooks",
1569
+ "worker_threads",
1570
+ "node:worker_threads"
1571
+ ]);
1572
+ NodeModule._load = function(request, ...rest) {
1573
+ if (blockedModules.has(request)) {
1574
+ throw new Error(
1575
+ `[Defense-in-depth] require('${request}') is blocked in worker context`
1576
+ );
1577
+ }
1578
+ return originalLoad.apply(this, [request, ...rest]);
1579
+ };
1549
1580
  }
1550
- pyodideLoading = loadPyodide({ indexURL: pyodideIndexURL });
1551
- pyodideInstance = await pyodideLoading;
1552
- return pyodideInstance;
1581
+ } catch {
1582
+ }
1583
+ var cpythonDir;
1584
+ try {
1585
+ cpythonDir = dirname(
1586
+ require2.resolve("../../../vendor/cpython-emscripten/python.cjs")
1587
+ );
1588
+ } catch (_e) {
1589
+ cpythonDir = dirname(import.meta.url).replace("file://", "") + "/../../../vendor/cpython-emscripten";
1553
1590
  }
1591
+ var stdlibZipPath = `${cpythonDir}/python313.zip`;
1554
1592
  function createHOSTFS(backend, FS, PATH) {
1555
1593
  const ERRNO_CODES = {
1556
1594
  EPERM: 63,
@@ -1816,114 +1854,438 @@ function createHOSTFS(backend, FS, PATH) {
1816
1854
  };
1817
1855
  return HOSTFS;
1818
1856
  }
1819
- async function runPython(input) {
1820
- const backend = new SyncFsBackend(input.sharedBuffer);
1821
- let pyodide;
1822
- try {
1823
- pyodide = await getPyodide();
1824
- } catch (e) {
1825
- return {
1826
- success: false,
1827
- error: `Failed to load Pyodide: ${e.message}`
1828
- };
1829
- }
1830
- pyodide.setStdout({ batched: () => {
1831
- } });
1832
- pyodide.setStderr({ batched: () => {
1833
- } });
1834
- try {
1835
- pyodide.runPython(`
1836
- import sys
1837
- if hasattr(sys.stdout, 'flush'):
1838
- sys.stdout.flush()
1839
- if hasattr(sys.stderr, 'flush'):
1840
- sys.stderr.flush()
1841
- `);
1842
- } catch (_e) {
1843
- }
1844
- pyodide.setStdout({
1845
- batched: (text) => {
1846
- backend.writeStdout(`${text}
1847
- `);
1848
- }
1849
- });
1850
- pyodide.setStderr({
1851
- batched: (text) => {
1852
- backend.writeStderr(`${text}
1853
- `);
1854
- }
1855
- });
1856
- const FS = pyodide.FS;
1857
- const PATH = pyodide.PATH;
1858
- const HOSTFS = createHOSTFS(backend, FS, PATH);
1859
- try {
1860
- try {
1861
- pyodide.runPython(`import os; os.chdir('/')`);
1862
- } catch (_e) {
1863
- }
1864
- try {
1865
- FS.mkdir("/host");
1866
- } catch (_e) {
1867
- }
1868
- try {
1869
- FS.unmount("/host");
1870
- } catch (_e) {
1871
- }
1872
- FS.mount(HOSTFS, { root: "/" }, "/host");
1873
- } catch (e) {
1874
- return {
1875
- success: false,
1876
- error: `Failed to mount HOSTFS: ${e.message}`
1877
- };
1878
- }
1879
- try {
1880
- pyodide.runPython(`
1881
- import sys
1882
- if '_jb_http_bridge' in sys.modules:
1883
- del sys.modules['_jb_http_bridge']
1884
- if 'jb_http' in sys.modules:
1885
- del sys.modules['jb_http']
1886
- `);
1887
- } catch (_e) {
1888
- }
1889
- const bridgeRequestFn = (url, method, headersJson, body) => {
1890
- try {
1891
- const headers = headersJson ? JSON.parse(headersJson) : void 0;
1892
- const result = backend.httpRequest(url, {
1893
- method: method || "GET",
1894
- headers,
1895
- body: body || void 0
1896
- });
1897
- return JSON.stringify(result);
1898
- } catch (e) {
1899
- return JSON.stringify({ error: e.message });
1900
- }
1901
- };
1902
- pyodide.registerJsModule("_jb_http_bridge", {
1903
- request: bridgeRequestFn
1904
- });
1905
- pyodide.globals.set("_jb_http_req_fn", bridgeRequestFn);
1857
+ function generateSetupCode(input) {
1906
1858
  const envSetup = Object.entries(input.env).map(([key, value]) => {
1907
1859
  return `os.environ[${JSON.stringify(key)}] = ${JSON.stringify(value)}`;
1908
1860
  }).join("\n");
1909
1861
  const argv0 = input.scriptPath || "python3";
1910
1862
  const argvList = [argv0, ...input.args].map((arg) => JSON.stringify(arg)).join(", ");
1911
- try {
1912
- await pyodide.runPythonAsync(`
1863
+ return `
1913
1864
  import os
1914
1865
  import sys
1915
- import builtins
1916
1866
  import json
1917
1867
 
1918
1868
  ${envSetup}
1919
1869
 
1920
1870
  sys.argv = [${argvList}]
1921
1871
 
1922
- # _jb_http_req_fn is injected by TypeScript via pyodide.globals.set().
1923
- # It's the bridge request function directly \u2014 no import of _jb_http_bridge needed.
1924
- # The _jb_http_bridge module is blocked from user import.
1872
+ # Path redirection: redirect /absolute paths to /host mount
1873
+ def _should_redirect(path):
1874
+ return (isinstance(path, str) and
1875
+ path.startswith('/') and
1876
+ not path.startswith('/lib') and
1877
+ not path.startswith('/proc') and
1878
+ not path.startswith('/host') and
1879
+ not path.startswith('/_jb_http'))
1880
+
1881
+ # builtins.open
1882
+ import builtins
1883
+ _orig_open = builtins.open
1884
+ def _redir_open(path, mode='r', *args, **kwargs):
1885
+ if _should_redirect(path):
1886
+ path = '/host' + path
1887
+ return _orig_open(path, mode, *args, **kwargs)
1888
+ builtins.open = _redir_open
1889
+
1890
+ # os file operations
1891
+ _orig_listdir = os.listdir
1892
+ def _redir_listdir(path='.'):
1893
+ if _should_redirect(path):
1894
+ path = '/host' + path
1895
+ return _orig_listdir(path)
1896
+ os.listdir = _redir_listdir
1897
+
1898
+ _orig_exists = os.path.exists
1899
+ def _redir_exists(path):
1900
+ if _should_redirect(path):
1901
+ path = '/host' + path
1902
+ return _orig_exists(path)
1903
+ os.path.exists = _redir_exists
1904
+
1905
+ _orig_isfile = os.path.isfile
1906
+ def _redir_isfile(path):
1907
+ if _should_redirect(path):
1908
+ path = '/host' + path
1909
+ return _orig_isfile(path)
1910
+ os.path.isfile = _redir_isfile
1911
+
1912
+ _orig_isdir = os.path.isdir
1913
+ def _redir_isdir(path):
1914
+ if _should_redirect(path):
1915
+ path = '/host' + path
1916
+ return _orig_isdir(path)
1917
+ os.path.isdir = _redir_isdir
1918
+
1919
+ _orig_stat = os.stat
1920
+ def _redir_stat(path, *args, **kwargs):
1921
+ if _should_redirect(path):
1922
+ path = '/host' + path
1923
+ return _orig_stat(path, *args, **kwargs)
1924
+ os.stat = _redir_stat
1925
+
1926
+ _orig_mkdir = os.mkdir
1927
+ def _redir_mkdir(path, *args, **kwargs):
1928
+ if _should_redirect(path):
1929
+ path = '/host' + path
1930
+ return _orig_mkdir(path, *args, **kwargs)
1931
+ os.mkdir = _redir_mkdir
1932
+
1933
+ _orig_makedirs = os.makedirs
1934
+ def _redir_makedirs(path, *args, **kwargs):
1935
+ if _should_redirect(path):
1936
+ path = '/host' + path
1937
+ return _orig_makedirs(path, *args, **kwargs)
1938
+ os.makedirs = _redir_makedirs
1939
+
1940
+ _orig_remove = os.remove
1941
+ def _redir_remove(path, *args, **kwargs):
1942
+ if _should_redirect(path):
1943
+ path = '/host' + path
1944
+ return _orig_remove(path, *args, **kwargs)
1945
+ os.remove = _redir_remove
1946
+
1947
+ _orig_rmdir = os.rmdir
1948
+ def _redir_rmdir(path, *args, **kwargs):
1949
+ if _should_redirect(path):
1950
+ path = '/host' + path
1951
+ return _orig_rmdir(path, *args, **kwargs)
1952
+ os.rmdir = _redir_rmdir
1953
+
1954
+ _orig_getcwd = os.getcwd
1955
+ def _redir_getcwd():
1956
+ cwd = _orig_getcwd()
1957
+ if cwd.startswith('/host'):
1958
+ return cwd[5:]
1959
+ return cwd
1960
+ os.getcwd = _redir_getcwd
1961
+
1962
+ _orig_chdir = os.chdir
1963
+ def _redir_chdir(path):
1964
+ if _should_redirect(path):
1965
+ path = '/host' + path
1966
+ return _orig_chdir(path)
1967
+ os.chdir = _redir_chdir
1968
+
1969
+ # glob
1970
+ import glob as _glob_module
1971
+ _orig_glob = _glob_module.glob
1972
+ def _redir_glob(pathname, *args, **kwargs):
1973
+ if _should_redirect(pathname):
1974
+ pathname = '/host' + pathname
1975
+ return _orig_glob(pathname, *args, **kwargs)
1976
+ _glob_module.glob = _redir_glob
1977
+
1978
+ _orig_iglob = _glob_module.iglob
1979
+ def _redir_iglob(pathname, *args, **kwargs):
1980
+ if _should_redirect(pathname):
1981
+ pathname = '/host' + pathname
1982
+ return _orig_iglob(pathname, *args, **kwargs)
1983
+ _glob_module.iglob = _redir_iglob
1984
+
1985
+ # os.walk
1986
+ _orig_walk = os.walk
1987
+ def _redir_walk(top, *args, **kwargs):
1988
+ redirected = False
1989
+ if _should_redirect(top):
1990
+ top = '/host' + top
1991
+ redirected = True
1992
+ for dirpath, dirnames, filenames in _orig_walk(top, *args, **kwargs):
1993
+ if redirected and dirpath.startswith('/host'):
1994
+ dirpath = dirpath[5:] if len(dirpath) > 5 else '/'
1995
+ yield dirpath, dirnames, filenames
1996
+ os.walk = _redir_walk
1997
+
1998
+ # os.scandir
1999
+ _orig_scandir = os.scandir
2000
+ def _redir_scandir(path='.'):
2001
+ if _should_redirect(path):
2002
+ path = '/host' + path
2003
+ return _orig_scandir(path)
2004
+ os.scandir = _redir_scandir
2005
+
2006
+ # io.open
2007
+ import io as _io_module
2008
+ _io_module.open = builtins.open
2009
+
2010
+ # shutil
2011
+ import shutil as _shutil_module
2012
+
2013
+ _orig_shutil_copy = _shutil_module.copy
2014
+ def _redir_shutil_copy(src, dst, *args, **kwargs):
2015
+ if _should_redirect(src): src = '/host' + src
2016
+ if _should_redirect(dst): dst = '/host' + dst
2017
+ return _orig_shutil_copy(src, dst, *args, **kwargs)
2018
+ _shutil_module.copy = _redir_shutil_copy
2019
+
2020
+ _orig_shutil_copy2 = _shutil_module.copy2
2021
+ def _redir_shutil_copy2(src, dst, *args, **kwargs):
2022
+ if _should_redirect(src): src = '/host' + src
2023
+ if _should_redirect(dst): dst = '/host' + dst
2024
+ return _orig_shutil_copy2(src, dst, *args, **kwargs)
2025
+ _shutil_module.copy2 = _redir_shutil_copy2
2026
+
2027
+ _orig_shutil_copyfile = _shutil_module.copyfile
2028
+ def _redir_shutil_copyfile(src, dst, *args, **kwargs):
2029
+ if _should_redirect(src): src = '/host' + src
2030
+ if _should_redirect(dst): dst = '/host' + dst
2031
+ return _orig_shutil_copyfile(src, dst, *args, **kwargs)
2032
+ _shutil_module.copyfile = _redir_shutil_copyfile
2033
+
2034
+ _orig_shutil_copytree = _shutil_module.copytree
2035
+ def _redir_shutil_copytree(src, dst, *args, **kwargs):
2036
+ if _should_redirect(src): src = '/host' + src
2037
+ if _should_redirect(dst): dst = '/host' + dst
2038
+ return _orig_shutil_copytree(src, dst, *args, **kwargs)
2039
+ _shutil_module.copytree = _redir_shutil_copytree
2040
+
2041
+ _orig_shutil_move = _shutil_module.move
2042
+ def _redir_shutil_move(src, dst, *args, **kwargs):
2043
+ if _should_redirect(src): src = '/host' + src
2044
+ if _should_redirect(dst): dst = '/host' + dst
2045
+ return _orig_shutil_move(src, dst, *args, **kwargs)
2046
+ _shutil_module.move = _redir_shutil_move
2047
+
2048
+ _orig_shutil_rmtree = _shutil_module.rmtree
2049
+ def _redir_shutil_rmtree(path, *args, **kwargs):
2050
+ if _should_redirect(path): path = '/host' + path
2051
+ return _orig_shutil_rmtree(path, *args, **kwargs)
2052
+ _shutil_module.rmtree = _redir_shutil_rmtree
2053
+
2054
+ # pathlib.Path
2055
+ from pathlib import Path
2056
+
2057
+ def _redirect_path(p):
2058
+ s = str(p)
2059
+ if _should_redirect(s):
2060
+ return Path('/host' + s)
2061
+ return p
2062
+
2063
+ Path._orig_stat = Path.stat
2064
+ def _path_stat(self, *args, **kwargs):
2065
+ return _redirect_path(self)._orig_stat(*args, **kwargs)
2066
+ Path.stat = _path_stat
2067
+
2068
+ Path._orig_exists = Path.exists
2069
+ def _path_exists(self):
2070
+ return _redirect_path(self)._orig_exists()
2071
+ Path.exists = _path_exists
2072
+
2073
+ Path._orig_is_file = Path.is_file
2074
+ def _path_is_file(self):
2075
+ return _redirect_path(self)._orig_is_file()
2076
+ Path.is_file = _path_is_file
2077
+
2078
+ Path._orig_is_dir = Path.is_dir
2079
+ def _path_is_dir(self):
2080
+ return _redirect_path(self)._orig_is_dir()
2081
+ Path.is_dir = _path_is_dir
2082
+
2083
+ Path._orig_open = Path.open
2084
+ def _path_open(self, *args, **kwargs):
2085
+ return _redirect_path(self)._orig_open(*args, **kwargs)
2086
+ Path.open = _path_open
2087
+
2088
+ Path._orig_read_text = Path.read_text
2089
+ def _path_read_text(self, *args, **kwargs):
2090
+ return _redirect_path(self)._orig_read_text(*args, **kwargs)
2091
+ Path.read_text = _path_read_text
2092
+
2093
+ Path._orig_read_bytes = Path.read_bytes
2094
+ def _path_read_bytes(self):
2095
+ return _redirect_path(self)._orig_read_bytes()
2096
+ Path.read_bytes = _path_read_bytes
2097
+
2098
+ Path._orig_write_text = Path.write_text
2099
+ def _path_write_text(self, *args, **kwargs):
2100
+ return _redirect_path(self)._orig_write_text(*args, **kwargs)
2101
+ Path.write_text = _path_write_text
2102
+
2103
+ Path._orig_write_bytes = Path.write_bytes
2104
+ def _path_write_bytes(self, data):
2105
+ return _redirect_path(self)._orig_write_bytes(data)
2106
+ Path.write_bytes = _path_write_bytes
2107
+
2108
+ Path._orig_mkdir = Path.mkdir
2109
+ def _path_mkdir(self, *args, **kwargs):
2110
+ return _redirect_path(self)._orig_mkdir(*args, **kwargs)
2111
+ Path.mkdir = _path_mkdir
2112
+
2113
+ Path._orig_rmdir = Path.rmdir
2114
+ def _path_rmdir(self):
2115
+ return _redirect_path(self)._orig_rmdir()
2116
+ Path.rmdir = _path_rmdir
2117
+
2118
+ Path._orig_unlink = Path.unlink
2119
+ def _path_unlink(self, *args, **kwargs):
2120
+ return _redirect_path(self)._orig_unlink(*args, **kwargs)
2121
+ Path.unlink = _path_unlink
2122
+
2123
+ Path._orig_iterdir = Path.iterdir
2124
+ def _path_iterdir(self):
2125
+ redirected = _redirect_path(self)
2126
+ for p in redirected._orig_iterdir():
2127
+ s = str(p)
2128
+ if s.startswith('/host'):
2129
+ yield Path(s[5:])
2130
+ else:
2131
+ yield p
2132
+ Path.iterdir = _path_iterdir
2133
+
2134
+ Path._orig_glob = Path.glob
2135
+ def _path_glob(self, pattern):
2136
+ redirected = _redirect_path(self)
2137
+ for p in redirected._orig_glob(pattern):
2138
+ s = str(p)
2139
+ if s.startswith('/host'):
2140
+ yield Path(s[5:])
2141
+ else:
2142
+ yield p
2143
+ Path.glob = _path_glob
2144
+
2145
+ Path._orig_rglob = Path.rglob
2146
+ def _path_rglob(self, pattern):
2147
+ redirected = _redirect_path(self)
2148
+ for p in redirected._orig_rglob(pattern):
2149
+ s = str(p)
2150
+ if s.startswith('/host'):
2151
+ yield Path(s[5:])
2152
+ else:
2153
+ yield p
2154
+ Path.rglob = _path_rglob
2155
+
2156
+ # Set cwd to host mount
2157
+ os.chdir('/host' + ${JSON.stringify(input.cwd)})
2158
+ `;
2159
+ }
2160
+ function createHTTPFS(backend, FS) {
2161
+ let lastResponse = null;
2162
+ const encoder = new TextEncoder();
2163
+ const decoder = new TextDecoder();
2164
+ const HTTPFS = {
2165
+ mount(_mount) {
2166
+ return HTTPFS.createNode(null, "/", 16877, 0);
2167
+ },
2168
+ createNode(parent, name, mode, dev) {
2169
+ const node = FS.createNode(parent, name, mode, dev);
2170
+ node.node_ops = HTTPFS.node_ops;
2171
+ node.stream_ops = HTTPFS.stream_ops;
2172
+ return node;
2173
+ },
2174
+ node_ops: {
2175
+ getattr(node) {
2176
+ const isDir = node.name === "/" || node.parent === node;
2177
+ return {
2178
+ dev: 1,
2179
+ ino: node.id,
2180
+ mode: isDir ? 16877 : 33206,
2181
+ nlink: 1,
2182
+ uid: 0,
2183
+ gid: 0,
2184
+ rdev: 0,
2185
+ size: lastResponse ? lastResponse.length : 0,
2186
+ atime: /* @__PURE__ */ new Date(),
2187
+ mtime: /* @__PURE__ */ new Date(),
2188
+ ctime: /* @__PURE__ */ new Date(),
2189
+ blksize: 4096,
2190
+ blocks: 0
2191
+ };
2192
+ },
2193
+ setattr(_node, _attr) {
2194
+ },
2195
+ lookup(parent, name) {
2196
+ return HTTPFS.createNode(parent, name, 33206);
2197
+ },
2198
+ mknod(parent, name, mode, _dev) {
2199
+ return HTTPFS.createNode(parent, name, mode);
2200
+ },
2201
+ rename() {
2202
+ },
2203
+ unlink() {
2204
+ },
2205
+ rmdir() {
2206
+ },
2207
+ readdir(_node) {
2208
+ return ["request"];
2209
+ },
2210
+ symlink() {
2211
+ },
2212
+ readlink(_node) {
2213
+ return "";
2214
+ }
2215
+ },
2216
+ stream_ops: {
2217
+ open(stream) {
2218
+ delete stream.hostContent;
2219
+ stream.hostModified = false;
2220
+ const accessMode = stream.flags & 3;
2221
+ const isRead = accessMode === 0;
2222
+ if (isRead && lastResponse) {
2223
+ stream.hostContent = lastResponse;
2224
+ }
2225
+ },
2226
+ close(stream) {
2227
+ if (stream.hostModified && stream.hostContent) {
2228
+ const reqJson = decoder.decode(stream.hostContent);
2229
+ try {
2230
+ const req = JSON.parse(reqJson);
2231
+ const result = backend.httpRequest(req.url, {
2232
+ method: req.method || "GET",
2233
+ headers: req.headers || void 0,
2234
+ body: req.body || void 0
2235
+ });
2236
+ lastResponse = encoder.encode(JSON.stringify(result));
2237
+ } catch (e) {
2238
+ lastResponse = encoder.encode(
2239
+ JSON.stringify({ error: e.message })
2240
+ );
2241
+ }
2242
+ }
2243
+ delete stream.hostContent;
2244
+ delete stream.hostModified;
2245
+ },
2246
+ read(stream, buffer, offset, length, position) {
2247
+ const content = stream.hostContent;
2248
+ if (!content) return 0;
2249
+ const size = content.length;
2250
+ if (position >= size) return 0;
2251
+ const bytesToRead = Math.min(length, size - position);
2252
+ buffer.set(content.subarray(position, position + bytesToRead), offset);
2253
+ return bytesToRead;
2254
+ },
2255
+ write(stream, buffer, offset, length, position) {
2256
+ let content = stream.hostContent || new Uint8Array(0);
2257
+ const newSize = Math.max(content.length, position + length);
2258
+ if (newSize > content.length) {
2259
+ const newContent = new Uint8Array(newSize);
2260
+ newContent.set(content);
2261
+ content = newContent;
2262
+ stream.hostContent = content;
2263
+ }
2264
+ content.set(buffer.subarray(offset, offset + length), position);
2265
+ stream.hostModified = true;
2266
+ return length;
2267
+ },
2268
+ llseek(stream, offset, whence) {
2269
+ let position = offset;
2270
+ if (whence === 1)
2271
+ position += stream.position;
2272
+ else if (whence === 2) {
2273
+ const content = stream.hostContent;
2274
+ position += content ? content.length : 0;
2275
+ }
2276
+ if (position < 0) throw new FS.ErrnoError(28);
2277
+ return position;
2278
+ }
2279
+ }
2280
+ };
2281
+ return HTTPFS;
2282
+ }
2283
+ function generateHttpBridgeCode() {
2284
+ return `
2285
+ # HTTP bridge: jb_http module
2286
+ # Write request JSON to /_jb_http/request (custom FS triggers HTTP via SharedArrayBuffer)
2287
+ # Then read response JSON from same path.
1925
2288
 
1926
- # Create jb_http module for HTTP requests
1927
2289
  class _JbHttpResponse:
1928
2290
  """HTTP response object similar to requests.Response"""
1929
2291
  def __init__(self, data):
@@ -1949,21 +2311,23 @@ class _JbHttpResponse:
1949
2311
  raise Exception(f"HTTP {self.status_code}: {self.reason}")
1950
2312
 
1951
2313
  class _JbHttp:
1952
- """HTTP client that bridges to just-bash's secureFetch"""
2314
+ """HTTP client that bridges to just-bash's secureFetch via custom FS"""
2315
+ def _do_request(self, method, url, headers=None, body=None):
2316
+ import json as _json
2317
+ req = _json.dumps({'url': url, 'method': method, 'headers': headers, 'body': body})
2318
+ # Write request to HTTPFS \u2014 close triggers the HTTP call synchronously
2319
+ with _orig_open('/_jb_http/request', 'w') as f:
2320
+ f.write(req)
2321
+ # Read response (cached by HTTPFS from the HTTP call above)
2322
+ with _orig_open('/_jb_http/request', 'r') as f:
2323
+ return _json.loads(f.read())
2324
+
1953
2325
  def request(self, method, url, headers=None, data=None, json_data=None):
1954
- # Uses _jb_http_req_fn captured above from sys.modules before scrub.
1955
- # The bridge module itself is blocked from user import.
1956
- if _jb_http_req_fn is None:
1957
- raise Exception('HTTP bridge not available')
1958
2326
  if json_data is not None:
1959
2327
  data = json.dumps(json_data)
1960
2328
  headers = headers or {}
1961
2329
  headers['Content-Type'] = 'application/json'
1962
- # Serialize headers to JSON to avoid PyProxy issues when passing to JS
1963
- headers_json = json.dumps(headers) if headers else None
1964
- result_json = _jb_http_req_fn(url, method, headers_json, data)
1965
- result = json.loads(result_json)
1966
- # Check for errors from the bridge (network not configured, URL not allowed, etc.)
2330
+ result = self._do_request(method, url, headers, data)
1967
2331
  if 'error' in result and result.get('status') is None:
1968
2332
  raise Exception(result['error'])
1969
2333
  return _JbHttpResponse(result)
@@ -1986,7 +2350,6 @@ class _JbHttp:
1986
2350
  def patch(self, url, headers=None, data=None, json=None, **kwargs):
1987
2351
  return self.request('PATCH', url, headers=headers, data=data, json_data=json, **kwargs)
1988
2352
 
1989
- # Register jb_http as an importable module
1990
2353
  import types
1991
2354
  jb_http = types.ModuleType('jb_http')
1992
2355
  jb_http._client = _JbHttp()
@@ -1999,609 +2362,123 @@ jb_http.patch = jb_http._client.patch
1999
2362
  jb_http.request = jb_http._client.request
2000
2363
  jb_http.Response = _JbHttpResponse
2001
2364
  sys.modules['jb_http'] = jb_http
2002
-
2003
- # ============================================================
2004
- # SANDBOX SECURITY SETUP
2005
- # ============================================================
2006
- # Only apply sandbox restrictions once per Pyodide instance
2007
- if not hasattr(builtins, '_jb_sandbox_initialized'):
2008
- def _jb_init_sandbox():
2009
- builtins._jb_sandbox_initialized = True
2010
-
2011
- # ------------------------------------------------------------
2012
- # 1. Block dangerous module imports (js, pyodide, pyodide_js, pyodide.ffi, _pyodide)
2013
- # These allow sandbox escape via JavaScript execution
2014
- # ------------------------------------------------------------
2015
- _BLOCKED_MODULES = frozenset({'js', 'pyodide', 'pyodide_js', 'pyodide.ffi', '_pyodide', '_pyodide_core', 'ctypes', '_ctypes', '_jb_http_bridge'})
2016
- _BLOCKED_PREFIXES = ('js.', 'pyodide.', 'pyodide_js.', '_pyodide.', '_pyodide_core.', 'ctypes.', '_ctypes.', '_jb_http_bridge.')
2017
-
2018
- # Remove pre-loaded dangerous modules from sys.modules
2019
- for _blocked_mod in list(sys.modules.keys()):
2020
- if _blocked_mod in _BLOCKED_MODULES or any(_blocked_mod.startswith(p) for p in _BLOCKED_PREFIXES):
2021
- del sys.modules[_blocked_mod]
2022
-
2023
- # Create a secure callable wrapper that hides introspection attributes
2024
- # This prevents access to __closure__, __kwdefaults__, __globals__, etc.
2025
- def _make_secure_import(orig_import, blocked, prefixes):
2026
- """Create import function wrapped to block introspection."""
2027
- def _inner(name, globals=None, locals=None, fromlist=(), level=0):
2028
- if name in blocked or any(name.startswith(p) for p in prefixes):
2029
- raise ImportError(f"Module '{name}' is blocked in this sandbox")
2030
- return orig_import(name, globals, locals, fromlist, level)
2031
-
2032
- class _SecureImport:
2033
- """Wrapper that hides function internals from introspection."""
2034
- __slots__ = ()
2035
- def __call__(self, name, globals=None, locals=None, fromlist=(), level=0):
2036
- try:
2037
- return _inner(name, globals, locals, fromlist, level)
2038
- except BaseException as _e:
2039
- # Strip traceback to prevent frame inspection leaking
2040
- # orig_import from _inner's closure variables
2041
- _e.__traceback__ = None
2042
- raise
2043
- def __getattribute__(self, name):
2044
- if name in ('__call__', '__class__'):
2045
- return object.__getattribute__(self, name)
2046
- raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
2047
- def __repr__(self):
2048
- return '<built-in function __import__>'
2049
- return _SecureImport()
2050
-
2051
- builtins.__import__ = _make_secure_import(builtins.__import__, _BLOCKED_MODULES, _BLOCKED_PREFIXES)
2052
-
2053
- # ------------------------------------------------------------
2054
- # 1b. Block imports via sys.meta_path finder
2055
- # importlib.import_module routes through _bootstrap._find_and_load,
2056
- # NOT through builtins.__import__. A meta_path finder blocks ALL
2057
- # import paths at the most fundamental level.
2058
- # ------------------------------------------------------------
2059
- _blocked_set = frozenset(_BLOCKED_MODULES)
2060
- _blocked_pfx = tuple(_BLOCKED_PREFIXES)
2061
-
2062
- class _BlockingFinder:
2063
- """Meta-path finder that blocks imports of sandboxed modules."""
2064
- __slots__ = ()
2065
- def find_module(self, name, path=None):
2066
- if name in _blocked_set or any(name.startswith(p) for p in _blocked_pfx):
2067
- raise ImportError(f"Module '{name}' is blocked in this sandbox")
2068
- return None
2069
- def find_spec(self, name, path, target=None):
2070
- if name in _blocked_set or any(name.startswith(p) for p in _blocked_pfx):
2071
- raise ImportError(f"Module '{name}' is blocked in this sandbox")
2072
- return None
2073
- sys.meta_path.insert(0, _BlockingFinder())
2074
-
2075
- # ------------------------------------------------------------
2076
- # 1c. Patch importlib.import_module and importlib.util.find_spec
2077
- # Belt-and-suspenders: also block at the importlib API level
2078
- # in case sys.modules already contains the blocked module.
2079
- # ------------------------------------------------------------
2080
- import importlib
2081
- import importlib.util
2082
-
2083
- _orig_import_module = importlib.import_module
2084
- def _secure_import_module_inner(name, package=None):
2085
- if name in _blocked_set or any(name.startswith(p) for p in _blocked_pfx):
2086
- raise ImportError(f"Module '{name}' is blocked in this sandbox")
2087
- return _orig_import_module(name, package)
2088
-
2089
- class _SecureImportModule:
2090
- __slots__ = ()
2091
- def __call__(self, name, package=None):
2092
- try:
2093
- return _secure_import_module_inner(name, package)
2094
- except BaseException as _e:
2095
- _e.__traceback__ = None
2096
- raise
2097
- def __getattribute__(self, name):
2098
- if name in ('__call__', '__class__'):
2099
- return object.__getattribute__(self, name)
2100
- raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
2101
- def __repr__(self):
2102
- return '<function import_module>'
2103
- importlib.import_module = _SecureImportModule()
2104
-
2105
- _orig_find_spec = importlib.util.find_spec
2106
- def _secure_find_spec_inner(name, package=None):
2107
- if name in _blocked_set or any(name.startswith(p) for p in _blocked_pfx):
2108
- raise ImportError(f"Module '{name}' is blocked in this sandbox")
2109
- return _orig_find_spec(name, package)
2110
-
2111
- class _SecureFindSpec:
2112
- __slots__ = ()
2113
- def __call__(self, name, package=None):
2114
- try:
2115
- return _secure_find_spec_inner(name, package)
2116
- except BaseException as _e:
2117
- _e.__traceback__ = None
2118
- raise
2119
- def __getattribute__(self, name):
2120
- if name in ('__call__', '__class__'):
2121
- return object.__getattribute__(self, name)
2122
- raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
2123
- def __repr__(self):
2124
- return '<function find_spec>'
2125
- importlib.util.find_spec = _SecureFindSpec()
2126
-
2127
- # Note: We intentionally do NOT replace sys.modules with a custom dict
2128
- # subclass. Replacing it breaks lazy imports (e.g., _strptime used by
2129
- # datetime.strptime) because Pyodide's C-level import machinery
2130
- # caches the original dict. Instead, we rely on:
2131
- # 1. Per-run scrubbing of blocked modules from sys.modules
2132
- # 2. meta_path finder blocking new imports
2133
- # 3. builtins.__import__ hook blocking import statements
2134
-
2135
- # ------------------------------------------------------------
2136
- # 1cc. Patch importlib._bootstrap at the deepest level
2137
- # _bootstrap.__import__ holds a reference to the ORIGINAL __import__
2138
- # (not our wrapper). An attacker can use it directly to bypass all
2139
- # our hooks. Patch it with our wrapper.
2140
- # Also patch _bootstrap._find_and_load \u2014 this is the function that
2141
- # the C-level import machinery calls. Even if an attacker steals
2142
- # orig_import via closure introspection AND replaces sys.meta_path,
2143
- # this blocks the import at the lowest Python-accessible level.
2144
- # ------------------------------------------------------------
2145
- _bootstrap_mod = sys.modules.get('importlib._bootstrap')
2146
- if _bootstrap_mod:
2147
- _bootstrap_mod.__import__ = builtins.__import__
2148
-
2149
- _orig_find_and_load = _bootstrap_mod._find_and_load
2150
- def _secure_find_and_load(name, import_):
2151
- if name in _blocked_set or any(name.startswith(p) for p in _blocked_pfx):
2152
- raise ImportError(f"Module '{name}' is blocked in this sandbox")
2153
- return _orig_find_and_load(name, import_)
2154
- _bootstrap_mod._find_and_load = _secure_find_and_load
2155
-
2156
- # Also patch _find_and_load_unlocked (called internally)
2157
- if hasattr(_bootstrap_mod, '_find_and_load_unlocked'):
2158
- _orig_find_and_load_unlocked = _bootstrap_mod._find_and_load_unlocked
2159
- def _secure_find_and_load_unlocked(name, import_):
2160
- if name in _blocked_set or any(name.startswith(p) for p in _blocked_pfx):
2161
- raise ImportError(f"Module '{name}' is blocked in this sandbox")
2162
- return _orig_find_and_load_unlocked(name, import_)
2163
- _bootstrap_mod._find_and_load_unlocked = _secure_find_and_load_unlocked
2164
-
2165
- del _BLOCKED_MODULES, _BLOCKED_PREFIXES, _make_secure_import
2166
-
2167
- # ------------------------------------------------------------
2168
- # 1d. Block gc object-discovery functions
2169
- # gc.get_objects() can find purged module objects still in memory
2170
- # (held by Pyodide C internals), enabling sandbox escape.
2171
- # ------------------------------------------------------------
2172
- import gc as _gc_module
2173
- _gc_module.get_objects = lambda: []
2174
- _gc_module.get_referrers = lambda *args: []
2175
- _gc_module.get_referents = lambda *args: []
2176
-
2177
- # ------------------------------------------------------------
2178
- # 1e. Neuter sys.settrace and sys.setprofile
2179
- # These debugging APIs expose call frames via trace callbacks.
2180
- # An attacker can use them to inspect closure variables in our
2181
- # import hooks (e.g., orig_import in _inner), stealing the
2182
- # original __import__ and bypassing all blocking.
2183
- # ------------------------------------------------------------
2184
- sys.settrace = lambda *args: None
2185
- sys.setprofile = lambda *args: None
2186
-
2187
- # ------------------------------------------------------------
2188
- # 2. Path redirection helper
2189
- # ------------------------------------------------------------
2190
- def _should_redirect(path):
2191
- """Check if a path should be redirected to /host."""
2192
- return (isinstance(path, str) and
2193
- path.startswith('/') and
2194
- not path.startswith('/lib') and
2195
- not path.startswith('/proc') and
2196
- not path.startswith('/host'))
2197
-
2198
- # ------------------------------------------------------------
2199
- # 3. Secure wrapper factory for file operations
2200
- # ------------------------------------------------------------
2201
- # This creates callable wrappers that hide __closure__, __globals__, etc.
2202
- def _make_secure_wrapper(func, name):
2203
- """Wrap a function to block introspection attributes."""
2204
- class _SecureWrapper:
2205
- __slots__ = ()
2206
- def __call__(self, *args, **kwargs):
2207
- return func(*args, **kwargs)
2208
- def __getattribute__(self, attr):
2209
- if attr in ('__call__', '__class__'):
2210
- return object.__getattribute__(self, attr)
2211
- raise AttributeError(f"'{type(self).__name__}' object has no attribute '{attr}'")
2212
- def __repr__(self):
2213
- return f'<built-in function {name}>'
2214
- return _SecureWrapper()
2215
-
2216
- # ------------------------------------------------------------
2217
- # 4. Redirect file operations to /host (with secure wrappers)
2218
- # ------------------------------------------------------------
2219
- # builtins.open
2220
- _orig_open = builtins.open
2221
- def _redir_open(path, mode='r', *args, **kwargs):
2222
- if _should_redirect(path):
2223
- path = '/host' + path
2224
- return _orig_open(path, mode, *args, **kwargs)
2225
- builtins.open = _make_secure_wrapper(_redir_open, 'open')
2226
-
2227
- # os.listdir
2228
- _orig_listdir = os.listdir
2229
- def _redir_listdir(path='.'):
2230
- if _should_redirect(path):
2231
- path = '/host' + path
2232
- return _orig_listdir(path)
2233
- os.listdir = _make_secure_wrapper(_redir_listdir, 'listdir')
2234
-
2235
- # os.path.exists
2236
- _orig_exists = os.path.exists
2237
- def _redir_exists(path):
2238
- if _should_redirect(path):
2239
- path = '/host' + path
2240
- return _orig_exists(path)
2241
- os.path.exists = _make_secure_wrapper(_redir_exists, 'exists')
2242
-
2243
- # os.path.isfile
2244
- _orig_isfile = os.path.isfile
2245
- def _redir_isfile(path):
2246
- if _should_redirect(path):
2247
- path = '/host' + path
2248
- return _orig_isfile(path)
2249
- os.path.isfile = _make_secure_wrapper(_redir_isfile, 'isfile')
2250
-
2251
- # os.path.isdir
2252
- _orig_isdir = os.path.isdir
2253
- def _redir_isdir(path):
2254
- if _should_redirect(path):
2255
- path = '/host' + path
2256
- return _orig_isdir(path)
2257
- os.path.isdir = _make_secure_wrapper(_redir_isdir, 'isdir')
2258
-
2259
- # os.stat
2260
- _orig_stat = os.stat
2261
- def _redir_stat(path, *args, **kwargs):
2262
- if _should_redirect(path):
2263
- path = '/host' + path
2264
- return _orig_stat(path, *args, **kwargs)
2265
- os.stat = _make_secure_wrapper(_redir_stat, 'stat')
2266
-
2267
- # os.mkdir
2268
- _orig_mkdir = os.mkdir
2269
- def _redir_mkdir(path, *args, **kwargs):
2270
- if _should_redirect(path):
2271
- path = '/host' + path
2272
- return _orig_mkdir(path, *args, **kwargs)
2273
- os.mkdir = _make_secure_wrapper(_redir_mkdir, 'mkdir')
2274
-
2275
- # os.makedirs
2276
- _orig_makedirs = os.makedirs
2277
- def _redir_makedirs(path, *args, **kwargs):
2278
- if _should_redirect(path):
2279
- path = '/host' + path
2280
- return _orig_makedirs(path, *args, **kwargs)
2281
- os.makedirs = _make_secure_wrapper(_redir_makedirs, 'makedirs')
2282
-
2283
- # os.remove
2284
- _orig_remove = os.remove
2285
- def _redir_remove(path, *args, **kwargs):
2286
- if _should_redirect(path):
2287
- path = '/host' + path
2288
- return _orig_remove(path, *args, **kwargs)
2289
- os.remove = _make_secure_wrapper(_redir_remove, 'remove')
2290
-
2291
- # os.rmdir
2292
- _orig_rmdir = os.rmdir
2293
- def _redir_rmdir(path, *args, **kwargs):
2294
- if _should_redirect(path):
2295
- path = '/host' + path
2296
- return _orig_rmdir(path, *args, **kwargs)
2297
- os.rmdir = _make_secure_wrapper(_redir_rmdir, 'rmdir')
2298
-
2299
- # os.getcwd - strip /host prefix
2300
- _orig_getcwd = os.getcwd
2301
- def _redir_getcwd():
2302
- cwd = _orig_getcwd()
2303
- if cwd.startswith('/host'):
2304
- return cwd[5:] # Strip '/host' prefix
2305
- return cwd
2306
- os.getcwd = _make_secure_wrapper(_redir_getcwd, 'getcwd')
2307
-
2308
- # os.chdir
2309
- _orig_chdir = os.chdir
2310
- def _redir_chdir(path):
2311
- if _should_redirect(path):
2312
- path = '/host' + path
2313
- return _orig_chdir(path)
2314
- os.chdir = _make_secure_wrapper(_redir_chdir, 'chdir')
2315
-
2316
- # ------------------------------------------------------------
2317
- # 5. Additional file operations (glob, walk, scandir, io.open)
2318
- # ------------------------------------------------------------
2319
- import glob as _glob_module
2320
-
2321
- _orig_glob = _glob_module.glob
2322
- def _redir_glob(pathname, *args, **kwargs):
2323
- if _should_redirect(pathname):
2324
- pathname = '/host' + pathname
2325
- return _orig_glob(pathname, *args, **kwargs)
2326
- _glob_module.glob = _make_secure_wrapper(_redir_glob, 'glob')
2327
-
2328
- _orig_iglob = _glob_module.iglob
2329
- def _redir_iglob(pathname, *args, **kwargs):
2330
- if _should_redirect(pathname):
2331
- pathname = '/host' + pathname
2332
- return _orig_iglob(pathname, *args, **kwargs)
2333
- _glob_module.iglob = _make_secure_wrapper(_redir_iglob, 'iglob')
2334
-
2335
- # os.walk (generator - needs special handling)
2336
- _orig_walk = os.walk
2337
- def _redir_walk(top, *args, **kwargs):
2338
- redirected = False
2339
- if _should_redirect(top):
2340
- top = '/host' + top
2341
- redirected = True
2342
- for dirpath, dirnames, filenames in _orig_walk(top, *args, **kwargs):
2343
- if redirected and dirpath.startswith('/host'):
2344
- dirpath = dirpath[5:] if len(dirpath) > 5 else '/'
2345
- yield dirpath, dirnames, filenames
2346
- os.walk = _make_secure_wrapper(_redir_walk, 'walk')
2347
-
2348
- # os.scandir
2349
- _orig_scandir = os.scandir
2350
- def _redir_scandir(path='.'):
2351
- if _should_redirect(path):
2352
- path = '/host' + path
2353
- return _orig_scandir(path)
2354
- os.scandir = _make_secure_wrapper(_redir_scandir, 'scandir')
2355
-
2356
- # io.open (same secure wrapper as builtins.open)
2357
- import io as _io_module
2358
- _io_module.open = builtins.open
2359
-
2360
- # ------------------------------------------------------------
2361
- # 6. shutil file operations
2362
- # ------------------------------------------------------------
2363
- import shutil as _shutil_module
2364
-
2365
- # shutil.copy(src, dst)
2366
- _orig_shutil_copy = _shutil_module.copy
2367
- def _redir_shutil_copy(src, dst, *args, **kwargs):
2368
- if _should_redirect(src):
2369
- src = '/host' + src
2370
- if _should_redirect(dst):
2371
- dst = '/host' + dst
2372
- return _orig_shutil_copy(src, dst, *args, **kwargs)
2373
- _shutil_module.copy = _make_secure_wrapper(_redir_shutil_copy, 'copy')
2374
-
2375
- # shutil.copy2(src, dst)
2376
- _orig_shutil_copy2 = _shutil_module.copy2
2377
- def _redir_shutil_copy2(src, dst, *args, **kwargs):
2378
- if _should_redirect(src):
2379
- src = '/host' + src
2380
- if _should_redirect(dst):
2381
- dst = '/host' + dst
2382
- return _orig_shutil_copy2(src, dst, *args, **kwargs)
2383
- _shutil_module.copy2 = _make_secure_wrapper(_redir_shutil_copy2, 'copy2')
2384
-
2385
- # shutil.copyfile(src, dst)
2386
- _orig_shutil_copyfile = _shutil_module.copyfile
2387
- def _redir_shutil_copyfile(src, dst, *args, **kwargs):
2388
- if _should_redirect(src):
2389
- src = '/host' + src
2390
- if _should_redirect(dst):
2391
- dst = '/host' + dst
2392
- return _orig_shutil_copyfile(src, dst, *args, **kwargs)
2393
- _shutil_module.copyfile = _make_secure_wrapper(_redir_shutil_copyfile, 'copyfile')
2394
-
2395
- # shutil.copytree(src, dst)
2396
- _orig_shutil_copytree = _shutil_module.copytree
2397
- def _redir_shutil_copytree(src, dst, *args, **kwargs):
2398
- if _should_redirect(src):
2399
- src = '/host' + src
2400
- if _should_redirect(dst):
2401
- dst = '/host' + dst
2402
- return _orig_shutil_copytree(src, dst, *args, **kwargs)
2403
- _shutil_module.copytree = _make_secure_wrapper(_redir_shutil_copytree, 'copytree')
2404
-
2405
- # shutil.move(src, dst)
2406
- _orig_shutil_move = _shutil_module.move
2407
- def _redir_shutil_move(src, dst, *args, **kwargs):
2408
- if _should_redirect(src):
2409
- src = '/host' + src
2410
- if _should_redirect(dst):
2411
- dst = '/host' + dst
2412
- return _orig_shutil_move(src, dst, *args, **kwargs)
2413
- _shutil_module.move = _make_secure_wrapper(_redir_shutil_move, 'move')
2414
-
2415
- # shutil.rmtree(path)
2416
- _orig_shutil_rmtree = _shutil_module.rmtree
2417
- def _redir_shutil_rmtree(path, *args, **kwargs):
2418
- if _should_redirect(path):
2419
- path = '/host' + path
2420
- return _orig_shutil_rmtree(path, *args, **kwargs)
2421
- _shutil_module.rmtree = _make_secure_wrapper(_redir_shutil_rmtree, 'rmtree')
2422
-
2423
- # ------------------------------------------------------------
2424
- # 7. pathlib.Path - redirect path resolution
2425
- # ------------------------------------------------------------
2426
- from pathlib import Path, PurePosixPath
2427
-
2428
- def _redirect_path(p):
2429
- """Convert a Path to redirect /absolute paths to /host."""
2430
- s = str(p)
2431
- if _should_redirect(s):
2432
- return Path('/host' + s)
2433
- return p
2434
-
2435
- # Helper to create method wrappers for Path
2436
- def _wrap_path_method(orig_method, name):
2437
- def wrapper(self, *args, **kwargs):
2438
- redirected = _redirect_path(self)
2439
- return getattr(redirected, '_orig_' + name)(*args, **kwargs)
2440
- return wrapper
2441
-
2442
- # Store original methods with _orig_ prefix, then replace with redirecting versions
2443
- # Path.stat()
2444
- Path._orig_stat = Path.stat
2445
- def _path_stat(self, *args, **kwargs):
2446
- return _redirect_path(self)._orig_stat(*args, **kwargs)
2447
- Path.stat = _path_stat
2448
-
2449
- # Path.exists()
2450
- Path._orig_exists = Path.exists
2451
- def _path_exists(self):
2452
- return _redirect_path(self)._orig_exists()
2453
- Path.exists = _path_exists
2454
-
2455
- # Path.is_file()
2456
- Path._orig_is_file = Path.is_file
2457
- def _path_is_file(self):
2458
- return _redirect_path(self)._orig_is_file()
2459
- Path.is_file = _path_is_file
2460
-
2461
- # Path.is_dir()
2462
- Path._orig_is_dir = Path.is_dir
2463
- def _path_is_dir(self):
2464
- return _redirect_path(self)._orig_is_dir()
2465
- Path.is_dir = _path_is_dir
2466
-
2467
- # Path.open()
2468
- Path._orig_open = Path.open
2469
- def _path_open(self, *args, **kwargs):
2470
- return _redirect_path(self)._orig_open(*args, **kwargs)
2471
- Path.open = _path_open
2472
-
2473
- # Path.read_text()
2474
- Path._orig_read_text = Path.read_text
2475
- def _path_read_text(self, *args, **kwargs):
2476
- return _redirect_path(self)._orig_read_text(*args, **kwargs)
2477
- Path.read_text = _path_read_text
2478
-
2479
- # Path.read_bytes()
2480
- Path._orig_read_bytes = Path.read_bytes
2481
- def _path_read_bytes(self):
2482
- return _redirect_path(self)._orig_read_bytes()
2483
- Path.read_bytes = _path_read_bytes
2484
-
2485
- # Path.write_text()
2486
- Path._orig_write_text = Path.write_text
2487
- def _path_write_text(self, *args, **kwargs):
2488
- return _redirect_path(self)._orig_write_text(*args, **kwargs)
2489
- Path.write_text = _path_write_text
2490
-
2491
- # Path.write_bytes()
2492
- Path._orig_write_bytes = Path.write_bytes
2493
- def _path_write_bytes(self, data):
2494
- return _redirect_path(self)._orig_write_bytes(data)
2495
- Path.write_bytes = _path_write_bytes
2496
-
2497
- # Path.mkdir()
2498
- Path._orig_mkdir = Path.mkdir
2499
- def _path_mkdir(self, *args, **kwargs):
2500
- return _redirect_path(self)._orig_mkdir(*args, **kwargs)
2501
- Path.mkdir = _path_mkdir
2502
-
2503
- # Path.rmdir()
2504
- Path._orig_rmdir = Path.rmdir
2505
- def _path_rmdir(self):
2506
- return _redirect_path(self)._orig_rmdir()
2507
- Path.rmdir = _path_rmdir
2508
-
2509
- # Path.unlink()
2510
- Path._orig_unlink = Path.unlink
2511
- def _path_unlink(self, *args, **kwargs):
2512
- return _redirect_path(self)._orig_unlink(*args, **kwargs)
2513
- Path.unlink = _path_unlink
2514
-
2515
- # Path.iterdir()
2516
- Path._orig_iterdir = Path.iterdir
2517
- def _path_iterdir(self):
2518
- redirected = _redirect_path(self)
2519
- for p in redirected._orig_iterdir():
2520
- # Strip /host prefix from results
2521
- s = str(p)
2522
- if s.startswith('/host'):
2523
- yield Path(s[5:])
2524
- else:
2525
- yield p
2526
- Path.iterdir = _path_iterdir
2527
-
2528
- # Path.glob()
2529
- Path._orig_glob = Path.glob
2530
- def _path_glob(self, pattern):
2531
- redirected = _redirect_path(self)
2532
- for p in redirected._orig_glob(pattern):
2533
- s = str(p)
2534
- if s.startswith('/host'):
2535
- yield Path(s[5:])
2536
- else:
2537
- yield p
2538
- Path.glob = _path_glob
2539
-
2540
- # Path.rglob()
2541
- Path._orig_rglob = Path.rglob
2542
- def _path_rglob(self, pattern):
2543
- redirected = _redirect_path(self)
2544
- for p in redirected._orig_rglob(pattern):
2545
- s = str(p)
2546
- if s.startswith('/host'):
2547
- yield Path(s[5:])
2548
- else:
2549
- yield p
2550
- Path.rglob = _path_rglob
2551
-
2552
- _jb_init_sandbox()
2553
- del _jb_init_sandbox
2554
-
2555
- # Per-run: scrub blocked modules from sys.modules.
2556
- # Pyodide's C-level JsFinder may re-insert 'js' etc. between runs.
2557
- # This is hardcoded inline (not a callable on builtins) so attackers cannot neuter it.
2558
- for _jb_k in list(sys.modules.keys()):
2559
- if _jb_k in frozenset({'js', 'pyodide', 'pyodide_js', 'pyodide.ffi', '_pyodide', '_pyodide_core', 'ctypes', '_ctypes', '_jb_http_bridge'}) or \\
2560
- any(_jb_k.startswith(_p) for _p in ('js.', 'pyodide.', 'pyodide_js.', '_pyodide.', '_pyodide_core.', 'ctypes.', '_ctypes.', '_jb_http_bridge.')):
2561
- try: del sys.modules[_jb_k]
2562
- except (KeyError, ImportError): pass
2563
- del _jb_k
2564
-
2565
- # Set cwd to host mount
2566
- os.chdir('/host' + ${JSON.stringify(input.cwd)})
2365
+ `;
2366
+ }
2367
+ var cachedStdlibZip = new Uint8Array(readFileSync(stdlibZipPath));
2368
+ async function runPython(input) {
2369
+ const backend = new SyncFsBackend(input.sharedBuffer);
2370
+ const createPythonModule = require2(`${cpythonDir}/python.cjs`);
2371
+ let moduleReady = false;
2372
+ const pendingStdout = [];
2373
+ const pendingStderr = [];
2374
+ let Module;
2375
+ try {
2376
+ Module = await createPythonModule({
2377
+ noInitialRun: true,
2378
+ preRun: [
2379
+ (mod) => {
2380
+ mod.FS.mkdirTree("/lib");
2381
+ mod.FS.writeFile("/lib/python313.zip", cachedStdlibZip);
2382
+ mod.ENV.PYTHONHOME = "/";
2383
+ mod.ENV.PYTHONPATH = "/lib/python313.zip";
2384
+ }
2385
+ ],
2386
+ print: (text) => {
2387
+ if (moduleReady) {
2388
+ backend.writeStdout(`${text}
2389
+ `);
2390
+ } else {
2391
+ pendingStdout.push(`${text}
2392
+ `);
2393
+ }
2394
+ },
2395
+ printErr: (text) => {
2396
+ if (typeof text === "string" && (text.includes("Could not find platform") || text.includes("LLVM Profile Error"))) {
2397
+ return;
2398
+ }
2399
+ if (moduleReady) {
2400
+ backend.writeStderr(`${text}
2401
+ `);
2402
+ } else {
2403
+ pendingStderr.push(`${text}
2567
2404
  `);
2405
+ }
2406
+ }
2407
+ });
2568
2408
  } catch (e) {
2569
2409
  return {
2570
2410
  success: false,
2571
- error: `Failed to set up environment: ${e.message}`
2411
+ error: `Failed to load CPython: ${e.message}`
2572
2412
  };
2573
2413
  }
2414
+ activateDefense();
2415
+ moduleReady = true;
2416
+ for (const text of pendingStdout) backend.writeStdout(text);
2417
+ for (const text of pendingStderr) backend.writeStderr(text);
2418
+ const HOSTFS = createHOSTFS(backend, Module.FS, Module.PATH);
2574
2419
  try {
2575
- const wrappedCode = `
2420
+ Module.FS.mkdir("/host");
2421
+ Module.FS.mount(HOSTFS, { root: "/" }, "/host");
2422
+ } catch (e) {
2423
+ return {
2424
+ success: false,
2425
+ error: `Failed to mount HOSTFS: ${e.message}`
2426
+ };
2427
+ }
2428
+ const HTTPFS = createHTTPFS(backend, Module.FS);
2429
+ try {
2430
+ Module.FS.mkdir("/_jb_http");
2431
+ Module.FS.mount(HTTPFS, { root: "/" }, "/_jb_http");
2432
+ } catch (e) {
2433
+ return {
2434
+ success: false,
2435
+ error: `Failed to mount HTTPFS: ${e.message}`
2436
+ };
2437
+ }
2438
+ const setupCode = generateSetupCode(input);
2439
+ const httpBridgeCode = generateHttpBridgeCode();
2440
+ const wrappedCode = `
2576
2441
  import sys
2577
2442
  _jb_exit_code = 0
2578
2443
  try:
2444
+ ${setupCode.split("\n").map((line) => ` ${line}`).join("\n")}
2445
+ ${httpBridgeCode.split("\n").map((line) => ` ${line}`).join("\n")}
2579
2446
  ${input.pythonCode.split("\n").map((line) => ` ${line}`).join("\n")}
2580
2447
  except SystemExit as e:
2581
2448
  _jb_exit_code = e.code if isinstance(e.code, int) else (1 if e.code else 0)
2449
+ except Exception as e:
2450
+ import traceback
2451
+ traceback.print_exc()
2452
+ _jb_exit_code = 1
2453
+ sys.exit(_jb_exit_code)
2582
2454
  `;
2583
- await pyodide.runPythonAsync(wrappedCode);
2584
- const exitCode = pyodide.globals.get("_jb_exit_code");
2455
+ try {
2456
+ Module.FS.mkdir("/tmp");
2457
+ } catch (_e) {
2458
+ }
2459
+ const encoder = new TextEncoder();
2460
+ const scriptPath = "/tmp/_jb_script.py";
2461
+ const scriptData = encoder.encode(wrappedCode);
2462
+ Module.FS.writeFile(scriptPath, scriptData);
2463
+ try {
2464
+ const ret = Module.callMain([scriptPath]);
2465
+ const exitCode = (typeof ret === "number" ? ret : 0) || process.exitCode || 0;
2585
2466
  backend.exit(exitCode);
2586
2467
  return { success: true };
2587
2468
  } catch (e) {
2588
2469
  const error = e;
2589
- backend.writeStderr(`${error.message}
2590
- `);
2591
- backend.exit(1);
2470
+ const exitCode = error.status ?? process.exitCode ?? 1;
2471
+ backend.exit(exitCode);
2592
2472
  return { success: true };
2593
2473
  }
2594
2474
  }
2595
2475
  var defense = null;
2596
- async function initializeWithDefense() {
2597
- await getPyodide();
2476
+ function activateDefense() {
2477
+ if (defense) return;
2598
2478
  defense = new WorkerDefenseInDepth({
2599
2479
  excludeViolationTypes: [
2600
- "proxy",
2601
- "setImmediate",
2602
- // 3. SharedArrayBuffer/Atomics: Used by sync-fs-backend.ts for synchronous
2603
- // filesystem communication between Pyodide's WASM thread and the main thread.
2604
- // Without this, Pyodide cannot perform synchronous file I/O operations.
2480
+ // SharedArrayBuffer/Atomics: Used by sync-fs-backend.ts for synchronous
2481
+ // filesystem communication between the WASM thread and the main thread.
2605
2482
  "shared_array_buffer",
2606
2483
  "atomics"
2607
2484
  ],
@@ -2610,9 +2487,15 @@ async function initializeWithDefense() {
2610
2487
  }
2611
2488
  });
2612
2489
  }
2490
+ process.on("uncaughtException", (e) => {
2491
+ parentPort?.postMessage({
2492
+ success: false,
2493
+ error: `Worker uncaught exception: ${e.message}`
2494
+ });
2495
+ });
2613
2496
  if (parentPort) {
2614
2497
  if (workerData) {
2615
- initializeWithDefense().then(() => runPython(workerData)).then((result) => {
2498
+ runPython(workerData).then((result) => {
2616
2499
  result.defenseStats = defense?.getStats();
2617
2500
  parentPort?.postMessage(result);
2618
2501
  }).catch((e) => {
@@ -2623,20 +2506,4 @@ if (parentPort) {
2623
2506
  });
2624
2507
  });
2625
2508
  }
2626
- parentPort.on("message", async (input) => {
2627
- try {
2628
- if (!defense) {
2629
- await initializeWithDefense();
2630
- }
2631
- const result = await runPython(input);
2632
- result.defenseStats = defense?.getStats();
2633
- parentPort?.postMessage(result);
2634
- } catch (e) {
2635
- parentPort?.postMessage({
2636
- success: false,
2637
- error: e.message,
2638
- defenseStats: defense?.getStats()
2639
- });
2640
- }
2641
- });
2642
2509
  }