Files
my-pi/packages/mom/docs/v86.md
T

9.4 KiB

v86 Sandbox Evaluation

v86 is an x86 emulator written in JavaScript/WebAssembly that can run Linux in the browser or Node.js. This document details our evaluation for using it as a sandboxed execution environment.

Overview

  • What it is: x86 PC emulator (32-bit, Pentium 4 level)
  • How it works: Translates machine code to WebAssembly at runtime
  • Guest OS: Alpine Linux 3.21 (32-bit x86)
  • Available packages: Node.js 22, Python 3.12, git, curl, etc. (full Alpine repos)

Key Findings

What Works

Feature Status Notes
Outbound TCP ✅ HTTP, HTTPS, TLS all work
Outbound UDP ✅ DNS queries work
WebSocket client ✅ Can connect to external WebSocket servers
File I/O ✅ 9p filesystem for host<->guest file exchange
State save/restore ✅ ~80-100MB state files, instant resume
Package persistence ✅ Installed packages persist in saved state
npm install ✅ Works (outbound HTTPS)
git clone ✅ Works (outbound HTTPS)

What Doesn't Work

Feature Status Notes
Inbound connections ❌ VM is behind NAT (10.0.2.x), needs port forwarding
ICMP ping ❌ Userspace network stack limitation
64-bit ❌ v86 only emulates 32-bit x86

Architecture

┌─────────────────────────────────────────────────────────┐
│                      Host (Node.js)                      │
│                                                          │
│  ┌──────────────┐     ┌─────────────────────────────┐   │
│  │ rootlessRelay│◄───►│           v86               │   │
│  │  (WebSocket) │     │  ┌─────────────────────┐    │   │
│  │              │     │  │   Alpine Linux      │    │   │
│  │  - DHCP      │     │  │   - Node.js 22      │    │   │
│  │  - DNS proxy │     │  │   - Python 3.12     │    │   │
│  │  - NAT       │     │  │   - etc.            │    │   │
│  └──────────────┘     │  └─────────────────────┘    │   │
│         │             │            │                 │   │
│         │             │     9p filesystem            │   │
│         ▼             │            │                 │   │
│    Internet           │            ▼                 │   │
│                       │     Host filesystem          │   │
│                       └─────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Components & Sizes

Component Size Purpose
v86.wasm ~2 MB x86 emulator
libv86.mjs ~330 KB JavaScript runtime
seabios.bin ~128 KB BIOS
vgabios.bin ~36 KB VGA BIOS
Alpine rootfs ~57 MB Compressed filesystem (loaded on-demand)
alpine-fs.json ~160 KB Filesystem index
rootlessRelay ~75 KB Network relay
Total ~60 MB Without saved state
Saved state ~80-100 MB Optional, for instant resume

Installation

npm install v86 ws

Building the Alpine Image

v86 provides Docker tooling to build the Alpine image:

git clone https://github.com/copy/v86.git
cd v86/tools/docker/alpine

# Edit Dockerfile to add packages:
# ENV ADDPKGS=nodejs,npm,python3,git,curl

./build.sh

This creates:

  • images/alpine-fs.json - Filesystem index
  • images/alpine-rootfs-flat/ - Compressed file chunks

Network Relay Setup

v86 needs a network relay for TCP/UDP connectivity. We use rootlessRelay:

git clone https://github.com/obegron/rootlessRelay.git
cd rootlessRelay
npm install

Required Patches for Host Access

To allow the VM to connect to host services via the gateway IP (10.0.2.2), apply these patches to relay.js:

Patch 1: Disable reverse TCP handling for gateway (line ~684)

// Change:
if (protocol === 6 && dstIP === GATEWAY_IP) {
  this.handleReverseTCP(ipPacket);
  return;
}

// To:
if (false && protocol === 6 && dstIP === GATEWAY_IP) { // PATCHED
  this.handleReverseTCP(ipPacket);
  return;
}

Patch 2: Redirect gateway TCP to localhost (line ~792)

