Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI
on:
push:
branches:
- main
pull_request:

jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
env:
WASM_TOOLS_VERSION: 1.245.1
WIT_BINDGEN_VERSION: 0.53.1
WAC_VERSION: 0.9.0
WASMTIME_VERSION: 41.0.3
VICEROY_VERSION: 0.16.4
steps:
- uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: wasm32-unknown-unknown, wasm32-wasip2
components: rustfmt, clippy
- name: Install wasm-tools, wit-bindgen and wac
run: |
tmp=$(mktemp -d)
curl -sLo "$tmp/wasm-tools.tar.gz" https://github.com/bytecodealliance/wasm-tools/releases/download/v${{ env.WASM_TOOLS_VERSION }}/wasm-tools-${{ env.WASM_TOOLS_VERSION }}-x86_64-linux.tar.gz
tar -xz -f "$tmp/wasm-tools.tar.gz" -C "$tmp"
mv "$tmp/wasm-tools-${{ env.WASM_TOOLS_VERSION }}-x86_64-linux/wasm-tools" /usr/local/bin/
curl -sLo "$tmp/wit-bindgen.tar.gz" https://github.com/bytecodealliance/wit-bindgen/releases/download/v${{ env.WIT_BINDGEN_VERSION }}/wit-bindgen-${{ env.WIT_BINDGEN_VERSION }}-x86_64-linux.tar.gz
tar -xz -f "$tmp/wit-bindgen.tar.gz" -C "$tmp"
mv "$tmp/wit-bindgen-${{ env.WIT_BINDGEN_VERSION }}-x86_64-linux/wit-bindgen" /usr/local/bin/
curl -sLo "$tmp/wac-cli" https://github.com/bytecodealliance/wac/releases/download/v${{ env.WAC_VERSION }}/wac-cli-x86_64-unknown-linux-musl
chmod a+x "$tmp/wac-cli"
mv "$tmp/wac-cli" /usr/local/bin/wac
- name: Install viceroy and wasmtime
run: |
tmp=$(mktemp -d)
curl -sLo "$tmp/viceroy.tar.gz" https://github.com/fastly/Viceroy/releases/download/v${{ env.VICEROY_VERSION }}/viceroy_v${{ env.VICEROY_VERSION }}_linux-amd64.tar.gz
tar -xz -f "$tmp/viceroy.tar.gz" -C "$tmp"
chmod a+x "$tmp/viceroy"
mv "$tmp/viceroy" /usr/local/bin/
curl -sLo "$tmp/wasmtime.tar.xz" https://github.com/bytecodealliance/wasmtime/releases/download/v${{ env.WASMTIME_VERSION }}/wasmtime-v${{ env.WASMTIME_VERSION }}-x86_64-linux.tar.xz
tar -xJ -f "$tmp/wasmtime.tar.xz" -C "$tmp"
mv "$tmp/wasmtime-v${{ env.WASMTIME_VERSION }}-x86_64-linux/wasmtime" /usr/local/bin/
file /usr/local/bin/wasmtime
wasmtime --version
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Build
run: make
- name: Test
run: make test
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
/target
trace.out
composed.wasm
viceroy.pid
# cannot viceroy yet
/tests/js.wasm
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ wasmtime-wasi = { version = "41.0.0", optional = true }

[features]
run = ["wasmtime", "wasmtime-wasi"]
default = ["run"]
#default = ["run"]
40 changes: 40 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.PHONY: all build-components build-cli test test-record test-fuzz run-fuzz run-record run-viceroy

all: build-components build-cli
build-cli:
cargo build --release
build-components:
cargo build -p debug --target wasm32-wasip2 --release
cargo build -p recorder --target wasm32-wasip2 --release
cp target/wasm32-wasip2/release/debug.wasm assets/debug.wasm
cp target/wasm32-wasip2/release/recorder.wasm assets/recorder.wasm

test: test-fuzz test-record

test-fuzz:
RUSTFLAGS="" $(MAKE) run-fuzz WASM=tests/calculator.wasm

