Skip to content

Commit cedc7ef

Browse files
authored
Print and compare prototypes in fuzzer interpreter (#8457)
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.
1 parent 23f061b commit cedc7ef

14 files changed

Lines changed: 224 additions & 112 deletions

scripts/fuzz_shell.js

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,29 @@ function printed(x, y) {
144144
// Print bigints in legalized form, which is two 32-bit numbers of the low
145145
// and high bits.
146146
return (Number(x & 0xffffffffn) | 0) + ' ' + (Number(x >> 32n) | 0)
147-
} else if (typeof x !== 'number') {
148-
// Something that is not a number or string, like a reference. We can't
149-
// print a reference because it could look different after opts - imagine
150-
// that a function gets renamed internally (that is, the problem is that
151-
// JS printing will emit some info about the reference and not a stable
152-
// external representation of it). In those cases just print the type,
153-
// which will be 'object' or 'function'.
154-
return typeof x;
147+
} else if (typeof x === 'object') {
148+
// This may be one of the externref imports, in which case we can print its
149+
// payload.
150+
if (Object.hasOwn(x, 'payload')) {
151+
return 'externref(' + x.payload + ')';
152+
}
153+
// Or maybe this is a JS error we caught.
154+
if (x instanceof Error) {
155+
return 'jserror';
156+
}
157+
// If this is a Wasm object, we can't access its type or any of its
158+
// internal structure, which might have been changed by optimizations
159+
// anyway. It might have a configured prototype, though, and that
160+
// prototype may be an imported externref global we can identify by the
161+
// payload we gave it.
162+
return 'object(' + printed(Object.getPrototypeOf(x)) + ')';
163+
} else if (typeof x === 'function') {
164+
// We cannot print function names because they might have been changed by
165+
// optimizations.
166+
return 'function';
155167
} else {
156168
// A number. Print the whole thing.
169+
assert(typeof x === 'number');
157170
return '' + x;
158171
}
159172
}
@@ -469,8 +482,15 @@ function makeImports(module) {
469482
baseImports[module] = {};
470483
}
471484
if (!baseImports[module][name]) {
472-
// TODO: Use different payloads for different imports.
473-
baseImports[module][name] = {};
485+
// Compute a payload from the import names. This must be kept in sync
486+
// with execution-results.h.
487+
var payload = 0;
488+
for (var name of [module, name]) {
489+
for (var c of name) {
490+
payload = (payload + c.charCodeAt(0)) % 251;
491+
}
492+
}
493+
baseImports[module][name] = { payload };
474494
}
475495
}
476496
}

