Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
15ad76f
Support async functions, streams, futures and error-contexts in fuzz …
chenyan2002 Nov 1, 2026
3ff325c
executor for import component only
chenyan2002 Nov 1, 2026
d560e2f
p3 for dialog
chenyan2002 Nov 1, 2026
aff66d4
drain executor after each export call for dialog mode
chenyan2002 Nov 1, 2026
c7377c0
record both writer and reader handles
chenyan2002 Nov 1, 2026
a3e1995
explain that the recorded handle id doesn't exactly match the main co…
chenyan2002 Nov 2, 2026
981a22d
add design doc
chenyan2002 Nov 3, 2026
4a51c51
fix
chenyan2002 Nov 3, 2026
e6716fd
correctness assumption
chenyan2002 Nov 3, 2026
9f9e476
cannot use epoch interruption
chenyan2002 Nov 3, 2026
987e827
new trace format
chenyan2002 Nov 4, 2026
d3874d0
make fuzz to use the new trace
chenyan2002 Nov 4, 2026
bff7ab2
Replay works except label and ToRust
chenyan2002 Nov 5, 2026
56e56b4
local ToRust trait
chenyan2002 Nov 5, 2026
6a011b0
replay checks for label upto renaming
chenyan2002 Nov 5, 2026
e94c156
Replay streams and futures, and record unique labels via alpha conver…
chenyan2002 Nov 6, 2026
dab9318
Fuzz with varied results, and hold the executor until a wait resumes
chenyan2002 Nov 6, 2026
47201a2
add race fixture test
chenyan2002 Nov 6, 2026
75f351f
report error trace on trap
chenyan2002 Nov 6, 2026
f88ad7d
Record async imports of p3 components
chenyan2002 Nov 6, 2026
b409ccf
Record streams and futures of p3 components, and consolidate fixtures
chenyan2002 Nov 7, 2026
5439c1c
Traces cut short by a trap now replay to the same trap.
chenyan2002 Nov 7, 2026
8b93278
Add wasi-http-p3 into test and fix things
chenyan2002 Nov 7, 2026
85d0540
Record cancel events; reveal the multiple events in the same waitable…
chenyan2002 Nov 7, 2026
fff961c
docs
chenyan2002 Nov 7, 2026
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
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ edition = "2024"

[dependencies]
trace = { path = "crates/trace" }
serde.workspace = true
anyhow = "1.0.98"
clap = { version = "4.5.40", features = ["derive"] }
heck = "0.5.0"
Expand Down
109 changes: 102 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build-components build-cli test test-cargo test-record test-fixture test-fuzz test-dialog run-fuzz run-record run-dialog run-viceroy
.PHONY: all build-components build-cli test test-cargo test-record test-fixture test-record-p3 test-record-http-p3 test-fuzz-replay test-fuzz test-dialog run-fuzz run-fuzz-replay run-record run-dialog run-viceroy

# `make test ONE_BYTE_PAGE=1` tests with 1-byte-page memories
ifeq ($(ONE_BYTE_PAGE),1)
Expand All @@ -7,6 +7,9 @@ WASMTIME_FLAGS = -W custom-page-sizes=y
endif
INSTRUMENT = target/release/proxy-component instrument $(INSTRUMENT_FLAGS)
WASMTIME = wasmtime $(WASMTIME_FLAGS)
# Runs wasmtime, and exits with 2 if the main component trapped, rather than the proxy failed
# (see tests/run-wasm.sh)
RUN_WASM = tests/run-wasm.sh

all: build-components build-cli
build-cli:
Expand All @@ -20,15 +23,22 @@ build-components:
cp target/one-byte-page/wasm32-wasip2/release/debug.wasm assets/debug.one_byte_page.wasm
cp target/one-byte-page/wasm32-wasip2/release/recorder.wasm assets/recorder.one_byte_page.wasm

test: test-cargo test-fuzz test-record test-fixture test-dialog
test: test-cargo test-fuzz test-record test-fixture test-record-p3 test-record-http-p3 test-fuzz-replay test-dialog

# Snapshot tests of the generated sources
test-cargo:
cargo test