test-record:
$(MAKE) run-record WASM=tests/rust.wasm
$(MAKE) run-record WASM=tests/go.wasm
$(MAKE) run-record WASM=tests/python.wasm

run-fuzz:
target/release/proxy-component instrument -m fuzz $(WASM)
wasmtime --invoke 'start()' composed.wasm

run-record:
target/release/proxy-component instrument -m record $(WASM)
$(MAKE) run-viceroy URL=localhost:7676
target/release/proxy-component instrument -m replay $(WASM)
wasmtime --invoke 'start()' composed.wasm < trace.out

run-viceroy:
viceroy composed.wasm > trace.out & echo $$! > viceroy.pid
until nc -z localhost 7676; do \
kill -0 $$(cat viceroy.pid) 2>/dev/null || exit 1; \
sleep 1; \
done
curl $(URL)
kill $$(cat viceroy.pid) || true
rm viceroy.pid
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,36 @@ we synthesize a new component that virtualizes the host interface and can option
This synthesized component can be linked, via `wac`, to produce a resulting component with the exact same WIT interface as the original,
but with the added side effects from the virtualized component. This allows us to instrument or virtualize the Wasm component without modifying the user code.

Currently, the tool focuses on using this technique to perform record and replay for Wasm components.
In the future, we can apply the same technique to other use cases, such as service chaining.
Currently, the tool focuses on using this technique to perform fuzzing, and record & replay for Wasm components.
In the future, we can apply the same technique to other use cases, such as generating adapters.

## Build

To build the CLI tool, just run `make all`.

To test record and fuzzing, run `make test`.

## Usage

### Record

