Skip to content

🏗️ Move wasm-smith's non-trapping mode logic to CodeBuilder - #769

Merged
fitzgen merged 28 commits into
bytecodealliance:mainfrom
itsrainy:rainy/code-builder-no-traps
Nov 9, 2022
Merged

🏗️ Move wasm-smith's non-trapping mode logic to CodeBuilder#769
fitzgen merged 28 commits into
bytecodealliance:mainfrom
itsrainy:rainy/code-builder-no-traps

Conversation

@itsrainy

Copy link
Copy Markdown
Contributor

Prior to this change, the non-trapping logic in wasm-smith was implemented as a post-processing step. Random wasm would get generated and then the no_traps() logic would attempt to modify the generated instructions to ensure that the resulting wasm wouldn't trap. This led to a couple challenges: there are some instructions (such as unreachable and call_indirect) that we don't want to emit at all in non-trapping mode, and there are some instructions that we haven't yet implemented a non-trapping solution for (such as SIMD Lane loads/stores). This meant that running wasm-smith in non-trapping mode could still result in a wasm program that would trap. While discussing how best to communicate and work around this nuance, @fitzgen and I realized that these were probably better dealt with while the code is being generated, rather than after the fact.

This change moves much of the non-trapping logic from src/core/no_traps.rs to a code_builder module (src/core/code_builder/no_traps.rs). It then calls into those functions from CodeBuilder, while entirely avoiding unreachable, call_indirect, and possibly trapping instructions that we haven't yet dealt with.

TODO:

  • I'm currently working on adding a fuzz target to verify that disallow_traps works and to find the cases I inevitably missed

@itsrainy
itsrainy force-pushed the rainy/code-builder-no-traps branch from 5368d5d to 4a4b4a7 Compare September 20, 2022 17:33

@fitzgen fitzgen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, although yeah we should have that fuzz target

Comment thread crates/wasm-smith/src/core.rs Outdated
Comment thread crates/wasm-smith/src/core/code_builder.rs
Comment thread crates/wasm-smith/src/core/code_builder.rs Outdated

/// Mask data and element segments' offsets to be in bounds of their
/// associated tables and memories.
fn no_trapping_segments(&mut self) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i just realized I never moved this over

