# 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

```bash
npm install v86 ws
```

## Building the Alpine Image

v86 provides Docker tooling to build the Alpine image:

```bash
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:

```bash
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)**
```javascript
// 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)**
```javascript
// 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)**
```javascript
// 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

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

## Basic Usage

```javascript
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)

```javascript
// 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)

```javascript
// 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).

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

## State Save/Restore

```javascript
// 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:

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

Or as a one-liner:
```bash
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:

```javascript
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

- [v86 GitHub](https://github.com/copy/v86)
- [v86 Networking Docs](https://github.com/copy/v86/blob/master/docs/networking.md)
- [v86 Alpine Setup](https://github.com/copy/v86/tree/master/tools/docker/alpine)
- [rootlessRelay](https://github.com/obegron/rootlessRelay)
- [v86 npm package](https://www.npmjs.com/package/v86)