```
$ cargo run instrument -m record <component.wasm>
$ proxy-component instrument -m record <component.wasm>
```
Run `composed.wasm` in the host runtime which the original wasm is supposed to run. The runtime also needs to implement
the [`record` interface](https://github.com/chenyan2002/proxy-component/blob/main/assets/recorder.wit#L3). See this [example PR](https://github.com/fastly/Viceroy/pull/546).
Run `composed.wasm` in the host runtime which the original wasm is supposed to run. The tool provides a guest implementation for record and replay APIs, which outputs the trace to stdout while recording, and reads the trace from stdin while replay.

In the future, we can make the `record` interface as a component, so that we don't need to make any changes to the host runtime.
The host runtime can also choose to implement the [`record` interface](https://github.com/chenyan2002/proxy-component/blob/main/assets/recorder.wit#L3). Then we can use the `--use-host-recorder` flag to skip composing the guest-side record implementation.

### Replay
### Replay

Assuming the trace captured from the record phase is stored in `trace.out`. We can run the following to replay the trace.

```
$ cargo run instrument -m replay <component.wasm>
$ cargo run run composed.wasm --invoke 'start()' --trace trace.out
$ proxy-component instrument -m replay <component.wasm>
$ wasmtime --invoke 'start()' composed.wasm < trace.out
```

Note that the trace is self-contained, and `composed.wasm` doesn't have any imports. This means that we can run `composed.wasm`
in a regular `wasmtime` without the host interface.
Note that the trace is self-contained, and `composed.wasm` doesn't have any imports. This means that we can run `composed.wasm` in a regular `wasmtime` without the host interface.

Another interesting use case is that we can replay the trace with a different Wasm binary, likely with a different compiler flag, or
a different optimization strategy, to compare the performance. We have assertions in the replay phase to make sure that the trace
Expand Down Expand Up @@ -67,3 +71,4 @@ $ cargo run generate bindings.rs <mode> -o lib.rs
* wasm-tools
* wit-bindgen
* wac
* viceroy (only needed to run the record test suite)
Binary file added assets/debug.wasm
Binary file not shown.
Binary file added assets/recorder.wasm
Binary file not shown.
17 changes: 11 additions & 6 deletions components/recorder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,10 @@ fn load_trace() {
let reader = std::io::BufReader::new(f);
for line in reader.lines() {
let line = line.unwrap();
if line.trim().is_empty() {
break;
match serde_json::from_str::<FuncCall>(&line) {
Ok(item) => res.push_back(item),
Err(_) => continue,
}
let item: FuncCall = serde_json::from_str(&line).unwrap();
res.push_back(item);
}
*v = Some(res);
});
Expand Down Expand Up @@ -104,8 +103,14 @@ impl bindings::exports::proxy::recorder::replay::Guest for Component {
.as_ref()
.is_some_and(|m| m.starts_with("wasi:cli/exit"))
{
//self.exit_called = true;
return Some("Something that can crash".to_string());
let code = if assert_args
.is_some_and(|args| args.get(0).is_some_and(|arg| arg.starts_with("err")))
{
1
} else {
0
};
std::process::exit(code);
}
call = v.as_mut().unwrap().pop_front().unwrap();
}
Expand Down
40 changes: 20 additions & 20 deletions src/instrument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ pub struct InstrumentArgs {
pub use_host_recorder: bool,
}

const DEBUG_WASM: &[u8] = include_bytes!("../assets/debug.wasm");
const RECORDER_WASM: &[u8] = include_bytes!("../assets/recorder.wasm");

pub fn run(args: InstrumentArgs) -> Result<()> {
// 1. Create a tmp directory and initialize a new Rust project in it.
let tmp_dir = init_rust_project()?;
Expand Down Expand Up @@ -77,10 +80,8 @@ pub fn run(args: InstrumentArgs) -> Result<()> {
let imports = format!("import:proxy={}", imports_wasm_path.display());
let exports = format!("export:proxy={}", exports_wasm_path.display());
let root = format!("root:component={}", args.wasm_file.display());
let debug = format!(
"import:debug={}/target/wasm32-wasip2/debug/debug.wasm",
env!("CARGO_MANIFEST_DIR")
);
fs::write(tmp_dir.join("debug.wasm"), DEBUG_WASM)?;
let debug = format!("import:debug={}/debug.wasm", tmp_dir.display());
let wac_path = tmp_dir.join("wit/compose.wac");
let mut cmd = Command::new("wac");
cmd.arg("compose")
Expand All @@ -96,10 +97,9 @@ pub fn run(args: InstrumentArgs) -> Result<()> {
.arg("-o")
.arg(output_file);
if !args.use_host_recorder {
let recorder = format!(
"import:recorder={}/target/wasm32-wasip2/debug/recorder.wasm",
env!("CARGO_MANIFEST_DIR")
);
let wasm_path = tmp_dir.join("recorder.wasm");
fs::write(&wasm_path, RECORDER_WASM)?;
let recorder = format!("import:recorder={}", wasm_path.display());
cmd.arg("--dep").arg(&recorder);
}
let status = cmd.status()?;
Expand Down Expand Up @@ -127,18 +127,18 @@ fn bindgen(
dest_name: &str,
) -> Result<()> {
let out_dir = tmp_dir.join(dest_name);
//let status = Command::new("wit-bindgen")
let status =
Command::new("/Users/chenyan/src/bytecodealliance/wit-bindgen/target/debug/wit-bindgen")
.arg("rust")
.arg(wit_dir)
.arg("--world")
.arg(world_name)
.arg("--generate-all")
.arg("--merge-structurally-equal-types=true")
.arg("--out-dir")
.arg(&out_dir)
.status()?;
let status = Command::new("wit-bindgen")
//let status =
// Command::new("/Users/chenyan/src/bytecodealliance/wit-bindgen/target/debug/wit-bindgen")
.arg("rust")
.arg(wit_dir)
.arg("--world")
.arg(world_name)
.arg("--generate-all")
//.arg("--merge-structurally-equal-types=true")
.arg("--out-dir")
.arg(&out_dir)
.status()?;
assert!(status.success());
let binding_file = out_dir.join(world_name.to_owned() + ".rs");
let codegen_mode = match mode {
Expand Down
Binary file added tests/calculator.wasm
Binary file not shown.
Binary file added tests/go.wasm
Binary file not shown.
Binary file added tests/python.wasm
Binary file not shown.
Binary file added tests/rust.wasm
Binary file not shown.
Loading