# The WASI p3 main component of tests/fixtures/p3, which uses error-context
P3_GUEST = tests/fixtures/p3/guest
P3_GUEST_OUT = target/fixture-p3
P3_GUEST_WASM = $(P3_GUEST_OUT)/wasm32-wasip2/release/proxy_p3_guest.wasm
test-fuzz:
$(MAKE) run-fuzz WASM=tests/calculator.wasm
$(MAKE) run-fuzz WASM=tests/wasi_http.wasm
$(MAKE) run-fuzz WASM=tests/wasi_http_p3.wasm
cargo build --manifest-path $(P3_GUEST)/Cargo.toml --target wasm32-wasip2 --release --target-dir $(P3_GUEST_OUT)
$(MAKE) run-fuzz WASM=$(P3_GUEST_WASM) WASMTIME_FLAGS="$(WASMTIME_FLAGS) -W component-model-error-context=y"
# build-only test
$(INSTRUMENT) -m fuzz tests/rust.wasm
$(INSTRUMENT) -m fuzz tests/go.wasm
Expand All @@ -47,9 +57,9 @@ test-record:
$(INSTRUMENT) -m record tests/wasi_http.wasm
$(INSTRUMENT) -m replay tests/wasi_http.wasm

# Records and replays the main component of tests/fixtures/proxy-run/guest, whose resources are
# Records and replays the main component of tests/fixtures/p2/guest, whose resources are
# dropped by the guest and by the host, in a composition with a host and a driver (see compose.wac)
FIXTURE = tests/fixtures/proxy-run
FIXTURE = tests/fixtures/p2
FIXTURE_OUT = target/fixture
FIXTURE_WASM = $(FIXTURE_OUT)/wasm32-wasip2/release
test-fixture:
Expand All @@ -60,21 +70,106 @@ test-fixture:
wac compose $(FIXTURE)/compose.wac -d test:mockhost=$(FIXTURE_WASM)/proxy_host.wasm -d test:driver=$(FIXTURE_WASM)/proxy_driver.wasm -d test:main=composed.wasm -o $(FIXTURE_OUT)/record.wasm
$(WASMTIME) --invoke 'run()' $(FIXTURE_OUT)/record.wasm > $(FIXTURE_OUT)/trace.out
# the host's drops of the guest's sessions
grep -c '"ExportArgs":{"method":"drop"' $(FIXTURE_OUT)/trace.out | grep -qx 2
grep -c '"by":"Host","at":null,"event":{"Drop"' $(FIXTURE_OUT)/trace.out | grep -qx 2
$(INSTRUMENT) -m replay $(FIXTURE_WASM)/proxy_guest.wasm
$(WASMTIME) --invoke 'start()' composed.wasm < $(FIXTURE_OUT)/trace.out
$(INSTRUMENT) -m replay --use-host-recorder $(FIXTURE_WASM)/proxy_guest.wasm
target/release/proxy-component run composed.wasm --invoke 'start()' --trace $(FIXTURE_OUT)/trace.out

# Records the main component of tests/fixtures/p3-record/guest, whose async imports, streams
# and futures wasmtime implements itself, and replays each trace: `run` with each pair of delays,
# RETURNED for 0, or random ones, `race`, which cancels the call that loses, and `cat` with stdin
# in one chunk, in several, or none.
P3_RECORD = tests/fixtures/p3-record/guest
P3_RECORD_OUT = target/fixture-p3-record
P3_RECORD_WASM = $(P3_RECORD_OUT)/wasm32-wasip2/release/proxy_p3_record_guest.wasm
test-record-p3:
cargo build --manifest-path $(P3_RECORD)/Cargo.toml --target wasm32-wasip2 --release --target-dir $(P3_RECORD_OUT)
$(INSTRUMENT) -m record $(P3_RECORD_WASM)
cp composed.wasm $(P3_RECORD_OUT)/record.wasm
$(INSTRUMENT) -m replay $(P3_RECORD_WASM)
cp composed.wasm $(P3_RECORD_OUT)/replay.wasm
for call in "run 5 15" "run 15 5" "run 0 10" "run 10 0" "run 0 0" "run 5 5" "run random" "run random" "run random" "run random" "run random" \
"race 5 30" "race 30 5" "race 10 0" "race 0 0" "race random" "race random" "race random"; do \
set -- $$call; env=""; [ "$$2" = random ] || env="--env DELAY_A=$$2 --env DELAY_B=$$3"; \
$(WASMTIME) $$env --invoke "$$1()" $(P3_RECORD_OUT)/record.wasm > $(P3_RECORD_OUT)/trace.out 2>&1 || { \
cat $(P3_RECORD_OUT)/trace.out; echo "recording failed, with the output above"; exit 1; }; \
$(WASMTIME) --invoke 'start()' $(P3_RECORD_OUT)/replay.wasm < $(P3_RECORD_OUT)/trace.out > $(P3_RECORD_OUT)/replay.out 2>&1 || { \
cat $(P3_RECORD_OUT)/replay.out; echo "replay failed, with the output above, of the trace in $(P3_RECORD_OUT)/trace.out"; exit 1; }; \
done
for input in "printf ''" "printf first; sleep 0.05; printf second" "for i in 1 2 3 4 5; do echo line \$$i; sleep 0.01; done" "head -c 10000 /dev/zero"; do \
sh -c "$$input" | $(WASMTIME) --invoke 'cat()' $(P3_RECORD_OUT)/record.wasm > $(P3_RECORD_OUT)/trace.out 2> $(P3_RECORD_OUT)/stderr.out || { \
cat $(P3_RECORD_OUT)/trace.out $(P3_RECORD_OUT)/stderr.out; echo "recording failed, with the output above"; exit 1; }; \
$(WASMTIME) --invoke 'start()' $(P3_RECORD_OUT)/replay.wasm < $(P3_RECORD_OUT)/trace.out > $(P3_RECORD_OUT)/replay.out 2>&1 || { \
cat $(P3_RECORD_OUT)/replay.out; echo "replay failed, with the output above, of the trace in $(P3_RECORD_OUT)/trace.out"; exit 1; }; \
done