// Change:
const socket = net.connect(dstPort, dstIP, () => {

// To:
const actualDstIP = dstIP === GATEWAY_IP ? "127.0.0.1" : dstIP;
const socket = net.connect(dstPort, actualDstIP, () => {

Patch 3: Redirect gateway UDP to localhost (lines ~1431 and ~1449)

// Change:
this.udpSocket.send(payload, dstPort, dstIP, (err) => {

// To:
const actualUdpDstIP = dstIP === GATEWAY_IP ? "127.0.0.1" : dstIP;
this.udpSocket.send(payload, dstPort, actualUdpDstIP, (err) => {

Starting the Relay

ENABLE_WSS=false LOG_LEVEL=1 node relay.js
# Listens on ws://127.0.0.1:8086/

Basic Usage

import { V86 } from "v86";
import path from "node:path";

const emulator = new V86({
    wasm_path: path.join(__dirname, "node_modules/v86/build/v86.wasm"),
    bios: { url: path.join(__dirname, "bios/seabios.bin") },
    vga_bios: { url: path.join(__dirname, "bios/vgabios.bin") },
    filesystem: {
        basefs: path.join(__dirname, "images/alpine-fs.json"),
        baseurl: path.join(__dirname, "images/alpine-rootfs-flat/"),
    },
    autostart: true,
    memory_size: 512 * 1024 * 1024,
    bzimage_initrd_from_filesystem: true,
    cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable console=ttyS0",
    net_device: {
        type: "virtio",
        relay_url: "ws://127.0.0.1:8086/",
    },
});

// Capture output
emulator.add_listener("serial0-output-byte", (byte) => {
    process.stdout.write(String.fromCharCode(byte));
});

// Send commands
emulator.serial0_send("echo hello\n");

Communication Methods

1. Serial Console (stdin/stdout)

// Send command
emulator.serial0_send("ls -la\n");

// Receive output
let output = "";
emulator.add_listener("serial0-output-byte", (byte) => {
    output += String.fromCharCode(byte);
});

2. 9p Filesystem (file I/O)

// Write file to VM
const data = new TextEncoder().encode("#!/bin/sh\necho hello\n");
await emulator.create_file("/tmp/script.sh", data);

// Read file from VM
const result = await emulator.read_file("/tmp/output.txt");
console.log(new TextDecoder().decode(result));

3. Network (TCP to host services)

From inside the VM, connect to 10.0.2.2:PORT to reach localhost:PORT on the host (requires patched relay).

# Inside VM
wget http://10.0.2.2:8080/  # Connects to host's localhost:8080

State Save/Restore

// Save state (includes all installed packages, files, etc.)
const state = await emulator.save_state();
fs.writeFileSync("vm-state.bin", Buffer.from(state));

// Restore state (instant resume, ~2 seconds)
const stateBuffer = fs.readFileSync("vm-state.bin");
await emulator.restore_state(stateBuffer.buffer);

Network Setup Inside VM

After boot, run these commands to enable networking:

modprobe virtio-net
ip link set eth0 up
udhcpc -i eth0

Or as a one-liner:

modprobe virtio-net && ip link set eth0 up && udhcpc -i eth0

The VM will get IP 10.0.2.15 (or similar) via DHCP from the relay.

Performance

Metric Value
Cold boot ~20-25 seconds
State restore ~2-3 seconds
Memory usage ~512 MB (configurable)

Typical Workflow for Mom

  1. First run:

    • Start rootlessRelay
    • Boot v86 with Alpine (~25s)
    • Setup network
    • Install needed packages (apk add nodejs npm python3 git)
    • Save state
  2. Subsequent runs:

    • Start rootlessRelay
    • Restore saved state (~2s)
    • Ready to execute commands
  3. Command execution:

    • Send commands via serial0_send()
    • Capture output via serial0-output-byte listener
    • Exchange files via 9p filesystem

Alternative: fetch Backend (No Relay Needed)

For HTTP-only networking, v86 has a built-in fetch backend:

net_device: {
    type: "virtio",
    relay_url: "fetch",
}

This uses the browser/Node.js fetch() API for HTTP requests. Limitations:

  • Only HTTP/HTTPS (no raw TCP/UDP)
  • No WebSocket
  • Host access via http://<port>.external (e.g., http://8080.external)

Files Reference

After building, you need these files:

project/
├── node_modules/v86/build/
│   ├── v86.wasm
│   └── libv86.mjs
├── bios/
│   ├── seabios.bin
│   └── vgabios.bin
├── images/
│   ├── alpine-fs.json
│   └── alpine-rootfs-flat/
│       └── *.bin.zst (many files)
└── rootlessRelay/
    └── relay.js (patched)

Resources