sandboxedjs 0.1.8 → 0.1.9

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.
package/README.md CHANGED
@@ -46,6 +46,7 @@ Node 18.17+. Everything is pure JavaScript and WebAssembly — no compilation st
46
46
  | **Node.js** | Real Node semantics via Nodepod — `require`, npm packages, `http`, `fs`, streams, `worker_threads` |
47
47
  | **Python** | MicroPython on WebAssembly, mounted on the *same* filesystem |
48
48
  | **WebAssembly** | Any `wasm32-wasi` binary is an executable — [drop it in `$PATH` and run it](#running-compiled-software) |
49
+ | **x86-64 Linux** | Native ELF binaries run on an emulated CPU, with a Linux syscall layer over the same filesystem |
49
50
  | **FFmpeg** | `ffmpeg` and `ffprobe` (FFmpeg 5.1) reading and writing container files directly — [optional install](#video-and-audio) |
50
51
  | **`/proc`** | Live and synthesised — `ps`, `top`, `free` and `uptime` all read the same source |
51
52
  | **Networking** | Virtual interfaces, `/etc/hosts` resolution, in-container HTTP servers, an optional bridge to a real host port |
@@ -443,69 +444,93 @@ MicroPython, not CPython — there is no `numpy`, and `pip` can install pure-Pyt
443
444
 
444
445
  ### Running compiled software
445
446
 
446
- The container has no CPU, so it cannot execute machine code but it can execute WebAssembly, and
447
- most compiled software can be built for it. Anything targeting `wasm32-wasi` is a program here,
448
- with no registration, wrapper or configuration:
447
+ Three kinds of program run here. JavaScript and WebAssembly run natively. Native **x86-64 Linux
448
+ binaries run on an emulated CPU** the container ships an x86-64 interpreter and a Linux syscall
449
+ layer that reads and writes the same filesystem as everything else.
449
450
 
450
451
  ```
451
- $ cp jq.wasm /usr/local/bin/jq && chmod +x /usr/local/bin/jq
452
- $ echo '{"name":"ada"}' | jq .name
453
- "ada"
452
+ $ file ./mytool
453
+ ./mytool: ELF 64-bit LSB executable, x86-64, statically linked
454
+ $ ./mytool --version
454
455
  ```
455
456
 
456
- The kernel decides what a file is by its magic bytes, exactly as `binfmt_misc` does on Linux, so a
457
- `.wasm` file is dispatched through the same lookup as `grep` or a shell script. The name on `$PATH`
458
- does not have to end in `.wasm`, `argv[0]` is the name it was invoked as, and the module runs
459
- against the container's real filesystem — `path_open("data.csv")` opens the file the shell just
460
- created, and what it writes is there for `cat` afterwards.
461
-
462
- To produce a binary that works, target WASI. From Go:
457
+ A statically linked binary needs nothing but the file. Build one with:
463
458
 
464
459
  ```bash
465
- GOOS=wasip1 GOARCH=wasm go build -o mytool
460
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o mytool
466
461
  ```
467
462
 
468
- From Rust: `cargo build --target wasm32-wasip1`. From C or C++: the `wasi-sdk` clang. Zig:
469
- `-target wasm32-wasi`.
463
+ Rust: `cargo build --target x86_64-unknown-linux-musl`. C: `musl-gcc -static`.
470
464
 
471
- `wasm info <file>` reports what a module was built for before you try to run it:
465
+ `x86 info` reports what a binary needs before you run it, and `x86 trace` prints every syscall,
466
+ which is how you find out where something stalls:
472
467
 
473
468
  ```
474
- $ wasm info mytool
475
- flavour: wasi
476
- imports: wasi_snapshot_preview1
477
- threads: no
478
- entry: _start
469
+ $ x86 info ./mytool
470
+ machine: x86-64
471
+ linkage: static
479
472
  runnable: yes
480
473
  ```
481
474
 
482
- **What does not run, and how it tells you.** Every executable format is recognised by name, so a
483
- failure says what the file actually is instead of `Exec format error`:
475
+ #### Dynamically linked binaries
476
+
477
+ A binary linked against glibc needs its loader and libraries — and those are **not simulated**.
478
+ They are ordinary x86-64 code, and a machine that can run the program can run them too. Copy them
479
+ in from any Linux system and the binary works:
480
+
481
+ ```
482
+ /lib64/ld-linux-x86-64.so.2
483
+ /lib/x86_64-linux-gnu/libc.so.6
484
+ ```
485
+
486
+ That is a couple of megabytes, not a distribution. If they are missing, the failure names the exact
487
+ file it wanted rather than failing vaguely.
488
+
489
+ #### WebAssembly
490
+
491
+ Anything targeting `wasm32-wasi` is also a program, with no emulation and no speed penalty:
492
+
493
+ ```
494
+ $ cp jq.wasm /usr/local/bin/jq && chmod +x /usr/local/bin/jq
495
+ $ echo '{"name":"ada"}' | jq .name
496
+ "ada"
497
+ ```
498
+
499
+ The kernel decides what a file is by its magic bytes, exactly as `binfmt_misc` does on Linux, so
500
+ every one of these is dispatched through the same lookup as `grep` or a shell script. The name on
501
+ `$PATH` need not match the format, `argv[0]` is the name it was invoked as, and all of them see the
502
+ container's real filesystem — a binary's `open("data.csv")` opens the file the shell just created.
503
+
504
+ `wasm info <file>` reports what a module was built for.
505
+
506
+ #### What does not run, and how it tells you
484
507
 
485
508
  | File | What happens |
486
509
  |---|---|
487
- | `wasm32-wasi` module | Runs |
488
- | ELF / Mach-O / PE binary | Named by architecture, with a note that a WebAssembly build would work |
489
- | Emscripten or wasm-bindgen `.wasm` | Points at the JavaScript loader generated beside it |
510
+ | Static x86-64 Linux ELF | Runs, emulated |
511
+ | Dynamic x86-64 Linux ELF | Runs once its loader and libraries are present; names them if not |
512
+ | `wasm32-wasi` module | Runs natively |
513
+ | ELF for another architecture | Named by architecture and refused |
514
+ | Mach-O / PE binary | Named as macOS or Windows; another OS is not emulated |
515
+ | Emscripten or wasm-bindgen `.wasm` | Points at the JavaScript loader beside it |
490
516
  | WASI preview 2 component | Points at `jco transpile` |
491
517
  | `GOOS=js` build | Points at `GOOS=wasip1` |
492
- | Threaded module (shared memory) | Refused up front rather than failing deep inside startup |
493
518
 
494
- The same honesty applies to npm. A package whose `bin` is a compiled executable, or whose real
495
- payload is one of several platform-gated `optionalDependencies`, is diagnosed at install time
496
- rather than at first run:
519
+ The same honesty applies to npm. A package whose `bin` is a compiled executable, whose payload
520
+ lives in platform-gated `optionalDependencies`, or which declares a `cpu` constraint, is diagnosed
521
+ at install time rather than at first run.
497
522
 
498
- ```
499
- $ npm i -g opencode-ai
500
- npm warn opencode-ai: 'opencode' points at ./bin/opencode.exe, which is a compiled
501
- npm warn executable, not JavaScript.
502
- npm warn Its real binary comes from 12 platform packages (opencode-linux-arm64, …),
503
- npm warn each of which is native machine code for one OS and CPU.
504
- ```
523
+ #### Limits of the emulator
505
524
 
506
- Limits worth knowing: no threads (`wasi-threads` modules are refused), no sockets (`sock_*`
507
- returns `ENOTSUP`), and `poll_oneoff` supports clock subscriptions only. Standard input is read to
508
- completion before a module starts, because WASI's read is synchronous.
525
+ - **Speed.** An interpreter, not a JIT expect it to be one to two orders of magnitude slower
526
+ than native. Fine for a CLI, wrong for a compiler.
527
+ - **Threads are cooperative.** `clone` creates a real thread with its own registers and stack, but
528
+ only one runs at a time, and switches happen at `futex`, `sched_yield` and `nanosleep`. A thread
529
+ that spins on memory without entering the kernel keeps the machine to itself.
530
+ - **Integer and the common vector instructions only.** Moves, bitwise operations, byte compares
531
+ and scalar floating point are implemented; the wider AVX paths are not. An unimplemented
532
+ instruction names itself and dumps the registers instead of misbehaving quietly.
533
+ - **No sockets, no signals, no `fork`/`exec`.** A guest cannot start another process.
509
534
 
510
535
  ### Filesystem from the host
511
536
 
@@ -767,10 +792,12 @@ Honest list of what does not work:
767
792
  pipeline as WebAssembly and runs fine, dev server included, as do Express, Koa, Fastify-style
768
793
  apps and plain `http` servers.
769
794
  - **Python is MicroPython**, so C extensions (`numpy`, `pandas`, `cryptography`) are unavailable.
770
- - **No machine code.** ELF, Mach-O and PE binaries cannot run, and neither can npm packages that
771
- are a wrapper around one. A `wasm32-wasi` build of the same tool does run see
772
- [Running compiled software](#running-compiled-software) but nothing converts a native binary
773
- into one for you.
795
+ - **The x86-64 emulator is an interpreter**, so native binaries run one to two orders of magnitude
796
+ slower than they would on a real CPU, its threads are cooperative rather than parallel, and it
797
+ implements the integer and common vector instructions rather than all of AVX. See
798
+ [Running compiled software](#running-compiled-software).
799
+ - **macOS and Windows binaries cannot run.** Another instruction set is a matter of emulation;
800
+ another operating system is not.
774
801
  - **No WebAssembly threads.** Modules built with shared memory are refused at startup.
775
802
  - **No real sockets.** HTTP servers work through the request proxy; raw TCP/UDP does not.
776
803
  - **No real processes.** Processes are cooperative async tasks: `kill -9` cannot interrupt a
@@ -844,6 +871,39 @@ await box.exec("greet there | tr a-z A-Z"); // → HELLO THERE
844
871
 
845
872
  It becomes a real file in `/usr/bin`, so `which greet`, `man greet` and shebang dispatch all work.
846
873
 
874
+ ### Adding an executable format
875
+
876
+ The kernel identifies executables by magic bytes and dispatches to a registered handler, the way
877
+ `binfmt_misc` does. WebAssembly is one such handler; so is each native format. Registering your own
878
+ is the supported way to teach the container a new kind of binary — including plugging in an
879
+ emulator:
880
+
881
+ ```ts
882
+ import { createContainer, type ExecFormat } from "sandboxedjs";
883
+
884
+ const box = await createContainer();
885
+
886
+ const upx: ExecFormat = {
887
+ name: "upx",
888
+ priority: 5, // consulted before the ELF handler
889
+ matches: (head) => head.subarray(0, 4).toString() === "UPX!",
890
+ describe: () => "UPX-compressed executable",
891
+ run: async (ctx, path) => {
892
+ /* decompress, then hand off however you like */
893
+ return 0;
894
+ },
895
+ };
896
+
897
+ box.kernel.formats.register(upx);
898
+ ```
899
+
900
+ A handler with no `run` is still useful: `explain(head, path)` returns the lines printed in place
901
+ of `Exec format error`, which is how ELF, Mach-O and PE files report what they are.
902
+
903
+ The x86-64 handler in `src/runtime/x86/` is the worked example: it matches on `\x7fELF` and its
904
+ `run` boots an interpreter, a Linux syscall layer and a process image. A handler for another
905
+ architecture would sit alongside it without the kernel changing at all.
906
+
847
907
  ## Examples
848
908
 
849
909
  See [`examples/`](./examples): a REPL, an Express API, a React app, a Python data pipeline, an
@@ -187,6 +187,7 @@ async function printReplBanner(box, opts) {
187
187
  row("node", node, "npm, npm -g, require, http servers"),
188
188
  row("python3", python && `MicroPython`, "no C extensions (numpy, pandas)"),
189
189
  row("wasm", "wasi p1", "wasm32-wasi binaries run as commands"),
190
+ row("x86-64", "emulated", "static Linux ELF binaries run too"),
190
191
  row("ffmpeg", ffmpeg, ffmpeg ? "video and audio" : "npm install @ffmpeg/core"),
191
192
  row(
192
193
  "network",
@@ -202,11 +203,12 @@ async function printReplBanner(box, opts) {
202
203
  " npm i -g prettier && prettier --version",
203
204
  " npm create vite@latest app -- --template react && cd app && npm i && npm run dev",
204
205
  " wasm info tool.wasm && chmod +x tool.wasm && ./tool.wasm",
206
+ " x86 info ./linux-binary && ./linux-binary # native ELF, emulated",
205
207
  "",
206
- `${DIM}Software runs here if it is JavaScript or WebAssembly. Any wasm32-wasi`,
207
- "build is a command: drop it in /usr/local/bin, chmod +x, call it by name.",
208
- "Compiled native binaries (ELF, Mach-O, PE) cannot run and say so by name —",
209
- `so can npm CLIs that are only a wrapper around one. No raw sockets.${RESET}`,
208
+ `${DIM}JavaScript and WebAssembly run natively. Static x86-64 Linux binaries run`,
209
+ "on an emulated CPU slower, and threads are cooperative. Dynamically linked",
210
+ "ones also need their loader and libc copied in. macOS and Windows binaries",
211
+ `cannot run and say so by name. No raw sockets.${RESET}`,
210
212
  "",
211
213
  `${DIM}Ctrl-D or \`exit\` to leave.${RESET}`,
212
214
  "",