# Records tests/wasi_http_p3.wasm under `wasmtime serve`, which reuses an instance for the requests,
# so its trace has an export call for each, and replays it. Not its echo (see
# docs/p3-record-replay.md, Labels), nor a fetch, which needs the network.
HTTP_OUT = target/fixture-http-p3
HTTP_ADDR = 127.0.0.1:8789
test-record-http-p3:
mkdir -p $(HTTP_OUT)
$(INSTRUMENT) -m record tests/wasi_http_p3.wasm
cp composed.wasm $(HTTP_OUT)/record.wasm
$(INSTRUMENT) -m replay tests/wasi_http_p3.wasm
cp composed.wasm $(HTTP_OUT)/replay.wasm
wasmtime serve $(WASMTIME_FLAGS) -S p3,http,cli --no-logging-prefix --addr $(HTTP_ADDR) $(HTTP_OUT)/record.wasm > $(HTTP_OUT)/trace.out 2> $(HTTP_OUT)/serve.err & echo $$! > $(HTTP_OUT)/serve.pid; \
sleep 2; \
for path in / "/wait?ms=20" /random "/wait?ms=5" /; do \
curl -sf -m 10 "http://$(HTTP_ADDR)$$path" > /dev/null || { kill `cat $(HTTP_OUT)/serve.pid`; cat $(HTTP_OUT)/serve.err; echo "GET $$path failed"; exit 1; }; \
done; \
kill `cat $(HTTP_OUT)/serve.pid`
$(WASMTIME) --invoke 'start()' $(HTTP_OUT)/replay.wasm < $(HTTP_OUT)/trace.out > $(HTTP_OUT)/replay.out 2>&1 || { \
cat $(HTTP_OUT)/replay.out; echo "replay failed, with the output above, of the trace in $(HTTP_OUT)/trace.out"; exit 1; }