Comment on lines +4792 to +4861
if module.config.disallow_traps() {
no_traps::store(
Instruction::V128Store { memarg },
module,
builder,
instructions,
);
} else {
instructions.push(Instruction::V128Store { memarg });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m wondering if this pattern would be better served by making a choice between trapping or non-trapping variants and then generating an instruction that is guaranteed to be either way? Are there any instructions that would fall in the “unknown” territory somewhere?

This sort of split model would allow to easily add/maintain other kinds of generation “modes” as well (e.g. deterministic vs non-deterministic), and I wonder if it would result in better fuzz coverage (the fuzzer would be able to flip between the two broad categories of instructions at runtime on a per-instruction basis)

@itsrainy itsrainy Sep 22, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nagisa that's interesting! I'm wondering how useful the "guaranteed to trap" case would actually be. If we were to create a version of this code that does the opposite (ie. makes every division have a denominator of 0, makes every load/store have an out of bounds address, etc), then I would think any generated programs would trap relatively early and not improve coverage all that much except in testing that traps are handled correctly by the runtime. Maybe a wasm-smith flag to generate programs that are guaranteed to eventually trap would be helpful for improving coverage, since that would allow us to explore whether different program state affects the trap-handling behavior of the runtime.

Either way, maybe we discuss this further on #266 (or a new issue)? I think work in that direction could be an improvement on this work, but not immediately necessary.

As far as ease of adding/maintaining other modes, I agree that having this turn into something like the below would be unideal:

if option_1 {
  generate_instruction();
} else if option_2 {
  generate_instruction_but_slightly_differently();
} else {
  generate_instruction_more_differently();
}

That being said, I think I'm inclined to leave that kind of refactoring for when/if we actually add more modes like this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That being said, I think I'm inclined to leave that kind of refactoring for when/if we actually add more modes like this

Of course, that was just me thinking out loud, not necessarily suggesting that this must be implemented here and now ^^ Should have made it clear in my original comment.

@itsrainy

Copy link
Copy Markdown
Contributor Author

@fitzgen I've been finagling with the fuzzer for a little bit and I haven't been able to get any functions to run yet, but I think I might be close(🤞).

The fuzzer currently errs on line 49 with

Insufficient resources: memory minimum size of 281474976710656 pages exceeds memory limits

I'm not sure where that size is coming from, but I think it's in the Memory being generated by dummy_imports. I haven't found any immediately obvious config options for Store, Engine, or Module that seem like they'd let me set that to something reasonable.

Comment on lines +17 to +18
config.threads_enabled = false;
config.allow_start_export = false;
config.max_memories = config.max_memories.min(1);
config.memory64_enabled = true;
config.exceptions_enabled = false;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These config options were mostly an attempt to get around errors that were preventing the Module or Store from getting instantiated. I would not be surprised if some of these aren't actually necessary.

@fitzgen fitzgen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good, a few suggestions below. Is it running and passing?

Comment thread fuzz/Cargo.toml Outdated
path = "fuzz_targets/no-traps.rs"
test = false
doc = false
required-features = ["wasmtime"] No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: no newline at the end of the file.

Comment thread fuzz/fuzz_targets/no-traps.rs Outdated
let (wasm_bytes, config) = match wasm_tools_fuzz::generate_valid_module(&mut u, |config, _| {
config.disallow_traps = true;
config.threads_enabled = false;
config.allow_start_export = false;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be necessary.

Comment thread fuzz/fuzz_targets/no-traps.rs Outdated
config.threads_enabled = false;
config.allow_start_export = false;
config.max_memories = config.max_memories.min(1);
config.memory64_enabled = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be necessary either.

Comment thread fuzz/fuzz_targets/no-traps.rs Outdated
config.disallow_traps = true;
config.threads_enabled = false;
config.allow_start_export = false;
config.max_memories = config.max_memories.min(1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not this one actually.

Comment thread fuzz/fuzz_targets/no-traps.rs Outdated
store.add_fuel(1_000).unwrap();
match func.call(&mut store, &args, &mut results) {
Ok(_) => continue,
Err(_) => panic!("wasm trapped"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trap might be because we ran out of fuel, in which case it is fine and we can continue.

Comment thread fuzz/fuzz_targets/no-traps.rs Outdated
let mut results = dummy::dummy_values(func_ty.results());
let func = instance.get_func(&mut store, export.name()).unwrap();
func_ty.results();
store.add_fuel(1_000).unwrap();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want to reset to exactly 1000 fuel each time, rather than keep adding 1000 in the case where there is left over fuel from last iteration.

https://cs.github.com/bytecodealliance/wasmtime/blob/bb6a8a717ab9dcf0594a577893fb31a6528ada1b/crates/fuzzing/src/oracles.rs?q=set_fuel#L790-L801

Comment on lines +27 to +41
// Validate the module or component and assert that it passes validation.
let mut validator = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures {
component_model: false,
multi_value: config.multi_value_enabled,
multi_memory: config.max_memories > 1,
bulk_memory: true,
reference_types: true,
simd: config.simd_enabled,
relaxed_simd: config.relaxed_simd_enabled,
memory64: config.memory64_enabled,
threads: config.threads_enabled,
exceptions: config.exceptions_enabled,
..wasmparser::WasmFeatures::default()
});
if let Err(e) = validator.validate_all(&wasm_bytes) {
panic!("Invalid module: {}", e);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a little bit easier to read this fuzz target if validation was pulled out into a validate function and then calling exports was also pulled out into its own call_function_exports function or something like that. As is, it is a bit of a wall of code to read.

@fitzgen

fitzgen commented Sep 23, 2022

Copy link
Copy Markdown
Member

The fuzzer currently errs on line 49 with

We can configure a maximum to the size of memories generated to avoid this issue.

@itsrainy

itsrainy commented Sep 23, 2022

Copy link
Copy Markdown
Contributor Author

Looks pretty good, a few suggestions below. Is it running and passing?

No not yet! As-is it bails at line 49 every time so it isn't even making it to the function calls. I'll remove the config options that aren't needed but I might need some help to figure out how to properly instantiate the wasm. I roughly followed the pattern in the failed-instantiations fuzzer, but I think I'm missing something

@itsrainy

itsrainy commented Oct 27, 2022

Copy link
Copy Markdown
Contributor Author

I've spent some time these past couple days tracking down some edge cases in the non-trapping logic. My general workflow for this has been:

  1. Run the no-traps fuzz target (cargo fuzz run no-traps --features=wasmtime -s none)
  2. Wait 3-10 minutes for the fuzzer to find a crash
  3. Minimize the case (cargo fuzz tmin ....)
  4. Figure out what the issue is by looking at the resulting .wat, our trap-avoiding logic, the specific wasmtime error, etc.
  5. Repeat

The amount of time spent at step 2 is increasing (which is good!), but due to the nature of this, it's hard to know if there are 3 more bugs or 300 more bugs. A good "readiness" measure here might be "fuzzer runs for X minutes without crashing" or even better "the fuzz target is starting to find issues elsewhere". For now, I'm going to step away for a day or so and continue plugging away at this later.

an f64. This is necessary because `i64::MAX as f64` will result in an
f64 value that is not representable by an i64.
…ctions

that must check if the memory offset plus the size of the type is within the bounds
of our memory. In order for us to emit this code, that value must fit in a u64.
In the case where the memory offset is u64::MAX, then our rust code panics in trying
to generate the wasm due to an attempt to add with overflow. To solve this, I've added
an upper bound on our memory offsets for load operations while running in non-trapping
mode.
@itsrainy
itsrainy force-pushed the rainy/code-builder-no-traps branch from 3af5bea to a42cabf Compare October 27, 2022 19:52
@itsrainy

itsrainy commented Nov 7, 2022

Copy link
Copy Markdown
Contributor Author

The only traps this is now throwing for me are StackOverflows on modules with recursive functions that look something like this:

  (func (;0;) (type 0) (result f64)
    call 0
     ...
   )

These can't easily be avoided without somehow keeping track of the call-stack size, which we'd have to do in the emitted wasm, and then somehow handling the "we're about to exceed our stack depth" case. It takes the fuzzer a few hours to run into this case on my machine.

From chatting with @fitzgen it sounds like ignoring this for now and documenting the behavior in the config docs should be good for the time being.

If I add a case to ignore StackOverflow traps, like this:

- Err(err) if err.to_string().contains("all fuel consumed") => continue,
+ Err(err)
+     if err.to_string().contains("all fuel consumed")
+         || err.to_string().contains("call stack exhausted") =>
+ {
+     continue
+ }                  

then the fuzzer runs for 20+ hours on my machine! I think this means that we've found all the cases that we would expect to trap 🎉

@fitzgen any other changes you'd like to see here?

@fitzgen fitzgen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks @itsrainy! Just one nitpick about documentation.

Comment thread crates/wasm-smith/src/config.rs

@fitzgen fitzgen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This was a big one!

@fitzgen
fitzgen merged commit 31e4f11 into bytecodealliance:main Nov 9, 2022
@itsrainy
itsrainy deleted the rainy/code-builder-no-traps branch November 9, 2022 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants