Skip to content

Print and compare prototypes in fuzzer interpreter#8457

Open
tlively wants to merge 3 commits intomainfrom
fuzzer-log-protos
Open

Print and compare prototypes in fuzzer interpreter#8457
tlively wants to merge 3 commits intomainfrom
fuzzer-log-protos

Conversation

@tlively
Copy link
Member

@tlively tlively commented Mar 11, 2026

When a struct flows out to JS, if it has a descriptor and that desriptor's first field is an externref, that field's value becomes the JS prototype of the struct. There is a class of bugs where we misoptimize something in this setup so that the JS-observable prototype changes. To catch those bugs in the fuzzer, update the fuzzer interpreter and fuzz_shell.js to print the prototypes of objects. This lets the fuzzer make sure that engines like V8 and interpreter agree on whether there is a prototype. Also update the fuzzer interpreter to compare configured prototypes when comparing two execution traces. This lets --fuzz-exec detect when optimizations have changed a prototype.

tlively added 2 commits March 11, 2026 15:18
GTO already handled keeping the prototype configuration fields on
descriptors of types that flowed out to JS via @binaryen.js.called
functions, but it did not handle propagating the information that a
type flows out to JS to that type's subtypes. It also did not consider
that types may flow out to JS via imports and exports. Fix these issues.
When a struct flows out to JS, if it has a descriptor and that desriptor's first field is an externref, that field's value becomes the JS prototype of the struct. There is a class of bugs where we misoptimize something in this setup so that the JS-observable prototype changes. To catch those bugs in the fuzzer, update the fuzzer interpreter and fuzz_shell.js to print the prototypes of objects. This lets the fuzzer make sure that engines like V8 and interpreter agree on whether there is a prototype. Also update the fuzzer interpreter to compare configured prototypes when comparing two execution traces. This lets --fuzz-exec detect when optimizations have changed a prototype.
@tlively tlively requested a review from kripken March 11, 2026 23:51
Base automatically changed from gto-jsinterop-subtypes-exports to main March 12, 2026 15:20
Copy link
Member

Choose a reason for hiding this comment

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

Does this PR need rebasing?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, just merged now.

}
// The environment always sees externalized references and is able to
// observe the difference between external references and externalized
// internal references. Make sure this is accounted for below by unrapping
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// internal references. Make sure this is accounted for below by unrapping
// internal references. Make sure this is accounted for below by unwrapping

std::ostream& operator<<(std::ostream& o, const WasmException& exn) {
auto exnData = exn.exn.getExnData();
return o << exnData->tag->name << " " << exnData->payload;
}
Copy link
Member

Choose a reason for hiding this comment

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

Why is this not needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

This was only used in two places: wasm-ctor-eval and execution-results. The latter was updated to use its own printValue function to print exception payloads, so I just inlined this original method into the single remaining use site in wasm-ctor-eval.

;; CHECK-NEXT: [exception thrown: A [ref (type $array.0 (array (mut i32))) (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0[..])]]
;; CHECK-NEXT: [exception thrown: A object(null)]
(func $array (export "array") (result (ref $A))
;; Throw a very large array. We should not print all 12K items in it, as that
Copy link
Member

Choose a reason for hiding this comment

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

Comment needs updating. But why is the new output better?

Copy link
Member Author

Choose a reason for hiding this comment

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

The new output matches what will be printed by fuzz_shell.js. I figure that the less they differ, the simpler.

Copy link
Member

Choose a reason for hiding this comment

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

It is simpler, yeah. We are losing something though, when the fuzzer compares its logging to itself after opts. See in fuzz_opt.py where we simplify the output for comparison reasons, e.g.

    # funcref(0) has the index of the function in it, and optimizations can
    # change that index, so ignore it
    out = re.sub(r'funcref\([\d\w$+-_:]+\)', 'funcref()', out)

I think we could do a similar thing for this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we could do something similar, but I think it's better not to. The input "fixing" is somewhat complex in the fuzzer, and bugs there can mean missing real bugs. I think it would be better to try to reduce the amount of fixing that happens in fuzz_opt.py. Also, I contend that if we ignore some difference in the fuzzer (and when comparing interpreter executions), then it's clearer if that difference doesn't show up in the textual log, either.

Copy link
Member

Choose a reason for hiding this comment

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

We only ignore it when comparing VMs, though. We can use the differences when comparing a VM to itself.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, the alternative here would be to keep the existing printing and additionally add new printing for the prototypes, then to update fuzz_opt.py as necessary to filter that out as well. Is that what you would prefer?

I really do think it would be attractive to have fuzz_shell.js and execution-results produce identical logging, though :/

Copy link
Member

Choose a reason for hiding this comment

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

I'm more worried about losing fuzzer coverage. But maybe there is a better way to do it. Does --fuzz-exec compare contents more deeply than what we print? That is, if the equality comparison in execution-results.h looks into objects when it can, that might be good enough.

!a.type.getHeapType().isMaybeShared(HeapType::i31)) {
return true;
// TODO: We could compare more information when we know it will be
// externally visible, for example when the type of the value is public.
Copy link
Member

Choose a reason for hiding this comment

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

I guess this TODO covers my last comment. Would it be hard to do, do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, gathering the public types should be easy, and then we could find the most specific public supertype of a GC value and then print the contents visible based on that type. How about I do that as a follow-up? This PR already makes areEqual more discerning than it was before, so there should not even be a temporary regression in coverage.

Copy link
Member

Choose a reason for hiding this comment

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

Plan sgtm.

Though I do think we'd have a temporary regression - coverage in fuzz_opt would decrease as execution-results improves - but I think it's fine, and the execution-results improvements later would more than make up for it.

@tlively
Copy link
Member Author

tlively commented Mar 13, 2026

Sounds good. I'll hold off on landing this until it can get through 100k iterations, but I'll start working on the follow-up.

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.

2 participants