# The spike's guest, which joins two concurrent results in the order its wakeups receive them
RACE = tests/fixtures/spike-p3-race
RACE_OUT = target/fixture-race
RACE_WASM = $(RACE_OUT)/wasm32-wasip2/release/race_guest.wasm
# Fuzzes the main components of tests/fixtures/p2/guest, the p3 and p3-record fixtures,
# wasi_http_p3 and the spike's guest, and replays each trace it writes, which ends as the fuzzing run did: to the
# end, or in the main component's trap, e.g. a guest's panic on a fuzzed error.
test-fuzz-replay:
cargo build --manifest-path $(FIXTURE)/guest/Cargo.toml --target wasm32-wasip2 --release --target-dir $(FIXTURE_OUT)
$(MAKE) run-fuzz-replay WASM=$(FIXTURE_WASM)/proxy_guest.wasm OUT=$(FIXTURE_OUT)
cargo build --manifest-path $(P3_GUEST)/Cargo.toml --target wasm32-wasip2 --release --target-dir $(P3_GUEST_OUT)
$(MAKE) run-fuzz-replay WASM=$(P3_GUEST_WASM) OUT=$(P3_GUEST_OUT) WASMTIME_FLAGS="$(WASMTIME_FLAGS) -W component-model-error-context=y"
cargo build --manifest-path $(P3_RECORD)/Cargo.toml --target wasm32-wasip2 --release --target-dir $(P3_RECORD_OUT)
$(MAKE) run-fuzz-replay WASM=$(P3_RECORD_WASM) OUT=$(P3_RECORD_OUT)
$(MAKE) run-fuzz-replay WASM=tests/wasi_http_p3.wasm OUT=target/fixture-http-p3
cargo build --manifest-path $(RACE)/Cargo.toml -p race-guest --target wasm32-wasip2 --release --target-dir $(RACE_OUT)
$(MAKE) run-fuzz-replay WASM=$(RACE_WASM) OUT=$(RACE_OUT)
run-fuzz-replay:
mkdir -p $(OUT)
$(INSTRUMENT) -m fuzz $(WASM)
cp composed.wasm $(OUT)/fuzz.wasm
$(INSTRUMENT) -m replay $(WASM)
cp composed.wasm $(OUT)/replay.wasm
for i in 1 2 3 4 5 6 7 8 9 10; do \
$(RUN_WASM) $(OUT)/fuzz-trace.out $(WASMTIME_FLAGS) --invoke 'start()' $(OUT)/fuzz.wasm > /dev/null; fuzz=$$?; \
if [ $$fuzz -eq 1 ]; then \
cat $(OUT)/fuzz-trace.out; echo "fuzzing failed, with the output above, in $(OUT)/fuzz-trace.out"; exit 1; \
fi; \
$(RUN_WASM) $(OUT)/replay.out $(WASMTIME_FLAGS) --invoke 'start()' $(OUT)/replay.wasm < $(OUT)/fuzz-trace.out > /dev/null; \
if [ $$? -ne $$fuzz ]; then \
cat $(OUT)/replay.out; echo "replay failed, with the output above, in $(OUT)/replay.out, of the trace in $(OUT)/fuzz-trace.out"; exit 1; \
fi; \
done

test-dialog:
rm tests/composed.wasm || true
for wasm in tests/*.wasm; do \
$(MAKE) run-dialog WASM=$$wasm; \
$(MAKE) run-dialog WASM=$$wasm || exit 1; \
done
cargo build --manifest-path $(P3_GUEST)/Cargo.toml --target wasm32-wasip2 --release --target-dir $(P3_GUEST_OUT)
$(MAKE) run-dialog WASM=$(P3_GUEST_WASM)

# A trap of the main component is what fuzzing finds, not a failure.
run-fuzz:
$(INSTRUMENT) -m fuzz $(WASM)
$(WASMTIME) --invoke 'start()' composed.wasm
$(RUN_WASM) /tmp/composed.out $(WASMTIME_FLAGS) --invoke 'start()' composed.wasm; [ $$? -ne 1 ]

run-record:
$(INSTRUMENT) -m record $(WASM)
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ To build the CLI tool, just run `make all`.
To test record and fuzzing, run `make test`.

`cargo test` checks the generated sources against the snapshots in `tests/snapshots`, generated
from the WIT in `tests/fixtures/proxy`. After an intended change, run `BLESS=1 cargo test` and
from the WIT in `tests/fixtures/p2/wit` and `tests/fixtures/p3/wit`. After an intended change, run `BLESS=1 cargo test` and
review the snapshots' diff.

## Usage
Expand All @@ -30,6 +30,12 @@ Run `composed.wasm` in the host runtime which the original wasm is supposed to r

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.

Note that the recorded handle id is not an exact match with the handle id the main component sees. Each
component instance has its own handle table, and the main component can allocate handles in it
that the proxy never sees, e.g., its own resources and streams. Each of those
shifts the main component's handle ids, so the imports component's handle ids, from its own table,
can't match them exactly.

### Replay

Assuming the trace captured from the record phase is stored in `trace.out`. We can run the following to replay the trace.
Expand All @@ -54,7 +60,7 @@ $ proxy-component instrument -m fuzz <component.wasm>
$ wasmtime --invoke 'start()' composed.wasm
```

Fuzzing the import return and export input based on the WIT type. This mode requires a [Debug component](components/debug/) to get random numbers and logging. The `composed.wasm` can be run in
Fuzzing the import return and export input based on the WIT type. This mode requires a [Debug component](components/debug/) to get random numbers. An import's result is drawn from a hash of its arguments and a seed for the run, so the same call gets the same result within a run, and another in the next run. The run prints its trace to stdout, in the same format as record mode, so `wasmtime --invoke 'start()' composed.wasm > trace.out` stores it, for replay mode to replay. To read the trace instead, `wasmtime --env PROXY_TRACE=text --invoke 'start()' composed.wasm` prints it as text. The `composed.wasm` can be run in
a standalone `wasmtime` without any special host functions.

### Dialog
Expand Down
Loading
Loading