userland.rundocs70Try the demo
Help

Limitations

What doesn't work, or works differently, in the current release. Each item is either recorded in the nano repository or was observed while writing these docs against SDK 0.2.10 (nano v0.2.1 runtime) in Chromium; the samples show the behavior live.

Items marked 0.2.10 were observed with the SDK this site runs; fixes are in progress upstream. The changelog tracks what lands.

Node on the host engine

The host-engine runner ("nodert") is checked against the real Node.js v25.4.0 running on the RISC-V runner. These differences are recorded in runners/node/DIVERGENCES.md:

Id
Area
On the host engine
Real Node
DIV-001
process.platform / arch
linux / x64 (on purpose, for package compatibility)
linux / riscv64 on the VM
DIV-BUF-M0, DIV-CONSOLE-M0, DIV-FS-M0, DIV-URL-M0
Buffer, console, fs, url
lean implementations (no console colors; fs covers sync, promises and callbacks)
upstream modules
DIV-INSPECT-PROMISE
util.inspect of promises/proxies
degraded output
full introspection
DIV-NET-M0
net
loopback only (kernel pipes)
TCP
DIV-HTTP-M0
http
HTTP/1.1 with Content-Length bodies; no chunked request parsing
llhttp
DIV-CRYPTO-M0
crypto
sha256, sha1, HMAC, random
full OpenSSL/BoringSSL
DIV-SQLITE-DUCKDB
node:sqlite
backed by DuckDB-wasm
embedded SQLite
DIV-ESM-CYCLE, DIV-ESM-DATAURL
ES modules
cycles concatenated into one module; blob:/data: module URLs
separate module records
DIV-WT-JSON
worker_threads
JSON messages over kernel pipes
structured clone, SAB transfer
DIV-RSPACK
rspack
unsupported
native bundler
DIV-VM-POW
**
correct (2**10 === 1024)
the emulated VM's FP pow is off by an ulp

Observed with SDK 0.2.10 in the browser (the SDK doesn't register every kernel service the node tier can use):

  • require("zlib") functions fail with ENOSYS: no service 'zlib'; node:sqlite fails with no service 'duckdb'.
  • child_process fails with ENOSYS: no vm spawn delegate registered.
  • vm.runInNewContext throws a TypeError.
  • http2 throws ERR_NODE_HOST_UNSUPPORTED; engines.node: "auto" does not retry such a program on the VM.
  • An ES module entry file (node /work/app.mjs) exits 0 without output.
  • A relative entry path (node(["main.js"], { cwd })) exits 1 without output; pass an absolute path.
  • Global fetch() doesn't complete before the process exits, and https.get() connects to a local port; use /dev/__net__ (see Networking).
SDK · JavaScript

Check a few of them

javascript
const probe = async (label, src) => {
  const r = await nano.node(["-e", src]);
  print(label.padEnd(14), "exit", r.exitCode, "|", r.stdout.split("\n")[0].slice(0, 60));
};
await probe("arch", "console.log(process.platform, process.arch)");
await probe("zlib", "require('zlib').gzipSync('x')");
await probe("child_process", "require('child_process').execSync('true')");
await probe("http2", "try { require('http2').connect('https://x') } catch (e) { console.log(e.code) }");
loading…
Expected output
arch           exit 0 | linux x64
zlib           exit 1 | ENOSYS: no service 'zlib'
child_process  exit 1 | ENOSYS: no vm spawn delegate registered
http2          exit 0 | ERR_NODE_HOST_UNSUPPORTED

The RISC-V runner

  • No symlinkat / linkat: ln -s inside the guest doesn't create a link. 98 syscalls are handled; others return ENOSYS (Syscalls).
  • Signals: rt_sigaction and rt_sigprocmask are stubs, so handlers never run; kill is a no-op.
  • Memory isn't returned: munmap is a stub and brk/mmap are bump allocators.
  • tar czf produces an empty archive and tar cf - to a pipe fails; write a .tar file, then gzip it.
  • Only 67 BusyBox applets are linked into /bin in console.wasm; call the rest as busybox <applet> (for example busybox httpd, busybox nproc).
  • BusyBox httpd answers a single connection (0.2.10); use Node.js servers for previews.
  • Speed: interpreted, roughly 50× to 1,600× slower than native depending on the workload (Performance).

The wasm runner (WASI preview 1)

  • No termios / raw TTY (WASM-TERM): line-oriented tools only.
  • No signals (WASM-SIGNAL): any kill terminates the app.
  • No sockets (WASM-SOCK): sock_* returns ENOTSUP.
  • cwd is the only preopened directory: paths outside it (..) fail with "Capabilities insufficient".
  • A registered app runs through execWasmApp only; nano.run("photon …") and shell lines report "not found" (0.2.10).
  • wasm-component artifacts are reserved and not runnable yet.

SDK

  • console.wasm embeds a single ELF (BusyBox) that the runtime also uses as its Node.js ELF: NodeRuntime.warmup() fails and VM node() without cwd/env runs BusyBox ("node: applet not found"). Use the slim nano.wasm with the catalog's node (0.2.10).
  • Interactive stdin blocks the page on the main thread (observed with SDK 0.2.6); use the worker.
  • writeStdin() before a run is discarded; redirect from a file instead.
  • The Shell sends node … lines to nano.node(), so a RISC-V node installed from the catalog is reached only through shExec or with engines.node: "vm".
  • Shell builtins: only one plain export KEY=value per line is tracked.
  • provision() restores the warmup-time filesystem on every run: pass files as extraFiles. It reloads the page once if the service worker doesn't control it yet. ProvisionedApp.sh() runs one applet without shell parsing.
  • Catalog: the current index (generation 12) has no wasm-app or wasm-service entries.

Scripting (Boa)

  • With webapis: ["timers"], setTimeout callbacks never run and awaiting a timer rejects with "script did not settle" (0.2.10).
  • timeoutMs is checked between async host calls; it can't interrupt synchronous code, so always set limits.
  • evalModule() resolves to null and imports are disabled.
  • Values cross the boundary as JSON.

Terminal

  • SDK 0.2.6 and 0.2.7 shipped a terminal without dispose() (removing the element left its VM running); 0.2.8 and later dispose on removal.
  • showPreview() of a server started from the terminal's interactive shell couldn't be verified on a page that had mounted another terminal before (the preview answered from the disposed one); there is no live sample for it.

Networking

  • Outbound traffic only through /dev/__net__, i.e. the browser's fetch(): CORS applies, CORS-blocked targets return 502 Bad Gateway unless you run the CORS proxy.
  • No raw TCP/UDP, no ICMP, no DNS; wget and Node's own sockets on the RISC-V runner can't resolve hosts.

Browsers and memory

  • Cross-origin isolation is mandatory (Requirements).
  • One VM's memory is at most 2 GiB (wasm32); Node.js on the RISC-V runner needs about 1.8 GB.
  • The site's samples are tested on Chromium; Firefox and Safari aren't covered by the test suite yet.

Reporting

Found something not listed here? Open an issue in the relevant repository (nano, sdk, terminal or catalog) on GitHub.