src/literal.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,14 @@ class Literal {
737737
Literal externalize() const;
738738
Literal internalize() const;
739739

740+
// Internalize an externalized value or externalize an internalized value,
741+
// otherwise return the literal unmodified.
742+
Literal unwrap() const;
743+
744+
// Get the JS prototype configured via this struct's descriptor, if it exists,
745+
// or null. Assumes this is a reference value.
746+
Literal getJSPrototype() const;
747+
740748
private:
741749
Literal addSatSI8(const Literal& other) const;
742750
Literal addSatUI8(const Literal& other) const;

src/tools/execution-results.h

Lines changed: 94 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <deque>
2222
#include <memory>
2323

24+
#include "ir/import-names.h"
2425
#include "ir/import-utils.h"
2526
#include "shell-interface.h"
2627
#include "support/utilities.h"
@@ -58,20 +59,9 @@ Tag& getJsTag() {
5859
return tag;
5960
}
6061

61-
void printValue(Literal value) {
62-
// Unwrap an externalized GC value to get the actual value, but not strings,
63-
// which are normally a subtype of ext.
64-
if (Type::isSubType(value.type, Type(HeapType::ext, Nullable)) &&
65-
!value.type.isString()) {
66-
value = value.internalize();
67-
}
68-
69-
// An anyref literal is a string.
70-
if (value.type.isRef() &&
71-
value.type.getHeapType().isMaybeShared(HeapType::any)) {
72-
value = value.externalize();
73-
}
62+
constexpr Index jsErrorPayload = 0xbad;
7463

64+
void printValue(Literal value) {
7565
// Don't print most reference values, as e.g. funcref(N) contains an index,
7666
// which is not guaranteed to remain identical after optimizations. Do not
7767
// print the type in detail (as even that may change due to closed-world
@@ -81,22 +71,32 @@ void printValue(Literal value) {
8171
//
8272
// The only references we print in full are strings and i31s, which have
8373
// simple and stable internal structures that optimizations will not alter.
84-
auto type = value.type;
85-
if (type.isRef()) {
86-
if (type.isString() || type.getHeapType().isMaybeShared(HeapType::i31)) {
87-
std::cout << value;
88-
} else if (value.isNull()) {
89-
std::cout << "null";
90-
} else if (type.isFunction()) {
91-
std::cout << "function";
92-
} else {
93-
std::cout << "object";
94-
}
74+
//
75+
// Non-references can be printed in full.
76+
if (!value.type.isRef()) {
77+
std::cout << value;
9578
return;
9679
}
97-
98-
// Non-references can be printed in full.
99-
std::cout << value;
80+
value = value.unwrap();
81+
auto heapType = value.type.getHeapType();
82+
if (heapType.isMaybeShared(HeapType::ext) &&
83+
value.getExternPayload() == jsErrorPayload) {
84+
std::cout << "jserror";
85+
return;
86+
}
87+
if (heapType.isString() || heapType.isMaybeShared(HeapType::ext) ||
88+
heapType.isMaybeShared(HeapType::i31)) {
89+
std::cout << value;
90+
} else if (value.isNull()) {
91+
std::cout << "null";
92+
} else if (heapType.isFunction()) {
93+
std::cout << "function";
94+
} else {
95+
// Print 'object' and its JS-visible prototype, which may be null.
96+
std::cout << "object(";
97+
printValue(value.getJSPrototype());
98+
std::cout << ')';
99+
}
100100
}
101101

102102
} // namespace
@@ -275,7 +275,7 @@ struct LoggingExternalInterface : public ShellExternalInterface {
275275

276276
void throwJSException() {
277277
// JS exceptions contain an externref.
278-
Literals arguments = {Literal::makeExtern(0, Unshared)};
278+
Literals arguments = {Literal::makeExtern(jsErrorPayload, Unshared)};
279279
auto payload = std::make_shared<ExnData>(&jsTag, arguments);
280280
throwException(WasmException{Literal(payload)});
281281
}
@@ -403,8 +403,18 @@ class FuzzerImportResolver
403403
if (mut || !type.isRef() || type.getHeapType() != HeapType::ext) {
404404
return nullptr;
405405
}
406-
// TODO: Generate a distinct payload for each global.
407-
synthesizedGlobals.emplace_back(Literals{Literal::makeExtern(0, Unshared)});
406+
// Optimizations may reorder or remove imports, so we need a distinct
407+
// payload that is independent of the import order. Just compute a simple
408+
// payload integer from the import names. This must be kept in sync with
409+
// fuzz_shell.js.
410+
Index payload = 0;
411+
for (auto name : {name.module, name.name}) {
412+
for (auto c : name.str) {
413+
payload = (payload + static_cast<Index>(c)) % 251;
414+
}
415+
}
416+
synthesizedGlobals.emplace_back(
417+
Literals{Literal::makeExtern(payload, Unshared)});
408418
return &synthesizedGlobals.back();
409419
}
410420

@@ -527,28 +537,56 @@ struct ExecutionResults {
527537
}
528538

529539
bool areEqual(Literal a, Literal b) {
530-
// Don't compare references. There are several issues here that we can't
531-
// fully handle, see https://github.com/WebAssembly/binaryen/issues/3378,
532-
// but the core issue is that since we optimize assuming a closed world, the
533-
// types and structure of GC data can arbitrarily change after
534-
// optimizations, even in ways that are externally visible from outside
535-
// the module.
536-
//
537-
// We can, however, compare strings as they refer to simple data that has a
538-
// consistent representation (the same reasons as why we can print them in
539-
// printValue(), above).
540+
// Only compare some references. In general the optimizer may change
541+
// identities and structures of functions, types, and GC values in ways that
542+
// are not externally observable. We must therefore limit ourselves to
543+
// comparing information that _is_ externally observable.
540544
//
541-
// TODO: Once we support optimizing under some form of open-world
542-
// assumption, we should be able to check that the types and/or structure of
543-
// GC data passed out of the module does not change.
544-
if (a.type.isRef() && !a.type.isString() &&
545-
!a.type.getHeapType().isMaybeShared(HeapType::i31)) {
546-
return true;
545+
// TODO: We could compare more information when we know it will be
546+
// externally visible, for example when the type of the value is public.
547+
if (!a.type.isRef() || !b.type.isRef()) {
548+
return a == b;
549+
}
550+
// The environment always sees externalized references and is able to
551+
// observe the difference between external references and externalized
552+
// internal references. Make sure this is accounted for below by unwrapping
553+
// the references.
554+
a = a.unwrap();
555+
b = b.unwrap();
556+
auto htA = a.type.getHeapType();
557+
auto htB = b.type.getHeapType();
558+
// What type hierarchy a heap type is in is generally observable.
559+
if (htA.getTop() != htB.getTop()) {
560+
return false;
547561
}
548-
if (a != b) {
549-
std::cout << "values not identical! " << a << " != " << b << '\n';
562+
// Null values are observable.
563+
if (htA.isBottom() || htB.isBottom()) {
564+
return a == b;
565+
}
566+
// String values are observable.
567+
if (htA.isString() || htB.isString()) {
568+
return a == b;
569+
}
570+
// i31 values are observable.
571+
if (htA.isMaybeShared(HeapType::i31) || htB.isMaybeShared(HeapType::i31)) {
572+
return a == b;
573+
}
574+
// External references are observable. (These cannot be externalized
575+
// internal references because they've already been unwrapped.)
576+
if (htA.isMaybeShared(HeapType::ext) || htB.isMaybeShared(HeapType::ext)) {
577+
return a == b;
578+
}
579+
// Configured prototypes are observable. Even if they are also opaque Wasm
580+
// references, their having different pointer identities is observable.
581+
// However, we have no way of comparing pointer identities across
582+
// executions, so just recursively look for externally observable
583+
// differences in the prototypes.
584+
if (!areEqual(a.getJSPrototype(), b.getJSPrototype())) {
550585
return false;
551586
}
587+
588+
// Other differences are not observable, so conservatively consider the
589+
// values equal.
552590
return true;
553591
}
554592

@@ -559,6 +597,7 @@ struct ExecutionResults {
559597
}
560598
for (Index i = 0; i < a.size(); i++) {
561599
if (!areEqual(a[i], b[i])) {
600+
std::cout << "values not identical! " << a[i] << " != " << b[i] << '\n';
562601
return false;
563602
}
564603
}
@@ -628,7 +667,13 @@ struct ExecutionResults {
628667
} catch (const TrapException&) {
629668
return Trap{};
630669
} catch (const WasmException& e) {
631-
std::cout << "[exception thrown: " << e << "]" << std::endl;
670+
auto& exn = *e.exn.getExnData();
671+
std::cout << "[exception thrown: " << exn.tag->name;
672+
for (auto val : exn.payload) {
673+
std::cout << ' ';
674+
printValue(val);
675+
}
676+
std::cout << "]" << std::endl;
632677
return Exception{};
633678
} catch (const HostLimitException&) {
634679
// This should be ignored and not compared with, as optimizations can

src/tools/wasm-ctor-eval.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,11 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface {
467467

468468
void throwException(const WasmException& exn) override {
469469
std::stringstream ss;
470-
ss << "exception thrown: " << exn;
470+
auto& data = *exn.exn.getExnData();
471+
ss << "exception thrown: " << data.tag->name;
472+
if (!data.payload.empty()) {
473+
ss << ' ' << data.payload;
474+
}
471475
throw FailToEvalException(ss.str());
472476
}
473477

src/wasm-interpreter.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ namespace wasm {
6161
struct WasmException {
6262
Literal exn;
6363
};
64-
std::ostream& operator<<(std::ostream& o, const WasmException& exn);
6564

6665
// An exception thrown when we try to execute non-constant code, that is, code
6766
// that we cannot properly evaluate at compile time (e.g. if it refers to an

src/wasm/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ set(wasm_SOURCES
77
wasm-binary.cpp
88
wasm-debug.cpp
99
wasm-emscripten.cpp
10-
wasm-interpreter.cpp
1110
wasm-io.cpp
1211
wasm-ir-builder.cpp
1312
wasm-stack.cpp

src/wasm/literal.cpp

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include "emscripten-optimizer/simple_ast.h"
2121
#include "ir/bits.h"
22+
#include "ir/js-utils.h"
2223
#include "literal.h"
2324
#include "pretty_printing.h"
2425
#include "support/bits.h"
@@ -514,6 +515,12 @@ bool Literal::operator==(const Literal& other) const {
514515
return i32 == other.i32;
515516
}
516517
if (heapType.isMaybeShared(HeapType::ext)) {
518+
if (hasExternPayload()) {
519+
if (!other.hasExternPayload()) {
520+
return false;
521+
}
522+
return getExternPayload() == other.getExternPayload();
523+
}
517524
return internalize() == other.internalize();
518525
}
519526
if (heapType.isMaybeShared(HeapType::any)) {
@@ -782,7 +789,14 @@ std::ostream& operator<<(std::ostream& o, Literal literal) {
782789
assert(literal.isData());
783790
auto data = literal.getGCData();
784791
assert(data);
785-
o << "[ref " << literal.type.getHeapType() << ' ' << data->values << ']';
792+
o << "[ref " << literal.type.getHeapType() << ' ' << data->values;
793+
if (!data->desc.isNull()) {
794+
if (!data->values.empty()) {
795+
o << ", ";
796+
}
797+
o << "desc=" << data->desc;
798+
}
799+
o << ']';
786800
}
787801
}
788802
restoreNormalColor(o);
@@ -3009,4 +3023,37 @@ Literal Literal::internalize() const {
30093023
return gcData->values[0];
30103024
}
30113025

3026+
Literal Literal::unwrap() const {
3027+
if (!type.isRef()) {
3028+
return *this;
3029+
}
3030+
if (type.getHeapType().isMaybeShared(HeapType::any)) {
3031+
// An internalized external reference (possibly a string).
3032+
return externalize();
3033+
}
3034+
if (type.getHeapType().isMaybeShared(HeapType::ext) && !hasExternPayload()) {
3035+
// An externalized internal reference.
3036+
return internalize();
3037+
}
3038+
// Something other reference that is not wrapped.
3039+
return *this;
3040+
}
3041+
3042+
Literal Literal::getJSPrototype() const {
3043+
assert(type.isRef());
3044+
if (auto desc = type.getHeapType().getDescriptorType();
3045+
desc && JSUtils::hasPossibleJSPrototypeField(*desc)) {
3046+
auto proto = gcData->desc.getGCData()->values[0].unwrap();
3047+
// Strings and numbers are not valid prototypes, so they appear as null.
3048+
// Externref nulls are also converted to nullref.
3049+
auto protoType = proto.type.getHeapType();
3050+
if (protoType.isMaybeShared(HeapType::i31) ||
3051+
protoType.isMaybeShared(HeapType::string) || protoType.isBottom()) {
3052+
return Literal::makeNull(HeapType::none);
3053+
}
3054+
return proto;
3055+
}
3056+
return Literal::makeNull(HeapType::none);
3057+
}
3058+
30123059
} // namespace wasm

src/wasm/wasm-interpreter.cpp

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)