Stability: 1 - Experimental
The node:wasi module does not currently provide the comprehensive file system security properties provided by some WASI runtimes. Do not rely on it to run untrusted code.
Overview
The WASI API provides an implementation of the WebAssembly System Interface specification. WASI gives WebAssembly applications access to the underlying operating system via a collection of POSIX-like functions.
import { readFile } from 'node:fs/promises';
import { WASI } from 'node:wasi';
import { argv, env } from 'node:process';
const wasi = new WASI({
version: 'preview1',
args: argv,
env,
preopens: {
'/local': '/some/real/path/that/wasm/can/access',
},
});
const wasm = await WebAssembly.compile(
await readFile(new URL('./demo.wasm', import.meta.url)),
);
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.start(instance);
Security
WASI provides a capabilities-based model through which applications are provided their own custom environment capabilities:
env - Environment variables
preopens - Directory access
stdin, stdout, stderr - Standard I/O
exit - Process termination
The current Node.js threat model does not provide secure sandboxing as present in some WASI runtimes. File system sandboxing can be escaped with various techniques. The project is exploring whether security guarantees could be added in future.
Class: WASI
The WASI class provides the WASI system call API and additional convenience methods for working with WASI-based applications.
Each WASI instance represents a distinct environment.
new WASI([options])
options
args Command-line arguments visible to the WebAssembly application. Default: []
env Environment object similar to process.env. Default: {}
preopens Directory structure mapping. Keys are virtual paths, values are real host paths
returnOnExit Return with exit code rather than terminating process. Default: true
stdin File descriptor for standard input. Default: 0
stdout File descriptor for standard output. Default: 1
stderr File descriptor for standard error. Default: 2
version WASI version ('unstable' or 'preview1'). Required
Creates a new WASI instance.
const wasi = new WASI({
version: 'preview1',
args: ['--verbose'],
env: {
NODE_ENV: 'production',
},
preopens: {
'/app': '/real/path/to/app',
'/tmp': '/real/path/to/tmp',
},
returnOnExit: true,
});
wasi.getImportObject()
Returns an import object that can be passed to WebAssembly.instantiate() if no other WASM imports are needed beyond those provided by WASI.
For version 'preview1':
{ wasi_snapshot_preview1: wasi.wasiImport }
For version 'unstable':
{ wasi_unstable: wasi.wasiImport }
wasi.start(instance)
Attempts to begin execution of instance as a WASI command by invoking its _start() export.
Requirements:
instance must have a _start() export
instance must not have an _initialize() export
instance must export a WebAssembly.Memory named memory
Note: Can only be called once per instance.
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.start(instance);
wasi.initialize(instance)
Attempts to initialize instance as a WASI reactor by invoking its _initialize() export, if present.
Requirements:
instance must not have a _start() export
instance must export a WebAssembly.Memory named memory
Note: Can only be called once per instance.
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.initialize(instance);
// Now you can call other exported functions
wasi.finalizeBindings(instance[, options])
instance
options
memory Default: instance.exports.memory
Sets up WASI host bindings without calling initialize() or start().
Useful when:
- Instantiating WASI modules in child threads
- Sharing memory across threads
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.finalizeBindings(instance);
wasi.wasiImport
Object implementing the WASI system call API. This object should be passed as the wasi_snapshot_preview1 import during instantiation.
const importObject = {
wasi_snapshot_preview1: wasi.wasiImport,
// other imports...
};
Creating WASM Modules
Example: Hello World
Create a WebAssembly text format file demo.wat:
(module
;; Import fd_write WASI function
(import "wasi_snapshot_preview1" "fd_write"
(func $fd_write (param i32 i32 i32 i32) (result i32)))
(memory 1)
(export "memory" (memory 0))
;; Write 'hello world\n' to memory at offset 8
(data (i32.const 8) "hello world\n")
(func $main (export "_start")
;; Create io vector in linear memory
(i32.store (i32.const 0) (i32.const 8)) ;; iov_base pointer
(i32.store (i32.const 4) (i32.const 12)) ;; iov_len
(call $fd_write
(i32.const 1) ;; stdout file descriptor
(i32.const 0) ;; iovs pointer
(i32.const 1) ;; iovs_len
(i32.const 20) ;; nwritten
)
drop
)
)
Compile to .wasm using wabt:
Use Cases
Running WASI Commands
Execute WebAssembly programs as commands:
import { WASI } from 'node:wasi';
import { readFile } from 'node:fs/promises';
const wasi = new WASI({
version: 'preview1',
args: process.argv.slice(2),
env: process.env,
});
const wasm = await WebAssembly.compile(
await readFile('./program.wasm')
);
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.start(instance);
File System Access
Provide controlled file system access:
const wasi = new WASI({
version: 'preview1',
preopens: {
'/sandbox': '/real/path/to/sandbox',
'/readonly': '/real/path/to/data',
},
});
The WebAssembly module can access:
/sandbox directory (mapped to real path)
/readonly directory (mapped to real path)
- Cannot access paths outside these directories
Environment Variables
Pass environment configuration:
const wasi = new WASI({
version: 'preview1',
env: {
DATABASE_URL: 'postgres://localhost/mydb',
LOG_LEVEL: 'debug',
API_KEY: process.env.API_KEY,
},
});
Custom I/O
Redirect standard I/O streams:
import fs from 'node:fs';
const logFile = fs.openSync('./app.log', 'a');
const inputFile = fs.openSync('./input.txt', 'r');
const wasi = new WASI({
version: 'preview1',
stdin: inputFile,
stdout: logFile,
stderr: logFile,
});
React to Exit Codes
Handle WebAssembly program exit:
const wasi = new WASI({
version: 'preview1',
returnOnExit: true, // Don't terminate Node.js process
});
try {
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.start(instance);
console.log('Program exited successfully');
} catch (err) {
if (err.code === 'WASI_EXIT') {
console.log('Program exited with code:', err.exitCode);
} else {
throw err;
}
}
WASI Versions
preview1
Stable WASI specification version:
const wasi = new WASI({ version: 'preview1' });
Import object key: wasi_snapshot_preview1
unstable
Experimental WASI features:
const wasi = new WASI({ version: 'unstable' });
Import object key: wasi_unstable
Limitations
- Security: File system sandboxing can be escaped
- System Calls: Limited subset of POSIX-like functions
- Performance: May be slower than native code
- Threading: Limited support for multi-threading
- Networking: Socket support varies by WASI version
Best Practices
- Always specify version: The
version option is required
- Minimal preopens: Only expose necessary directories
- Validate inputs: Don’t trust WASM module behavior
- Handle exits: Use
returnOnExit: true to handle exit codes
- Error handling: Wrap WASI operations in try-catch blocks
- Resource cleanup: Close file descriptors when done
Debugging
Enable WASI debugging:
const wasi = new WASI({
version: 'preview1',
args: ['--verbose'],
env: { DEBUG: 'wasi:*' },
});
Inspect WASI imports:
console.log(Object.keys(wasi.wasiImport));
// Prints available WASI system calls
Further Reading