Run seleniumbase-rs plugins that are written in any language that compiles to WebAssembly (Rust, C, Zig, Go, AssemblyScript, or plain WAT), sandboxed by the pure-Rust interpreter wasmi.
A module describes a plugin in data: a name, browser switches, scripts for pages, URL patterns to block. The host reads that data once, when it loads the module, checks every value, and then drops the module. Nothing of the module runs while the browser does. The module has no imports: no files, network, clock or randomness, and it cannot call back into the host. Only pure functions cross the boundary.
Plugin framework and community plugins for seleniumbase-rs
Install it from the same revision of the plugins repository that your seleniumbase-rs pins; see Installing a plugin. The plugins overview is here.
Use it#
WasmPlugin::from_bytes takes the module from memory instead, and
from_file_with / from_bytes_with take a Limits to change the defaults
below. Loading fails with a WasmError that says what is wrong with the module;
it never panics and never hangs. A WasmError converts into a PluginError, so
? works in code that returns the plugin API’s Result.
Loading is synchronous and does the interpreter’s work on the calling thread. A
well-behaved module takes microseconds to milliseconds; a hostile one can spend
its whole fuel budget on each of its calls and then fails. In async code, load
modules at start-up or inside spawn_blocking.
The loaded WasmPlugin is an ordinary Plugin:
| Hook | What it does with the module’s data |
|---|---|
name | The name from sbrs_name. |
packages | One Package::script per entry of sbrs_scripts, loaded only_on its hosts or everywhere. |
before_launch | Appends the arguments of sbrs_launch_args to LaunchSettings::args. |
on_page_created | When there are blocked URLs: Network.enable, then Network.setBlockedURLs with the patterns. |
requirements | CHROMIUM_ONLY when the module blocks URLs (it uses the DevTools Protocol); none otherwise. |
The ABI, version 1#
A module exports a linear memory called memory and these functions. None takes
a parameter.
| Export | Required | Returns |
|---|---|---|
memory | yes | The module’s linear memory (32-bit; at most one). |
sbrs_abi_version() -> i32 | yes | 1. Any other value is refused. |
sbrs_name() -> i64 | yes | A JSON string: the plugin’s name. |
sbrs_launch_args() -> i64 | no | A JSON array of strings: browser switches. |
sbrs_scripts() -> i64 | no | A JSON array of script objects. |
sbrs_blocked_urls() -> i64 | no | A JSON array of strings: URL patterns to block. |
An optional export that is missing means “none”. An export that is present with
the wrong type is an error. The sbrs_ prefix is reserved for the ABI: an
export that starts with it and is not in the table (a typo such as
sbrs_script) is an error, so a mistake is never silently ignored. Exports with other names are ignored, so a module can export
what it likes.
Returning a string: the packed i64#
A function that returns text returns one i64 that packs a pointer and a
length, and leaves the UTF-8 JSON text in its memory at that place:
The host reads memory[pointer .. pointer + length] right after the call
returns, so the bytes must stay valid until then: a static string, or a
buffer the module leaks. A range outside the memory, or longer than the limit,
is an error. The text must be valid UTF-8 and exactly one JSON value, with
nothing after it.
The values#
A JSON string of 1 to 64 characters from a-z, 0-9, - and _, starting
with a letter or a digit, such as "my-plugin". It must be unique in the
PluginSet, like any plugin name.
At most 256 strings of at most 4096 bytes. Each is a switch: it starts with -,
followed by a name that starts with a letter or a digit ("--lang=de"), and
holds no control characters. A bare --, or anything that is not a switch, is
refused because the browser would take it for a URL.
At most 64 objects with exactly these two fields and no others:
At most 1024 strings of 1 to 2048 bytes, Chrome URL patterns where * matches
anything ("*.png", "*://ads.example.com/*").
Limits#
The host applies these to every module. Limits changes the first four.
| Limit | Default |
|---|---|
| Module size | 4 MiB |
Memory (at start and after any memory.grow) | 16 MiB |
| Fuel for each call, and for the start function | 20,000,000 (roughly as many instructions) |
| Size of one returned JSON value | 1 MiB |
| Call stack | wasmi’s defaults; a recursion bomb traps |
| Imports | none: a module that imports anything is refused |
| Memories, tables | 1 memory; tables of at most 100,000 elements; wasmi’s strict limits on functions, globals and parameters |
| Memory page size, memory64 | the standard 64 KiB page and 32-bit addresses only |
A function that runs out of fuel stops with WasmError::OutOfFuel; a trap
(unreachable, stack overflow, out-of-bounds access) is WasmError::Trapped.
memory.grow past the cap returns -1, as the WebAssembly specification says.
The text format is not accepted: give from_bytes a binary module (use wat or
wasm-tools to build one from WAT).
What a module can still do#
The sandbox keeps a module from touching your machine while it is loaded. What it describes then reaches three places, so review a module you did not write.
- Scripts run inside pages, with the page’s rights. A script for
"everywhere"sees every page, cookie and password in the browser, and can send them anywhere the page can. Preferhostslists. - Browser switches go to the browser. The host refuses the ones known to
run programs or switch off the browser’s protections, by name, wherever they
appear in the list:
renderer-cmd-prefix,utility-cmd-prefix,zygote-cmd-prefix,gpu-launcher,ppapi-plugin-launcher,browser-subprocess-path,nacl-gdb,no-sandbox,disable-setuid-sandbox,disable-gpu-sandbox,no-zygote,disable-web-security,ignore-certificate-errors,proxy-server,proxy-pac-url,remote-debugging-port,remote-debugging-address,remote-debugging-pipe,remote-allow-origins,load-extension,disable-extensions-exceptanduser-data-dir(FORBIDDEN_FLAGSin the code). That list is a floor, not a complete audit of Chrome’s switches: readWasmPlugin::launch_args()if you must be sure. - Blocked URLs change what a page can load.
Write a module#
A module can be written in WAT or in any language that compiles to WebAssembly. Two examples:
examples/hello.wat is a whole plugin: it is named
hello and logs a line on example.com. The two strings sit in the data
segments; each function returns the pointer in the high half and the length in
the low half.
Build the binary with wasm-tools parse examples/hello.wat -o hello.wasm, or
in Rust with wat::parse_str.
Compile a plugin to wasm32-unknown-unknown as a cdylib, with no dependencies
and no unsafe:
To build the JSON at run time (with serde_json, say), leak the String so it
stays valid after the function returns: pack(Box::leak(json.into_boxed_str())).
The host calls each function once and then discards the module, so the leak is
a single allocation.
Test a module#
Load it, then run the plugin against the recording fakes of
seleniumbase-rs-plugin-testing: no browser, no module-specific harness.
Versioning#
The ABI version is the number sbrs_abi_version returns. A change that an
existing module could not survive (a new required export, another meaning for a
field) bumps it. A host refuses a module of a version it does not speak, with
WasmError::UnsupportedAbi, before it looks at anything else. For that reason
sbrs_abi_version() -> i32 keeps its signature in every version.