@david.uhlir/vm-machine
v0.1.16
Published
Helper to evaluate code in a virtual machine environment.
Readme
VM-machine
A powerful and secure library for executing JavaScript code in isolated virtual machines using Node.js isolated-vm. Perfect for running untrusted code, creating sandboxed execution environments, and building extensible applications with plugin systems.
Features
- Secure Execution: Run JavaScript in completely isolated V8 contexts
- Memory Management: Configurable memory limits and automatic cleanup
- Modular Library System: Extensible plugin architecture with built-in libraries
- IPC Communication: Safe communication between host and VM contexts
- Error Handling: Comprehensive error handling with timeout support
- TypeScript Support: Full TypeScript definitions included
- Production Ready: Battle-tested with comprehensive test suite
Installation
npm install @david.uhlir/vm-machineQuick Start
import { VM, VMConsoleLibrary, VMIpcLibrary, VMIpcRxLibrary } from '@david.uhlir/vm-machine'
// Create libraries
const consoleLib = new VMConsoleLibrary()
const ipcRxLib = new VMIpcRxLibrary({
calculateSum: async (a: number, b: number) => a + b
})
const ipcTxLib = new VMIpcLibrary()
// Initialize VM
const vm = new VM([consoleLib, ipcRxLib, ipcTxLib])
await vm.initialize()
// Execute code
const code = `
console.log('Hello from VM!');
async function main() {
const result = await calculateSum(5, 10);
return { message: 'Calculation done', result };
}
return { main };
`
await vm.run(code)
// Call VM function
const result = await ipcTxLib.callIpc('main', [])
console.log(result) // { message: 'Calculation done', result: 15 }
// Cleanup
vm.dispose()Complete Example: JavaScript Project Runner
Here's a comprehensive example showing how to run a complete JavaScript project with filesystem access, modules, and monitoring:
import {
VM,
VMConsoleLibrary,
VMCodeBaseLibrary,
VMMeasureLibrary,
VMIpcRxLibrary,
VMIpcLibrary,
VMTimingLibrary
} from '@david.uhlir/vm-machine'
async function runJavaScriptProject() {
// Setup filesystem and module system
const codebaseLib = new VMCodeBaseLibrary({
maxMemfsSize: 10 * 1024 * 1024, // 10MB
maxFileCount: 1000,
moduleResolver: async (name) => {
// Custom module resolver for external libraries
if (name === 'axios') {
return `module.exports = { get: async (url) => ({ data: 'mocked response' }) };`
}
return undefined
}
})
// Setup performance monitoring
const measureLib = new VMMeasureLibrary({
responseTimeLimit: 5000 // 5 second limit
})
// Setup host functions
const ipcRxLib = new VMIpcRxLibrary({
saveToDatabase: async (data: any) => {
console.log('Saving to database:', data)
return { id: Date.now(), success: true }
},
fetchApiData: async (endpoint: string) => {
console.log('Fetching from API:', endpoint)
return { data: 'API response data', timestamp: Date.now() }
}
})
const ipcTxLib = new VMIpcLibrary({ timeout: 10000 })
const consoleLib = new VMConsoleLibrary()
const timingLib = new VMTimingLibrary({ limitCount: 50 })
// Create VM with all libraries
const vm = new VM([
consoleLib,
codebaseLib,
measureLib,
ipcRxLib,
ipcTxLib,
timingLib
], {
memoryLimit: 128, // 128MB
onError: (error) => console.error('VM Error:', error),
onCatastrophicError: (error) => {
console.error('CRITICAL VM ERROR:', error)
process.exit(1)
}
})
try {
// Get virtual filesystem
const fs = codebaseLib.getFs()
// Create project structure
await fs.promises.mkdir('/src', { recursive: true })
await fs.promises.mkdir('/config', { recursive: true })
// Add configuration
await fs.promises.writeFile('/config/app.json', JSON.stringify({
apiUrl: 'https://api.example.com',
retries: 3,
timeout: 5000
}))
// Add utility module
await fs.promises.writeFile('/src/utils.js', `
const fs = require('fs/promises');
async function loadConfig() {
const configData = await fs.readFile('/config/app.json', 'utf-8');
return JSON.parse(configData);
}
function processData(data, multiplier = 1) {
return data.map(item => ({
...item,
value: item.value * multiplier,
processed: true,
timestamp: new Date().toISOString()
}));
}
module.exports = { loadConfig, processData };
`)
// Add main application
await fs.promises.writeFile('/src/app.js', `
const utils = require('./utils');
const axios = require('axios'); // Resolved by custom resolver
async function processApiData() {
console.log('Starting data processing...');
// Load configuration
const config = await utils.loadConfig();
console.log('Loaded config:', config);
// Fetch data from API (via host function)
const response = await fetchApiData('/users');
console.log('API Response:', response);
// Simulate data processing
const mockData = [
{ id: 1, value: 10 },
{ id: 2, value: 20 },
{ id: 3, value: 30 }
];
const processed = utils.processData(mockData, 2);
console.log('Processed data:', processed);
// Save to database (via host function)
const saveResult = await saveToDatabase({
type: 'processed_data',
data: processed,
config: config
});
console.log('Save result:', saveResult);
// Use timer for delayed operation
await new Promise(resolve => {
setTimeout(() => {
console.log('Delayed operation completed');
resolve();
}, 1000);
});
return {
success: true,
processedCount: processed.length,
saveId: saveResult.id
};
}
module.exports = { processApiData };
`)
// Set entry point
codebaseLib.setEntryFile('/src/app.js')
// Initialize and run VM
await vm.initialize()
// Start performance monitoring
const monitorInterval = setInterval(async () => {
measureLib.ping()
const stats = await measureLib.getUsageStats()
console.log(`VM Stats - Memory: ${(stats.usedMemory / 1024 / 1024).toFixed(2)}MB (${stats.memoryUsedPercentage.toFixed(1)}%), Response: ${stats.responseTime}ms`)
}, 2000)
// Run the project
await vm.run() // This will automatically execute /src/app.js
// Call the main function
const result = await ipcTxLib.callIpc('processApiData', [])
console.log('Final result:', result)
// Read any files created by the VM
try {
const files = await fs.promises.readdir('/output')
console.log('Output files created:', files)
} catch (e) {
console.log('No output directory created')
}
clearInterval(monitorInterval)
return result
} finally {
vm.dispose()
}
}
// Usage
runJavaScriptProject()
.then(result => console.log('Project completed:', result))
.catch(error => console.error('Project failed:', error))This example demonstrates:
- Virtual filesystem with project structure
- Module system with custom resolvers
- Host-VM communication for API calls and database operations
- Performance monitoring with real-time stats
- Timer support for delayed operations
- Error handling and resource management
- Resource limits and security boundaries
Complete Multi-Threading Example
Here's an advanced example showcasing the full power of VM-machine with threading capabilities:
import {
VM,
VMConsoleLibrary,
VMCodeBaseLibrary,
VMThreadLibrary,
VMMeasureMasterLibrary,
VMTimingLibrary,
VMIpcRxLibrary
} from '@david.uhlir/vm-machine'
async function runMultiThreadedApplication() {
const codebaseLib = new VMCodeBaseLibrary({
maxMemfsSize: 20 * 1024 * 1024, // 20MB
maxFileCount: 2000
})
const measureMaster = new VMMeasureMasterLibrary()
const fs = codebaseLib.getFs()
// Create worker thread for CPU-intensive tasks
await fs.promises.writeFile('/cpu-worker.js', `
console.log('CPU Worker thread started');
let taskCount = 0;
exposeCallables({
calculatePrimes: async (max) => {
taskCount++;
console.log(\`Starting prime calculation up to \${max} (task #\${taskCount})\`);
const primes = [];
for (let num = 2; num <= max; num++) {
let isPrime = true;
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) {
isPrime = false;
break;
}
}
if (isPrime) primes.push(num);
// Report progress for large calculations
if (num % 1000 === 0) {
await host.reportProgress(\`Checked \${num}/\${max} numbers\`);
}
}
await host.logResult(\`Found \${primes.length} primes up to \${max}\`);
return { primes: primes.slice(-10), count: primes.length, taskId: taskCount };
},
getWorkerStats: async () => ({
taskCount,
workerId: 'cpu-worker',
isAlive: true
})
});
`)
// Create I/O worker thread for database operations
await fs.promises.writeFile('/io-worker.js', `
console.log('I/O Worker thread started');
let operationCount = 0;
exposeCallables({
processDataBatch: async (batch) => {
operationCount++;
console.log(\`Processing batch of \${batch.length} items (op #\${operationCount})\`);
const results = [];
for (const item of batch) {
// Simulate database operation
await new Promise(resolve => setTimeout(resolve, 50));
const processed = {
...item,
processed: true,
processedAt: new Date().toISOString(),
workerId: 'io-worker',
operationId: operationCount
};
// Save to host database
const saved = await host.saveItem(processed);
results.push({ ...processed, savedId: saved.id });
}
return { results, totalProcessed: results.length };
},
getWorkerStats: async () => ({
operationCount,
workerId: 'io-worker',
isAlive: true
})
});
`)
// Create main coordinator application
await fs.promises.writeFile('/coordinator.js', `
console.log('Main coordinator started');
let cpuWorker, ioWorker;
const results = [];
async function initializeWorkers() {
console.log('Initializing worker threads...');
cpuWorker = await startThread('/cpu-worker');
ioWorker = await startThread('/io-worker');
console.log('Workers initialized:', {
cpu: cpuWorker.threadId,
io: ioWorker.threadId
});
}
async function runParallelTasks() {
console.log('Starting parallel task execution...');
// Start CPU-intensive task
const primeTask = cpuWorker.calculatePrimes(10000);
// Start I/O tasks in parallel
const batches = [
[{ id: 1, data: 'batch1' }, { id: 2, data: 'batch1' }],
[{ id: 3, data: 'batch2' }, { id: 4, data: 'batch2' }],
[{ id: 5, data: 'batch3' }, { id: 6, data: 'batch3' }]
];
const ioTasks = batches.map(batch => ioWorker.processDataBatch(batch));
// Wait for all tasks to complete
const [primeResult, ...ioResults] = await Promise.all([primeTask, ...ioTasks]);
console.log('All tasks completed!');
console.log('Prime calculation result:', {
count: primeResult.count,
lastPrimes: primeResult.primes
});
console.log('I/O processing results:', {
totalBatches: ioResults.length,
totalItems: ioResults.reduce((sum, r) => sum + r.totalProcessed, 0)
});
return { primeResult, ioResults };
}
async function monitorWorkers() {
const [cpuStats, ioStats] = await Promise.all([
cpuWorker.getWorkerStats(),
ioWorker.getWorkerStats()
]);
console.log('Worker statistics:', { cpuStats, ioStats });
return { cpuStats, ioStats };
}
async function cleanup() {
console.log('Cleaning up workers...');
if (cpuWorker && !cpuWorker.isDisposed) {
cpuWorker.dispose();
}
if (ioWorker && !ioWorker.isDisposed) {
ioWorker.dispose();
}
console.log('Cleanup complete');
}
async function main() {
try {
await initializeWorkers();
const results = await runParallelTasks();
await monitorWorkers();
// Schedule cleanup
setTimeout(cleanup, 2000);
return {
success: true,
results
};
} catch (error) {
console.error('Error in main execution:', error);
await cleanup();
throw error;
}
}
// Export the main function for external calling
exposeCallables({ main });
`)
// Setup thread library with monitoring
const threadLib = new VMThreadLibrary({
threadMemoryLimit: 64, // 64MB per thread
threadsCountLimit: 10,
getThreadLibraries: (threadId) => [
new VMConsoleLibrary((level, ...args) =>
console.log(`[THREAD-${threadId}]`, ...args)
),
new VMTimingLibrary({ limitCount: 20 }),
measureMaster.getNewLibrary(`thread-${threadId}`),
new VMIpcRxLibrary({
reportProgress: async (message) => {
console.log(`[PROGRESS-${threadId}]`, message);
return { acknowledged: true };
},
logResult: async (message) => {
console.log(`[RESULT-${threadId}]`, message);
return { logged: true };
},
saveItem: async (item) => {
console.log(`[SAVE-${threadId}]`, 'Saving item:', item.id);
return {
id: Date.now() + Math.random(),
saved: true,
timestamp: new Date().toISOString()
};
}
})
]
})
// Setup main VM with all capabilities
const vm = new VM([
new VMConsoleLibrary((level, ...args) => console.log('[MAIN]', ...args)),
codebaseLib,
threadLib,
measureMaster,
new VMTimingLibrary(),
new VMIpcRxLibrary({
getSystemInfo: async () => ({
activeThreads: threadLib.getThreads().size,
mainVmMemory: 'simulated-memory-info',
timestamp: Date.now()
})
})
], {
memoryLimit: 128, // 128MB for main VM
onError: (error) => console.error('[MAIN-ERROR]', error),
onCatastrophicError: (error) => {
console.error('[CRITICAL-ERROR]', error);
process.exit(1);
}
})
try {
// Set entry point and initialize
codebaseLib.setEntryFile('/coordinator.js')
await vm.initialize()
// Start performance monitoring
const monitorInterval = setInterval(async () => {
await measureMaster.pingAll()
const stats = await measureMaster.getCombinedStats()
console.log('[MONITOR]', 'System Performance:', {
totalMemory: `${(stats.usedMemory / 1024 / 1024).toFixed(2)} MB`,
memoryPercentage: `${stats.memoryUsedPercentage.toFixed(1)}%`,
avgResponseTime: `${stats.responseAverageTime.toFixed(2)}ms`,
maxResponseTime: `${stats.responseTime}ms`,
activeInstances: stats.managedCount,
activeThreads: threadLib.getThreads().size
})
}, 3000)
// Run the application
await vm.run() // This auto-executes coordinator.js
// Let it run for a while to see the threading in action
await new Promise(resolve => setTimeout(resolve, 10000))
clearInterval(monitorInterval)
console.log('[MAIN]', 'Application completed successfully')
} finally {
vm.dispose()
}
}
// Run the multi-threaded application
runMultiThreadedApplication()
.then(() => console.log('Multi-threaded application finished'))
.catch(error => console.error('Application failed:', error))This advanced example demonstrates:
- Multi-threading with specialized worker threads for CPU and I/O tasks
- Parallel processing with coordinated task execution
- Inter-thread communication via host functions
- Resource monitoring across all VM instances and threads
- Thread lifecycle management with automatic cleanup
- Error handling with graceful degradation
- Performance monitoring with real-time metrics
- Complex coordination between main VM and worker threads
Core API
VM Class
The main class for creating and managing virtual machines.
class VM {
constructor(libraries: VMLibrary[], options?: VMOptions)
async initialize(): Promise<void>
async run(code: string): Promise<void>
dispose(): void
}VMOptions
interface VMOptions {
memoryLimit?: number; // Memory limit in MB (default: 64)
onError?: (error: Error) => void;
onCatastrophicError?: (error: string) => void;
onEnd?: () => void;
onDispose?: () => void;
}Example with Options
const vm = new VM([consoleLib, ipcRxLib, ipcTxLib], {
memoryLimit: 128, // 128MB limit
onError: (error) => console.error('VM Error:', error),
onCatastrophicError: (error) => console.error('Catastrophic:', error),
onEnd: () => console.log('VM execution completed'),
onDispose: () => console.log('VM disposed')
})Library Exports
The package exports the following main classes:
import {
// Core VM class
VM,
// Built-in libraries
VMConsoleLibrary, // Console logging support
VMInjectLibrary, // Variable/function injection
VMIpcRxLibrary, // VM calls host functions
VMIpcLibrary, // Host calls VM functions (also known as VMIpcTxLibrary)
VMTimingLibrary, // setTimeout/setInterval support
VMCodeBaseLibrary, // Virtual filesystem with memfs and module system
VMMeasureLibrary, // Performance monitoring and resource usage
VMThreadLibrary, // Thread management utilities
// Base classes for custom libraries
VMLibrary,
VMLibraryGroup
} from '@david.uhlir/vm-machine'Note: VMIpcLibrary handles host-to-VM communication (calling VM functions from host), while VMIpcRxLibrary handles VM-to-host communication (VM calling host functions).
Built-in Libraries
VMConsoleLibrary
Provides console logging functionality to the VM.
class VMConsoleLibrary extends VMLibrary {
constructor(writeLog?: (level: string, ...args: any[]) => void)
}Usage
// Default console output
const consoleLib = new VMConsoleLibrary()
// Custom logger
const consoleLib = new VMConsoleLibrary((level, ...args) => {
console.log(`[VM-${level.toUpperCase()}]`, ...args)
})
const code = `
console.log('Info message');
console.error('Error message');
console.warn('Warning message');
`VMIpcRxLibrary & VMIpcLibrary (IPC Communication)
IPC (Inter-Process Communication) is split into two complementary libraries for clear separation of concerns:
- VMIpcRxLibrary: Provides functions that the VM can call on the host (VM receives/calls host functions)
- VMIpcLibrary: Allows the host to call functions defined in the VM with timeout support (host transmits calls to VM)
VMIpcRxLibrary
Provides callable functions from VM to host environment.
class VMIpcRxLibrary extends VMLibrary {
constructor(callables: { [name: string]: (...args: any[]) => Promise<any> })
}VMIpcLibrary
Allows host to call VM functions with timeout support.
class VMIpcLibrary extends VMLibrary {
constructor(options?: { timeout?: number })
async callIpc(name: string, args: any[]): Promise<any>
}Complete IPC Example
// Host functions callable from VM
const ipcRxLib = new VMIpcRxLibrary({
fetchUser: async (id: number) => {
return await database.users.findById(id)
},
saveData: async (data: any) => {
return await database.save(data)
},
delay: async (ms: number) => {
await new Promise(resolve => setTimeout(resolve, ms))
}
})
// VM function call handler with timeout
const ipcTxLib = new VMIpcLibrary({
timeout: 10000 // 10 second timeout for VM function calls
})
const vm = new VM([ipcRxLib, ipcTxLib])
await vm.initialize()
const code = `
// VM can call host functions (via VMIpcRxLibrary)
async function processUser(userId) {
const user = await fetchUser(userId); // Calls host function
await delay(100); // Calls host function
const processed = {
...user,
processedAt: new Date().toISOString()
};
await saveData(processed); // Calls host function
return processed;
}
// Return functions that host can call
return { processUser };
`
await vm.run(code)
// Host calls VM function (via VMIpcLibrary)
const result = await ipcTxLib.callIpc('processUser', [123])Understanding the IPC Architecture
The IPC system is designed with clear directional responsibilities:
VMIpcRxLibrary (VM Receives/Calls Host Functions)
- VM can call functions defined on the host
- Functions are injected into VM global scope
- Host provides the callable implementations
- Async execution with error propagation
VMIpcLibrary (Host Transmits Calls to VM Functions)
- Host can call functions returned by VM code
- Functions must be returned in VM's return object
- Supports timeout configuration for long-running VM functions
- Bidirectional error handling
Important Notes:
- VM functions called by host (via VMIpcLibrary) must be async
- Host functions called by VM (via VMIpcRxLibrary) must return promises
- Both libraries can be used together for full bidirectional communication
- Each library handles its own error states and timeouts
VMInjectLibrary
Injects variables and functions into the VM global scope.
class VMInjectLibrary extends VMLibrary {
constructor(injectables: { [name: string]: any })
}Usage
const injectLib = new VMInjectLibrary({
API_URL: 'https://api.example.com',
version: '1.0.0',
config: {
retries: 3,
timeout: 5000
},
helperFunction: (x: number) => x * 2
})
const code = `
console.log('API URL:', API_URL);
console.log('Version:', version);
console.log('Config:', config);
const doubled = helperFunction(21); // 42
return { doubled, config };
`VMTimingLibrary
Provides setTimeout and setInterval functionality with resource limits.
class VMTimingLibrary extends VMLibrary {
constructor(options?: VMTimingLibraryOptions)
}
interface VMTimingLibraryOptions {
limitCount?: number // Maximum number of concurrent timers (default: 100)
}Usage
const timingLib = new VMTimingLibrary({
limitCount: 50 // Limit to 50 concurrent timers
})
const code = `
let counter = 0;
const intervalId = setInterval(() => {
console.log('Counter:', ++counter);
if (counter >= 3) {
clearInterval(intervalId);
}
}, 1000);
setTimeout(() => {
console.log('Delayed execution');
}, 2500);
return { started: true };
`VMCodeBaseLibrary
Provides a virtual filesystem using memfs and a complete module system for running JavaScript projects inside the VM. This is essential for running complex applications that need file system access and module imports.
class VMCodeBaseLibrary extends VMLibraryGroup {
constructor(options?: VMCodeBaseModulesLibraryOptions)
setEntryFile(entryFile: string): void
getFs(): IFs
addModule(module: BaseCodebaseModule): void
addModule(name: string, source: string): void
}
interface VMCodeBaseModulesLibraryOptions {
mockModulesList?: string[] // List of Node.js modules to mock (default: builtin modules)
moduleResolver?: (name: string) => Promise<string | undefined>
maxMemfsSize?: number // Maximum filesystem size in bytes (default: 10MB)
maxFileCount?: number // Maximum number of files (default: 1000)
}Key Features
- Virtual Filesystem: Complete filesystem implementation using memfs
- Module System: Support for
require()and ES modules with relative/absolute imports - Resource Limits: Configurable memory and file count limits for security
- Mock Modules: Automatic mocking of Node.js built-in modules
- File Operations: Full fs/promises API support within the VM
Usage
const codebaseLib = new VMCodeBaseLibrary({
maxMemfsSize: 5 * 1024 * 1024, // 5MB limit
maxFileCount: 500,
mockModulesList: ['fs', 'path', 'crypto'] // Mock these modules
})
// Add files to the virtual filesystem
const fs = codebaseLib.getFs()
await fs.promises.writeFile('/src/utils.js', `
module.exports = {
add: (a, b) => a + b,
multiply: (a, b) => a * b
};
`)
await fs.promises.writeFile('/src/main.js', `
const utils = require('./utils');
const fs = require('fs/promises');
async function main() {
console.log('Sum:', utils.add(5, 3));
// Write result to virtual filesystem
await fs.writeFile('/output.txt', 'Result: ' + utils.multiply(4, 7));
return { success: true };
}
module.exports = { main };
`)
// Set entry file to auto-execute
codebaseLib.setEntryFile('/src/main.js')
const vm = new VM([codebaseLib, new VMConsoleLibrary()])
await vm.initialize()
await vm.run() // Will automatically execute main.js
// Access files created by VM code
const output = await fs.promises.readFile('/output.txt', 'utf-8')
console.log(output) // "Result: 28"Advanced Module System Usage
// Custom module resolver
const codebaseLib = new VMCodeBaseLibrary({
moduleResolver: async (name) => {
if (name === 'lodash') {
return `module.exports = { map: (arr, fn) => arr.map(fn) };`
}
if (name.startsWith('@mycompany/')) {
// Load from external source
return await loadFromDatabase(name)
}
return undefined
}
})
// Add modules programmatically
codebaseLib.addModule('config', `
module.exports = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
`)
const code = `
const config = require('config');
const _ = require('lodash'); // Resolved by custom resolver
const myLib = require('@mycompany/utils'); // Also resolved by custom resolver
const numbers = [1, 2, 3, 4, 5];
const doubled = _.map(numbers, x => x * 2);
return { doubled, config };
`VMThreadLibrary
Provides advanced multi-threading capabilities with isolated VM contexts for each thread. Each thread runs in its own VM with independent memory limits and can communicate bidirectionally with the main VM and host.
class VMThreadLibrary extends VMLibraryGroup {
constructor(options: VMThreadLibraryOptions)
getThreads(): Map<string, ThreadInfo>
}
interface VMThreadLibraryOptions {
threadMemoryLimit?: number // Memory limit per thread in MB (default: 16)
threadsCountLimit?: number // Maximum number of concurrent threads
getThreadLibraries: (threadId: string) => VMLibrary[] // Libraries for each thread
}
interface ThreadInfo {
vm: VM
tx: VMIpcTxLibrary // For calling thread functions
rx: VMIpcRxLibrary // For thread calling host functions
}Key Features
- Isolated Threads: Each thread runs in its own V8 isolate with independent memory
- Bidirectional Communication: Threads can call host functions and host can call thread functions
- Automatic Management: Built-in watchdog for thread health monitoring
- Resource Limits: Configurable memory and thread count limits
- Dynamic Loading: Threads can be created from filesystem or inline code
- Lifecycle Management: Automatic cleanup and disposal
Basic Usage
const codebaseLib = new VMCodeBaseLibrary()
const fs = codebaseLib.getFs()
// Create thread file
await fs.promises.writeFile('/worker.js', `
console.log('Worker thread started!');
// Expose functions that main VM can call
exposeCallables({
processData: async (data) => {
console.log('Processing data in thread:', data);
// Thread can call host functions
const result = await host.saveToDatabase({
threadId: global.isThread,
processedData: data.map(x => x * 2)
});
return { success: true, result };
},
heavyComputation: async (numbers) => {
// CPU-intensive work in isolated thread
return numbers.reduce((sum, n) => sum + Math.sqrt(n * n), 0);
}
});
`)
// Create main application
await fs.promises.writeFile('/main.js', `
console.log('Main application started');
async function main() {
// Start a worker thread
const worker = await startThread('/worker');
console.log('Worker thread ID:', worker.threadId);
// Call thread functions
const result1 = await worker.processData([1, 2, 3, 4, 5]);
console.log('Processing result:', result1);
const result2 = await worker.heavyComputation([100, 200, 300]);
console.log('Computation result:', result2);
// Clean up thread
setTimeout(() => {
console.log('Disposing worker thread');
worker.dispose();
}, 5000);
}
main();
`)
const threadLib = new VMThreadLibrary({
threadMemoryLimit: 32, // 32MB per thread
threadsCountLimit: 10, // Max 10 concurrent threads
getThreadLibraries: (threadId) => [
new VMConsoleLibrary((level, ...args) =>
console.log(`[THREAD-${threadId}]`, ...args)
),
new VMTimingLibrary(),
// Add other libraries as needed
]
})
const vm = new VM([
new VMConsoleLibrary(),
codebaseLib,
threadLib,
new VMIpcRxLibrary({
saveToDatabase: async (data) => {
console.log('Host saving data:', data);
return { id: Date.now(), saved: true };
}
})
])
codebaseLib.setEntryFile('/main.js')
await vm.initialize()
await vm.run()Advanced Thread Communication
const threadLib = new VMThreadLibrary({
getThreadLibraries: (threadId) => [
new VMConsoleLibrary(),
new VMIpcRxLibrary({
// Host functions available to threads
getDatabaseConnection: async () => ({ connected: true }),
logMessage: async (message) => console.log(`Thread ${threadId}:`, message),
notifyComplete: async (result) => {
console.log(`Thread ${threadId} completed:`, result);
return { acknowledged: true };
}
})
]
})
// Complex worker thread
await fs.promises.writeFile('/data-processor.js', `
let processingCount = 0;
exposeCallables({
startProcessing: async (config) => {
const db = await host.getDatabaseConnection();
await host.logMessage('Starting data processing with config: ' + JSON.stringify(config));
// Simulate processing
const results = [];
for (let i = 0; i < config.itemCount; i++) {
processingCount++;
// Simulate work
await new Promise(resolve => setTimeout(resolve, config.delay || 100));
results.push({
id: i,
processed: true,
timestamp: Date.now(),
threadCount: processingCount
});
// Progress updates
if (i % 10 === 0) {
await host.logMessage(\`Progress: \${i}/\${config.itemCount}\`);
}
}
await host.notifyComplete({ totalProcessed: results.length });
return results;
},
getStatus: async () => ({
processingCount,
isAlive: true,
memoryUsage: 'simulated-memory-info'
})
});
`)
// Main coordination logic
await fs.promises.writeFile('/coordinator.js', `
const workers = [];
const results = [];
async function spawnWorkers(count = 3) {
for (let i = 0; i < count; i++) {
const worker = await startThread('/data-processor');
workers.push(worker);
console.log(\`Spawned worker \${i + 1}/\${count}: \${worker.threadId}\`);
}
}
async function distributeWork() {
const tasks = workers.map((worker, index) =>
worker.startProcessing({
itemCount: 50,
delay: 50 + (index * 20), // Stagger processing
workerId: index
})
);
const workerResults = await Promise.all(tasks);
results.push(...workerResults.flat());
console.log(\`All workers completed. Total items processed: \${results.length}\`);
}
async function monitorWorkers() {
const statuses = await Promise.all(
workers.map(worker => worker.getStatus())
);
console.log('Worker statuses:', statuses);
}
async function cleanup() {
console.log('Cleaning up workers...');
workers.forEach(worker => worker.dispose());
}
async function main() {
await spawnWorkers(3);
await distributeWork();
await monitorWorkers();
setTimeout(cleanup, 1000);
}
main();
`)Thread Lifecycle and Health Monitoring
// The VMThreadLibrary automatically monitors thread health
const threadLib = new VMThreadLibrary({
threadMemoryLimit: 64,
threadsCountLimit: 5,
getThreadLibraries: (threadId) => [
new VMConsoleLibrary(),
new VMMeasureLibrary({
responseTimeLimit: 3000 // Kill thread if response > 3s
})
]
})
// Monitor thread health
setInterval(() => {
const threads = threadLib.getThreads()
console.log(`Active threads: ${threads.size}`)
threads.forEach((thread, threadId) => {
if (thread.vm.isDisposed) {
console.log(`Thread ${threadId} is disposed`)
} else {
console.log(`Thread ${threadId} is running`)
}
})
}, 2000)
// Thread with error handling
await fs.promises.writeFile('/error-prone-worker.js', `
let operationCount = 0;
exposeCallables({
riskyOperation: async (shouldFail = false) => {
operationCount++;
if (shouldFail && operationCount > 3) {
throw new Error('Simulated thread error');
}
// Simulate long-running operation
await new Promise(resolve => setTimeout(resolve, 1000));
return {
success: true,
operationCount,
timestamp: Date.now()
};
}
});
`)Performance and Resource Management
// Resource monitoring with thread library
const measureMaster = new VMMeasureMasterLibrary()
const threadLib = new VMThreadLibrary({
threadMemoryLimit: 32,
getThreadLibraries: (threadId) => [
new VMConsoleLibrary(),
measureMaster.getNewLibrary(`thread-${threadId}`) // Monitor each thread
]
})
// Monitor overall performance
setInterval(async () => {
await measureMaster.pingAll()
const stats = await measureMaster.getCombinedStats()
console.log('System Performance:', {
totalMemoryUsed: `${(stats.usedMemory / 1024 / 1024).toFixed(2)} MB`,
memoryPercentage: `${stats.memoryUsedPercentage.toFixed(1)}%`,
averageResponseTime: `${stats.responseAverageTime.toFixed(2)}ms`,
slowestResponseTime: `${stats.responseTime}ms`,
managedInstances: stats.managedCount
})
}, 2000)
// Thread pool pattern
await fs.promises.writeFile('/thread-pool.js', `
const pool = [];
const maxPoolSize = 3;
async function getWorker() {
if (pool.length > 0) {
return pool.pop();
}
if (pool.length < maxPoolSize) {
const worker = await startThread('/data-processor');
return worker;
}
throw new Error('Thread pool exhausted');
}
function releaseWorker(worker) {
if (!worker.isDisposed && pool.length < maxPoolSize) {
pool.push(worker);
} else {
worker.dispose();
}
}
async function processWithPool(data) {
const worker = await getWorker();
try {
const result = await worker.processData(data);
releaseWorker(worker);
return result;
} catch (error) {
worker.dispose(); // Don't return failed workers to pool
throw error;
}
}
// Process multiple items using thread pool
const tasks = Array.from({length: 10}, (_, i) =>
processWithPool(\`data-item-\${i}\`)
);
const results = await Promise.all(tasks);
console.log('Pool processing complete:', results.length);
`)VMMeasureLibrary
Provides performance monitoring, resource usage tracking, and response time measurement.
class VMMeasureLibrary extends VMLibrary {
constructor(options?: VMMeasureLibraryOptions, onDispose?: () => void)
async getUsageStats(): Promise<UsageStats>
ping(): void
getResponseTime(): number
}
interface VMMeasureLibraryOptions {
responseTimeLimit?: number // Maximum allowed response time in milliseconds
}
interface UsageStats {
usedMemory: number
totalMemory: number
memoryUsedPercentage: number
responseTime: number
}Usage
const measureLib = new VMMeasureLibrary({
responseTimeLimit: 1000 // Panic stop if response time > 1000ms
})
const vm = new VM([measureLib, new VMConsoleLibrary()])
await vm.initialize()
// Monitor VM performance
setInterval(async () => {
measureLib.ping() // Send ping to measure response time
const stats = await measureLib.getUsageStats()
console.log('VM Stats:', {
memoryUsed: `${(stats.usedMemory / 1024 / 1024).toFixed(2)} MB`,
memoryPercentage: `${stats.memoryUsedPercentage.toFixed(2)}%`,
responseTime: `${stats.responseTime}ms`
})
}, 1000)
const code = `
// VM code can also trigger pings
global.__ping(); // Manually trigger ping from VM
// Simulate work
const data = new Array(1000000).fill(0).map((_, i) => i * i);
return { processed: data.length };
`Advanced Monitoring with VMMeasureMasterLibrary
const masterLib = new VMMeasureMasterLibrary({
responseTimeLimit: 2000
})
// Create multiple monitored VMs
const vm1Lib = masterLib.getNewLibrary('worker-1')
const vm2Lib = masterLib.getNewLibrary('worker-2')
const vm1 = new VM([vm1Lib], { memoryLimit: 32 })
const vm2 = new VM([vm2Lib], { memoryLimit: 64 })
// Monitor all VMs together
const overallStats = await masterLib.getOverallUsageStats()
console.log('Overall VM Performance:', {
totalMemory: overallStats.usedMemory,
individual: overallStats.nested
})
const combinedStats = await masterLib.getCombinedStats()
console.log('Combined Stats:', {
totalMemoryUsed: combinedStats.usedMemory,
averageResponseTime: combinedStats.responseAverageTime,
slowestResponseTime: combinedStats.responseTime,
managedVMs: combinedStats.managedCount
})Creating Custom Libraries
You can create custom libraries in several ways depending on your needs:
1. Extending VMLibrary Base Class
For complete control over library functionality:
import { VMLibrary } from '@david.uhlir/vm-machine'
class CustomMathLibrary extends VMLibrary {
getReferences() {
return {
__advancedMath: (operation: string, ...args: number[]) => {
try {
switch (operation) {
case 'fibonacci':
return this.fibonacci(args[0])
case 'factorial':
return this.factorial(args[0])
default:
throw new Error(`Unknown operation: ${operation}`)
}
} catch (error) {
this.onError?.(error as Error)
return null
}
}
}
}
getVmCode(): string {
return `
global.fibonacci = (n) => __advancedMath('fibonacci', n);
global.factorial = (n) => __advancedMath('factorial', n);
`
}
dispose(): void {
// Cleanup resources
}
private fibonacci(n: number): number {
if (n <= 1) return n
return this.fibonacci(n - 1) + this.fibonacci(n - 2)
}
private factorial(n: number): number {
if (n <= 1) return 1
return n * this.factorial(n - 1)
}
}2. Extending VMIpcRxLibrary for Async Functions
When you need to provide async functions to the VM, extend VMIpcRxLibrary. This is the most common and recommended approach:
import { VMIpcRxLibrary } from '@david.uhlir/vm-machine'
import crypto from 'crypto'
import fs from 'fs/promises'
class CustomCryptoLibrary extends VMIpcRxLibrary {
constructor() {
super({
// All functions must return Promise<any>
randomHash: async (): Promise<string> => {
return crypto.randomBytes(16).toString('hex')
},
sha256: async (data: string | Buffer): Promise<string> => {
return crypto.createHash('sha256').update(data).digest('hex')
},
encrypt: async (text: string, secret: string): Promise<string> => {
const key = crypto.createHash('sha256').update(secret).digest()
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
let encrypted = cipher.update(text, 'utf8', 'base64')
encrypted += cipher.final('base64')
return iv.toString('base64') + ':' + encrypted
},
decrypt: async (encrypted: string, secret: string): Promise<string> => {
const key = crypto.createHash('sha256').update(secret).digest()
const [ivBase64, encryptedData] = encrypted.split(':')
const iv = Buffer.from(ivBase64, 'base64')
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
let decrypted = decipher.update(encryptedData, 'base64', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
})
}
// Override getVmCode to provide clean API
public getVmCode(): string {
const originalCode = super.getVmCode()
return `
${originalCode};
// Create clean crypto API
global.crypto = {
randomHash: global.randomHash,
sha256: global.sha256,
encrypt: global.encrypt,
decrypt: global.decrypt
};
`
}
}
// Usage in VM code:
const code = `
const hash = await crypto.randomHash();
const digest = await crypto.sha256('hello world');
const encrypted = await crypto.encrypt('secret data', 'password');
const decrypted = await crypto.decrypt(encrypted, 'password');
return { hash, digest, encrypted, decrypted };
`3. Advanced Example: File System Library
import { VMIpcRxLibrary } from '@david.uhlir/vm-machine'
import fs from 'fs/promises'
import path from 'path'
class CustomFileSystemLibrary extends VMIpcRxLibrary {
private allowedPaths: string[]
constructor(allowedPaths: string[] = []) {
super({
readFile: async (filePath: string): Promise<string> => {
this.validatePath(filePath)
return await fs.readFile(filePath, 'utf-8')
},
writeFile: async (filePath: string, content: string): Promise<void> => {
this.validatePath(filePath)
await fs.writeFile(filePath, content, 'utf-8')
},
listDirectory: async (dirPath: string): Promise<string[]> => {
this.validatePath(dirPath)
return await fs.readdir(dirPath)
},
getStats: async (filePath: string): Promise<any> => {
this.validatePath(filePath)
const stats = await fs.stat(filePath)
return {
size: stats.size,
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
modifiedTime: stats.mtime,
createdTime: stats.birthtime
}
}
})
this.allowedPaths = allowedPaths.map(p => path.resolve(p))
}
private validatePath(filePath: string): void {
const resolvedPath = path.resolve(filePath)
const isAllowed = this.allowedPaths.some(allowedPath =>
resolvedPath.startsWith(allowedPath)
)
if (!isAllowed) {
throw new Error(`Access denied: ${filePath} is outside allowed paths`)
}
}
public getVmCode(): string {
const originalCode = super.getVmCode()
return `
${originalCode};
global.fs = {
readFile: global.readFile,
writeFile: global.writeFile,
listDirectory: global.listDirectory,
getStats: global.getStats
};
`
}
}
// Usage
const fsLib = new CustomFileSystemLibrary(['/tmp', '/var/data'])
const vm = new VM([fsLib])
const code = `
const files = await fs.listDirectory('/tmp');
const content = await fs.readFile('/tmp/test.txt');
await fs.writeFile('/tmp/output.txt', 'Hello World');
return { files, content };
`4. Database Access Library
import { VMIpcRxLibrary } from '@david.uhlir/vm-machine'
interface DatabaseConfig {
host: string
database: string
// ... other config
}
class CustomDatabaseLibrary extends VMIpcRxLibrary {
private db: any // Your database connection
constructor(config: DatabaseConfig) {
super({
query: async (sql: string, params: any[] = []): Promise<any[]> => {
// Validate SQL for security
this.validateSQL(sql)
return await this.db.query(sql, params)
},
insert: async (table: string, data: Record<string, any>): Promise<number> => {
this.validateTableName(table)
const columns = Object.keys(data).join(', ')
const placeholders = Object.keys(data).map(() => '?').join(', ')
const values = Object.values(data)
const result = await this.db.query(
`INSERT INTO ${table} (${columns}) VALUES (${placeholders})`,
values
)
return result.insertId
},
select: async (table: string, where: Record<string, any> = {}): Promise<any[]> => {
this.validateTableName(table)
const whereClause = Object.keys(where).length > 0
? 'WHERE ' + Object.keys(where).map(key => `${key} = ?`).join(' AND ')
: ''
return await this.db.query(
`SELECT * FROM ${table} ${whereClause}`,
Object.values(where)
)
},
update: async (table: string, data: Record<string, any>, where: Record<string, any>): Promise<number> => {
this.validateTableName(table)
const setClause = Object.keys(data).map(key => `${key} = ?`).join(', ')
const whereClause = Object.keys(where).map(key => `${key} = ?`).join(' AND ')
const result = await this.db.query(
`UPDATE ${table} SET ${setClause} WHERE ${whereClause}`,
[...Object.values(data), ...Object.values(where)]
)
return result.affectedRows
}
})
// Initialize database connection
this.initDatabase(config)
}
private validateSQL(sql: string): void {
// Basic SQL injection protection
const forbidden = ['DROP', 'DELETE', 'TRUNCATE', 'ALTER', 'CREATE']
const upperSQL = sql.toUpperCase()
if (forbidden.some(keyword => upperSQL.includes(keyword))) {
throw new Error('Forbidden SQL operation')
}
}
private validateTableName(table: string): void {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
throw new Error('Invalid table name')
}
}
private async initDatabase(config: DatabaseConfig): Promise<void> {
// Initialize your database connection here
}
public getVmCode(): string {
const originalCode = super.getVmCode()
return `
${originalCode};
global.db = {
query: global.query,
insert: global.insert,
select: global.select,
update: global.update
};
`
}
public dispose(): void {
super.dispose()
if (this.db) {
this.db.close()
}
}
}
// Usage
const dbLib = new CustomDatabaseLibrary({
host: 'localhost',
database: 'myapp'
})
const code = `
// Insert user
const userId = await db.insert('users', {
name: 'John Doe',
email: '[email protected]'
});
// Select users
const users = await db.select('users', { active: 1 });
// Update user
await db.update('users', { last_login: new Date() }, { id: userId });
return { userId, users };
`Key Principles for Custom Libraries
- Extend VMIpcRxLibrary for async functions - this is the most common pattern
- All callable functions must return Promise when extending VMIpcRxLibrary
- Override getVmCode() to provide clean APIs in the VM
- Add security validation for any external access (files, database, network)
- Implement proper disposal to clean up resources
- Handle errors gracefully with try-catch blocks
- Use meaningful function names that will be available in VM global scope
Security Considerations
Memory Limits
Always set appropriate memory limits to prevent resource exhaustion:
const vm = new VM([...libraries], {
memoryLimit: 64, // MB
onCatastrophicError: (error) => {
console.error('Memory or security violation:', error)
// Handle critical errors
}
})Input Validation
Validate all data passed between host and VM:
const ipcRxLib = new VMIpcRxLibrary({
processData: async (data: unknown) => {
// Validate input
if (typeof data !== 'object' || data === null) {
throw new Error('Invalid input: expected object')
}
// Process safely
return { processed: true, data }
}
})Error Boundaries
Implement comprehensive error handling:
const vm = new VM([...libraries], {
onError: (error) => {
console.error('VM Runtime Error:', error.message)
// Log, report, or handle gracefully
},
onCatastrophicError: (error) => {
console.error('Critical VM Error:', error)
// Emergency cleanup, restart, or shutdown
}
})Architecture & Internal Mechanisms
Core VM Architecture
The VM-machine is built on top of Node.js isolated-vm which provides true V8 isolates for secure code execution. Here's how the system works internally:
Isolated V8 Context
// Each VM instance creates its own V8 isolate
const isolate = new ivm.Isolate({
memoryLimit: 64, // MB
onCatastrophicError: (error) => {
// Handle memory violations, infinite loops, etc.
}
})
// Context provides the execution environment
const context = await isolate.createContext()Library System
Libraries extend functionality by injecting host functions and VM code:
- Host References: Functions callable from VM (via
getReferences()) - VM Code: JavaScript code injected into VM context (via
getVmCode()) - Lifecycle Hooks:
attach(),start(),afterStart(),dispose()
Communication Bridge
The system uses several mechanisms for host-VM communication:
Synchronous Communication:
- Direct function calls via
ivm.Reference - Immediate data transfer with copy semantics
Asynchronous Communication:
- Promise-based function calls
- Callback systems for timers and events
Shared Memory:
SharedArrayBufferfor blocking operations (used in module system)Atomicsfor synchronization between async/sync boundaries
Module System Deep Dive
The VMCodeBaseLibrary implements a sophisticated module system that bridges the gap between Node.js-style require() and VM isolation:
Async-to-Sync Bridge
// Problem: require() is synchronous, but we need async module loading
// Solution: SharedArrayBuffer + Atomics for blocking operations
// 1. VM calls __requireSource(name) - starts async operation
global.__requireSource(name);
// 2. VM blocks using Atomics.wait until host loads module
const res = Atomics.wait(__codebaseSABReference, 0, 0);
// 3. Host loads module asynchronously and signals completion
this.synchronizationArray[0] = 1;
Atomics.notify(this.synchronizationArray, 0, 1);
// 4. VM continues and retrieves loaded code
const code = global.__requireSourceGetLastResult();Module Resolution Algorithm
- Custom Resolver: Check if
options.moduleResolvercan handle the module - Added Modules: Check modules added via
addModule() - Filesystem: Look for files in the virtual filesystem (memfs)
- Path Normalization: Handle relative/absolute paths and file extensions
Virtual Filesystem Security
- Memory Limits: Configurable max size and file count
- Read-only Protection: Prevent modification of core files
- Access Control: Whitelist of allowed filesystem operations
Performance Monitoring System
Response Time Measurement
// Host sends ping timestamp
measureLib.ping() // Sets this.lastPingTime = Date.now()
// VM responds with pong
global.__ping() // Calls global.__pong()
// Host calculates round-trip time
this.lastResponseTime = Date.now() - this.lastPingTimeMemory Tracking
// Get real-time V8 heap statistics
const heap = await isolate.getHeapStatistics()
return {
usedMemory: heap.used_heap_size,
totalMemory: heap.heap_size_limit,
memoryUsedPercentage: (heap.used_heap_size / heap.heap_size_limit) * 100
}Multi-VM Monitoring
The VMMeasureMasterLibrary provides centralized monitoring of multiple VM instances with aggregated statistics and resource tracking.
Security Model
Isolation Guarantees
- Memory Isolation: Each VM has its own V8 heap with configurable limits
- CPU Isolation: Catastrophic error handling for infinite loops/recursion
- Filesystem Isolation: Virtual filesystem with no access to host filesystem
- Network Isolation: No direct network access (only through host-provided functions)
Resource Management
- Memory Limits: Hard limits enforced by V8 isolate
- Timer Limits: Configurable maximum number of concurrent timers
- File Limits: Maximum virtual filesystem size and file count
- Response Time Limits: Automatic panic stop for unresponsive code
Error Boundaries
const vm = new VM([...libraries], {
onError: (error) => {
// Recoverable errors (syntax, runtime, etc.)
},
onCatastrophicError: (error) => {
// Critical errors requiring VM termination
// Memory violations, infinite loops, etc.
}
})Advanced Usage
Safe Code Execution with Data Processing
import { VM, VMConsoleLibrary, VMInjectLibrary, VMIpcRxLibrary, VMIpcLibrary } from '@david.uhlir/vm-machine'
class SafeCodeRunner {
async executeUserCode(userCode: string, inputData: any): Promise<any> {
// Host functions available to VM
const ipcRxLib = new VMIpcRxLibrary({
// Provide safe data access
getData: async (key: string) => {
// Validate and return data safely
if (key === 'input') return inputData
throw new Error(`Access denied to: ${key}`)
},
// Provide safe logging
logResult: async (message: string) => {
console.log('VM Result:', message)
return { logged: true }
}
})
// VM function caller
const ipcTxLib = new VMIpcLibrary({ timeout: 5000 })
// Inject safe constants
const injectLib = new VMInjectLibrary({
MAX_ITEMS: 1000,
API_VERSION: '1.0.0'
})
const vm = new VM([
new VMConsoleLibrary(),
ipcRxLib,
ipcTxLib,
injectLib
], {
memoryLimit: 32, // Limit memory
onError: (error) => console.error('VM Error:', error)
})
try {
await vm.initialize()
await vm.run(userCode)
// Call the main function if it exists
const result = await ipcTxLib.callIpc('main', [])
return { success: true, result }
} catch (error) {
return { success: false, error: error.message }
} finally {
vm.dispose()
}
}
}
// Usage example
const runner = new SafeCodeRunner()
const userCode = `
async function main() {
console.log('Processing data with version:', API_VERSION);
// Get input data from host
const data = await getData('input');
// Process the data safely
const processed = data.map(x => x * 2).slice(0, MAX_ITEMS);
// Log result
await logResult(\`Processed \${processed.length} items\`);
return {
processed,
count: processed.length,
version: API_VERSION
};
}
return { main };
`
const result = await runner.executeUserCode(userCode, [1, 2, 3, 4, 5])
console.log(result)
// Output: { success: true, result: { processed: [2, 4, 6, 8, 10], count: 5, version: '1.0.0' } }Mathematical Expression Evaluator
class MathEvaluator {
async evaluateExpression(expression: string, variables: Record<string, number> = {}): Promise<number> {
// Provide math functions to VM
const ipcRxLib = new VMIpcRxLibrary({
// Safe math operations
pow: async (base: number, exponent: number) => Math.pow(base, exponent),
sqrt: async (x: number) => Math.sqrt(x),
sin: async (x: number) => Math.sin(x),
cos: async (x: number) => Math.cos(x),
log: async (x: number) => Math.log(x)
})
const ipcTxLib = new VMIpcLibrary()
// Inject variables
const injectLib = new VMInjectLibrary(variables)
const vm = new VM([ipcRxLib, ipcTxLib, injectLib], {
memoryLimit: 8, // Very small for math operations
onError: (error) => { throw new Error(`Math evaluation error: ${error.message}`) }
})
try {
await vm.initialize()
const code = `
async function evaluate() {
// User's expression is evaluated here safely
return ${expression};
}
return { evaluate };
`
await vm.run(code)
return await ipcTxLib.callIpc('evaluate', [])
} finally {
vm.dispose()
}
}
}
// Usage
const evaluator = new MathEvaluator()
// Simple expression
let result = await evaluator.evaluateExpression('2 + 3 * 4') // 14
// With variables
result = await evaluator.evaluateExpression('x * y + z', { x: 5, y: 10, z: 2 }) // 52
// With math functions
result = await evaluator.evaluateExpression('await pow(2, 3) + await sqrt(16)') // 12Template Processing with Data
class TemplateProcessor {
async processTemplate(template: string, data: any): Promise<string> {
const ipcRxLib = new VMIpcRxLibrary({
// Provide data access
getValue: async (path: string) => {
// Safe property access using path like 'user.name'
return path.split('.').reduce((obj, key) => obj?.[key], data)
},
// Provide utility functions
formatDate: async (date: string) => new Date(date).toLocaleDateString(),
uppercase: async (str: string) => str.toUpperCase(),
lowercase: async (str: string) => str.toLowerCase()
})
const ipcTxLib = new VMIpcLibrary()
const timingLib = new VMTimingLibrary()
const vm = new VM([ipcRxLib, ipcTxLib, timingLib], {
memoryLimit: 16,
onError: (error) => { throw error }
})
try {
await vm.initialize()
const code = `
async function processTemplate() {
// Template processing logic
let result = \`${template}\`;
// Replace {{variable}} patterns
const regex = /\{\{([^}]+)\}\}/g;
const matches = [];
let match;
while ((match = regex.exec(\`${template}\`)) !== null) {
matches.push({
full: match[0],
path: match[1].trim()
});
}
for (const m of matches) {
let value;
// Check for function calls like {{formatDate(user.birthdate)}}
if (m.path.includes('(')) {
const funcMatch = m.path.match(/(\w+)\(([^)]+)\)/);
if (funcMatch) {
const funcName = funcMatch[1];
const argPath = funcMatch[2];
const argValue = await getValue(argPath);
if (funcName === 'formatDate') value = await formatDate(argValue);
else if (funcName === 'uppercase') value = await uppercase(argValue);
else if (funcName === 'lowercase') value = await lowercase(argValue);
else value = argValue;
}
} else {
value = await getValue(m.path);
}
result = result.replace(m.full, value || '');
}
return result;
}
return { processTemplate };
`
await vm.run(code)
return await ipcTxLib.callIpc('processTemplate', [])
} finally {
vm.dispose()
}
}
}
// Usage
const processor = new TemplateProcessor()
const template = `
Hello {{user.name}}!
Your registration date: {{formatDate(user.registeredAt)}}
Email: {{lowercase(user.email)}}
Status: {{uppercase(user.status)}}
`
const data = {
user: {
name: 'John Doe',
email: '[email protected]',
status: 'active',
registeredAt: '2024-01-15'
}
}
const result = await processor.processTemplate(template, data)
console.log(result)
// Output:
// Hello John Doe!
// Your registration date: 1/15/2024
// Email: [email protected]
// Status: ACTIVEPerformance Tips
- Reuse VM instances when possible instead of creating new ones
- Set appropriate memory limits based on your use case
- Implement timeouts for long-running operations
- Use connection pooling for database operations in IPC functions
- Monitor memory usage and dispose VMs regularly
- Batch operations when processing multiple items
Testing
The library includes comprehensive tests. Run them with:
npm testExample test structure:
import { expect } from 'chai'
import { VM, VMConsoleLibrary, VMIpcRxLibrary, VMIpcLibrary } from '@david.uhlir/vm-machine'
describe('Custom Tests', () => {
let vm: VM
afterEach(() => {
if (vm) {
vm.dispose()
vm = undefined
}
})
it('should execute code safely', async () => {
const ipcRxLib = new VMIpcRxLibrary({
testFunction: async () => 'success'
})
const ipcTxLib = new VMIpcLibrary()
vm = new VM([ipcRxLib, ipcTxLib])
await vm.initialize()
const code = `
async function main() {
return await testFunction();
}
return { main };
`
await vm.run(code)
const result = await ipcTxLib.callIpc('main', [])
expect(result).to.equal('success')
})
})Error Handling
The library provides multiple layers of error handling:
VM-level Errors
const vm = new VM([...libraries], {
onError: (error: Error) => {
// Runtime errors (syntax, reference errors, etc.)
console.error('Runtime error:', error.message)
},
onCatastrophicError: (error: string) => {
// Memory violations, security issues
console.error('Critical error:', error)
process.exit(1) // May need to restart
}
})IPC Errors
const ipcRxLib = new VMIpcRxLibrary({
riskyOperation: async (data: any) => {
if (!data) {
throw new Error('Data is required')
}
// This error will be propagated to VM
return processData(data)
}
})
const ipcTxLib = new VMIpcLibrary({
timeout: 5000 // Prevent hanging calls
})
// In VM code:
const code = `
async function main() {
try {
const result = await riskyOperation(null);
return { success: true, result };
} catch (error) {
return { success: false, error: error.message };
}
}
return { main };
`Library Disposal Errors
class SafeCustomLibrary extends VMLibrary {
dispose(): void {
try {
// Cleanup resources
this.connections?.forEach(conn => conn.close())
this.timers?.forEach(timer => clearTimeout(timer))
} catch (error) {
console.error('Disposal error:', error)
// Don't throw - allow other libraries to dispose
}
}
}Creating Custom Libraries
You can create custom libraries in several ways depending on your needs:
1. Extending VMLibrary Base Class
For complete control over library functionality:
import { VMLibrary } from '@david.uhlir/vm-machine'
class CustomMathLibrary extends VMLibrary {
getReferences() {
return {
__advancedMath: (operation: string, ...args: number[]) => {
try {
switch (operation) {
case 'fibonacci':
return this.fibonacci(args[0])
case 'factorial':
return this.factorial(args[0])
default:
throw new Error(`Unknown operation: ${operation}`)
}
} catch (error) {
throw new Error(`Math operation failed: ${error.message}`)
}
}
}
}
getVmCode() {
return `
global.Math.fibonacci = (n) => global.__advancedMath('fibonacci', n);
global.Math.factorial = (n) => global.__advancedMath('factorial', n);
`
}
dispose(): void {
// Always call super.dispose() when extending VMLibrary
super.dispose()
// Add your cleanup logic here
console.log('CustomMathLibrary disposed')
}
private fibonacci(n: number): number {
if (n <= 1) return n
return this.fibonacci(n - 1) + this.fibonacci(n - 2)
}
private factorial(n: number): number {
if (n <= 1) return 1
return n * this.factorial(n - 1)
}
}2. Extending VMIpcRxLibrary for Async Operations
This is the recommended pattern for libraries that provide async functions to the VM, as shown in vm.crypto.library.ts:
import { VMIpcRxLibrary } from '@david.uhlir/vm-machine'
import fs from 'fs/promises'
import path from 'path'
class VMFileSystemLibrary extends VMIpcRxLibrary {
private allowedPaths: string[]
constructor(allowedPaths: string[] = []) {
super({
__readFile: async (filePath: string): Promise<string> => {
this.validateP