Phase 1 Open source, early, works today

No more code reviews. Fuzz instead.

appfuzz runs your WebAssembly app thousands of times against a simulated backend and finds what a review misses: crashes on empty lists and Unicode, user input in your HTML unescaped, loops that send one database query per row. Pass or fail, in CI.

Install on Linux or macOS

$ curl -fsSL https://appfuzz.dev/install.sh | sh

Then run appfuzz review next to your .wasm.

One page view, as the table grows

Database queries per request, lower is better

  1. 10 rowsdevelopment11
  2. 100 rowslaunch week101
  3. 1,000 rowsa good year1,001

0limit 1001,000

fail n_plus_1 calls to items#get grow as 1·n with n = length of the list items#all returns; cost breaks its limit at n = 100

The chatty test app, Rust build. appfuzz run with 10, 100 and 1,000 rows from the simulated items#all: 11, 101 and 1,001 host calls, and 11 ms, 101 ms and 1,004 ms of simulated database time.

Tested on components built from

Four kinds of bug a quick read lets through.

Each one passes review because the code looks fine. appfuzz doesn't read code. It runs it, with the inputs and backend answers you didn't think of.

Crashes

panics, exceptions, hangs, stack overflows

Empty results, failing queries, Unicode, a leap day, the year 2038. Every crash is shrunk to the smallest input that causes it.

appfuzz probe values

Security

XSS, leaked errors, ungranted imports

Request fields and stored data that reach your HTML unescaped. Internal error text on the page. Imports the platform never granted.

appfuzz review

Database

n+1, nested loops, retry storms

Queries that grow with a list or a request field, the same query twice, a loop that retries a failed write 50 times.

appfuzz amplify

Wrong answers

200 %, order bugs, swallowed errors

Usage above 100 %, a total that changes when rows come back in another order, an error page sent with status 200.

appfuzz probe pairs

Also for ops: CPU and memory that grow faster than the input, slow call chains, busy waiting, one slow dependency that blows the latency budget.

A minute with appfuzz.

Five commands on chatty, a small app that lists items and then fetches each one again. Every output is real, cut short.

~/bench/apps/chatty
$ appfuzz inspect
{
  "exports": [ "bench:web/handler#handle" ],
  "imports": [ "bench:web/items@0.1.0", "wasi:cli", "wasi:io", … ],
  "findings": [
    { "severity": "info", "kind": "host_call_in_loop",
      "title": "handle calls bench:web/items#get inside a loop
               (static; check with `scale`)" }
  ],
  "next": [ "appfuzz run", "appfuzz fuzz --time 2m" ]
}

Same bug, three languages, same finding.

appfuzz never reads your source. It runs the compiled component, so it doesn't care what you wrote it in. One n+1 loop, written three times, reviewed with the same limits.

for it in items::all() {
    if let Ok(full) = items::get(it.id) {
        out += &format!("<li>{}</li>", esc(&full.owner));
    }
}

65 KB component. cargo, wasm32-wasip2. About 11,800 runs a second on 24 threads.

appfuzz review, the same in all three

fail n_plus_1 calls to items#get grow as 1·n with n = length of the list items#all returns; cost breaks its limit at n = 100

fail over_budget handle uses 129 cost, the limit is 100

warn chatty items#get is called for each element items#all returned

warn redundant_calls items#get: 13 of 50 calls repeat earlier calls with the same arguments

More bugs that pass a quick read.

From the 26 test apps appfuzz runs on every commit. Each has one planted bug, and appfuzz must find it and nothing else. Findings word for word.

Assumes the database answers

let it = items::get(id).unwrap();

fail crash when items#get returns err: called `Result::unwrap()` on an `Err` value

A helpful error page

Err(e) => format!(
    "items.get({id}) failed in shard 7: {}",
    esc(&e),
)

warn handle shows internal error details in its output: the error text items#get returned

Retries until it works

match store::save("visits", "1") {
    Ok(id) => break id,
    Err(_) if tries < 50 => tries += 1,
    Err(_) => break 0,
}

warn store#save is called 50 more times in a row after it failed

Usage can't go over 100 %. Can it?

$ appfuzz check value_range .usage 0 100

fail .usage is 200, outside 0–100. Counterexample saved, with the backend data that causes it.

Built for the agent that wrote the app.

Marketing builds a dashboard, Sales a calculator. An agent writes each in an afternoon, and nobody has time to read them. The same agent can run appfuzz on its own work.

JSON everywhere
Every command prints one JSON document.
Always a next step
Each output lists the commands worth running next, ids filled in.
Suspicions become tests
check turns "this can't go over 100" into a search for a counterexample.
Same input, same result
Time and the backend are simulated. Every run replays exactly.

What it won't do.

A pass means the search found nothing. That's evidence, not proof.

Catch malicious apps
Not yet. It finds careless code, not hidden backdoors.
Know your business rules
It can't tell that create writes, or that a field is personal data.
Test sequences of requests
Each run is one request. Double submits are out of reach for now.
See inside interpreters
TypeScript and Python apps are measured just as well from outside, but the search is less guided and runs are slower.

Try it on your app.

your app
$ curl -fsSL https://appfuzz.dev/install.sh | sh
$ appfuzz --wasm my_app.wasm inspect
$ appfuzz --wasm my_app.wasm review

Open source under the MIT license. Written in Rust on wasmtime. Phase 1 of 3 is done.