Skip to content

Commit a3a0a30

Browse files
committed
Merge branch 'main' into unsubypting-propagate-left
2 parents 1931b55 + cb75dcc commit a3a0a30

13 files changed

Lines changed: 342 additions & 59 deletions

scripts/fuzz_opt.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,9 @@ def handle(self, wasm):
15821582

15831583
run([in_bin('wasm-split'), wasm, '--split',
15841584
'--split-funcs', ','.join(split_funcs),
1585+
# make the new exports easily identifiable, as we need to ignore
1586+
# them in part of fuzz_shell.js
1587+
'--export-prefix=__fuzz_split_',
15851588
'--primary-output', primary,
15861589
'--secondary-output', secondary] + split_feature_opts)
15871590

@@ -1616,7 +1619,12 @@ def optimize(name):
16161619

16171620
# get the output from the split modules, linking them using JS
16181621
# TODO run liftoff/turboshaft/etc.
1619-
linked_output = run_d8_wasm(primary, args=[secondary, exports_to_call])
1622+
args = [
1623+
secondary,
1624+
exports_to_call,
1625+
'--fuzz-split',
1626+
]
1627+
linked_output = run_d8_wasm(primary, args=args)
16201628
linked_output = fix_output(linked_output)
16211629

16221630
# see D8.can_compare_to_self: we cannot compare optimized outputs if

scripts/fuzz_shell.js

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,24 @@ if (!binary) {
4747
// passed a final parameter in the form of "exports:X,Y,Z" then we call
4848
// specifically the exports X, Y, and Z.
4949
var exportsToCall;
50-
if (argv.length > 0 && argv[argv.length - 1].startsWith('exports:')) {
51-
exportsToCall = argv[argv.length - 1].substr('exports:'.length).split(',');
52-
argv.pop();
50+
51+
// Passing --fuzz-split makes us treat the two input files as split off
52+
// from a single one, and we will run them as that single file (ignoring extra
53+
// exports from the second one, and from wasm-split itself). This allows us to
54+
// get the same behavior from split modules as before the split.
55+
var fuzzSplit = false;
56+
57+
for (var i = 0; i < argv.length; i++) {
58+
var curr = argv[i];
59+
if (curr.startsWith('exports:')) {
60+
exportsToCall = curr.substr('exports:'.length).split(',');
61+
argv.splice(i, 1);
62+
i--;
63+
} else if (curr == '--fuzz-split') {
64+
fuzzSplit = true;
65+
argv.splice(i, 1);
66+
i--;
67+
}
5368
}
5469

5570
// If a second parameter is given, it is a second binary that we will link in
@@ -417,8 +432,15 @@ if (secondBinary) {
417432
});
418433
}
419434

420-
// Compile and instantiate a wasm file.
421-
function build(binary) {
435+
// Compile and instantiate a wasm file. Receives the binary to build, and
436+
// whether it is the second one.
437+
function build(binary, isSecond) {
438+
if (fuzzSplit && isSecond) {
439+
assert(secondBinary);
440+
// Provide the primary module's exports to the secondary.
441+
imports['primary'] = exports;
442+
}
443+
422444
var module = new WebAssembly.Module(binary);
423445

424446
var instance;
@@ -429,6 +451,14 @@ function build(binary) {
429451
quit();
430452
}
431453

454+
// Do not add the second instance's exports to the list, as that would be
455+
// noticeable by calls to call-export-*. When fuzzing wasm-split, we want the
456+
// original module's exports to be provided from the primary module, and it is
457+
// the only interface to the outside.
458+
if (fuzzSplit && isSecond) {
459+
return;
460+
}
461+
432462
// Update the exports. Note that this adds onto |exports| and |exportList|,
433463
// which is intentional: if we build another wasm, or build this one more
434464
// than once, we want to be able to call them all, so we unify all their
@@ -446,6 +476,13 @@ function build(binary) {
446476
var value = instance.exports[key];
447477
value = wrapExportForJSPI(value);
448478
exports[key] = value;
479+
480+
if (fuzzSplit && key.startsWith('__fuzz_split_')) {
481+
// We are fuzzing wasm-split, and this is a new export generated by
482+
// wasm-split. Do not note these exports as callable from call-export*,
483+
// as they do not match the original pre-split module.
484+
continue;
485+
}
449486
exportList.push({ name: key, value: value });
450487
}
451488
}
@@ -559,7 +596,7 @@ build(binary);
559596

560597
// Build the second wasm, if one was provided.
561598
if (secondBinary) {
562-
build(secondBinary);
599+
build(secondBinary, true);
563600
}
564601

565602
// Run.

scripts/test/fuzzing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@
107107
'instrument-branch-hints.wast',
108108
# Contains a subtype chain that exceeds depth limits.
109109
'reorder-types-real.wast',
110+
# Contains a name with "__fuzz_split", indicating it is emitted by
111+
# wasm-split, confusing the fuzzer because it is in the initial content.
112+
'fuzz_shell_second.wast',
110113
]
111114

112115

src/passes/AbstractTypeRefining.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,22 @@ struct AbstractTypeRefining : public Pass {
394394
}
395395
}
396396

397+
// If we optimize away a descriptor type, then we must fix up any
398+
// ref.get_desc of it, as ReFinalize would fix us up to return it. This
399+
// can only happen when no such descriptor is created, which means the
400+
// instruction will never be reached (no struct with such a descriptor was
401+
// created).
402+
void visitRefGetDesc(RefGetDesc* curr) {
403+
auto optimized = getOptimized(curr->type);
404+
if (!optimized) {
405+
return;
406+
}
407+
408+
Builder builder(*getModule());
409+
replaceCurrent(builder.makeSequence(builder.makeDrop(curr->ref),
410+
builder.makeUnreachable()));
411+
}
412+
397413
void visitBrOn(BrOn* curr) {
398414
if (curr->op == BrOnNull || curr->op == BrOnNonNull) {
399415
return;

src/passes/ConstantFieldPropagation.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,15 @@ struct FunctionOptimizer : public WalkerPass<PostWalker<FunctionOptimizer>> {
220220

221221
void visitRefGetDesc(RefGetDesc* curr) {
222222
optimizeRead(curr, curr->ref, StructUtils::DescriptorIndex);
223+
224+
// RefGetDesc has the interesting property that we can write a value into
225+
// the field that cannot be read from it: it is valid to write a null, but
226+
// a null can never be read (it would have trapped on the write). Fix that
227+
// up as needed to not break validation.
228+
if (!Type::isSubType(getCurrent()->type, curr->type)) {
229+
Builder builder(*getModule());
230+
replaceCurrent(builder.makeRefAs(RefAsNonNull, getCurrent()));
231+
}
223232
}
224233

225234
void optimizeRead(Expression* curr,
@@ -485,6 +494,10 @@ struct PCVScanner
485494
Index index,
486495
PossibleConstantValues& info) {
487496
info.note(expr, *getModule());
497+
// TODO: For descriptors we can ignore nullable values that are written, as
498+
// they trap. That is, if one place writes a null and another writes a
499+
// global, only the global is readable, and we can optimize there -
500+
// the null is not a second value.
488501
}
489502

490503
void noteDefault(Type fieldType,

src/passes/Heap2Local.cpp

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,10 @@ struct Struct2Local : PostWalker<Struct2Local> {
604604
Builder builder;
605605
const FieldList& fields;
606606

607+
// The descriptor can arrive as nullable, but we trap if it is null, so there
608+
// is only something to store if it is non-nullable, and we store it that way.
609+
Type descType;
610+
607611
Struct2Local(StructNew* allocation,
608612
EscapeAnalyzer& analyzer,
609613
Function* func,
@@ -616,7 +620,8 @@ struct Struct2Local : PostWalker<Struct2Local> {
616620
localIndexes.push_back(builder.addVar(func, field.type));
617621
}
618622
if (allocation->desc) {
619-
localIndexes.push_back(builder.addVar(func, allocation->desc->type));
623+
descType = allocation->desc->type.with(NonNullable);
624+
localIndexes.push_back(builder.addVar(func, descType));
620625
}
621626

622627
// Replace the things we need to using the visit* methods.
@@ -744,7 +749,7 @@ struct Struct2Local : PostWalker<Struct2Local> {
744749
}
745750
}
746751
if (curr->desc) {
747-
tempIndexes.push_back(builder.addVar(func, curr->desc->type));
752+
tempIndexes.push_back(builder.addVar(func, descType));
748753
}
749754

750755
// Store the initial values into the temp locals.
@@ -773,8 +778,7 @@ struct Struct2Local : PostWalker<Struct2Local> {
773778
contents.push_back(builder.makeLocalSet(localIndexes[i], val));
774779
}
775780
if (curr->desc) {
776-
auto* val =
777-
builder.makeLocalGet(tempIndexes[numTemps - 1], curr->desc->type);
781+
auto* val = builder.makeLocalGet(tempIndexes[numTemps - 1], descType);
778782
contents.push_back(
779783
builder.makeLocalSet(localIndexes[fields.size()], val));
780784
}
@@ -902,13 +906,12 @@ struct Struct2Local : PostWalker<Struct2Local> {
902906
} else {
903907
// The cast succeeds iff the optimized allocation's descriptor is the
904908
// same as the given descriptor and traps otherwise.
905-
auto type = allocation->desc->type;
906909
replaceCurrent(builder.blockify(
907910
builder.makeDrop(curr->ref),
908911
builder.makeIf(
909912
builder.makeRefEq(
910913
curr->desc,
911-
builder.makeLocalGet(localIndexes[fields.size()], type)),
914+
builder.makeLocalGet(localIndexes[fields.size()], descType)),
912915
builder.makeRefNull(allocation->type.getHeapType()),
913916
builder.makeUnreachable())));
914917
}
@@ -939,19 +942,13 @@ struct Struct2Local : PostWalker<Struct2Local> {
939942
return;
940943
}
941944

942-
auto type = allocation->desc->type;
943-
Expression* value = builder.makeLocalGet(localIndexes[fields.size()], type);
944-
if (type != curr->type) {
945-
// We know exactly the allocation that flows into this expression, so we
946-
// know the exact type of the descriptor. This type may be more precise
947-
// than the static type of this expression.
948-
refinalize = true;
949-
if (type.isNull()) {
950-
// This traps.
951-
value = builder.makeUnreachable();
952-
}
953-
}
945+
auto descIndex = localIndexes[fields.size()];
946+
Expression* value = builder.makeLocalGet(descIndex, descType);
954947
replaceCurrent(builder.blockify(builder.makeDrop(curr->ref), value));
948+
949+
// After removing the ref.get_desc, a null may be falling through,
950+
// requiring refinalization to update parents.
951+
refinalize = true;
955952
}
956953

957954
void visitStructSet(StructSet* curr) {

src/passes/Print.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3903,4 +3903,12 @@ std::ostream& operator<<(std::ostream& o, wasm::ModuleType pair) {
39033903
return o;
39043904
}
39053905

3906+
std::ostream& operator<<(std::ostream& o, wasm::ModuleHeapType pair) {
3907+
if (auto it = pair.first.typeNames.find(pair.second);
3908+
it != pair.first.typeNames.end()) {
3909+
return o << it->second.name;
3910+
}
3911+
return o << "(unnamed)";
3912+
}
3913+
39063914
} // namespace std

src/passes/Unsubtyping.cpp

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,6 @@ template<typename K, typename V> using Map = std::unordered_map<K, V>;
129129
template<typename T> using Set = std::unordered_set<T>;
130130
#endif
131131

132-
#if UNSUBTYPING_DEBUG
133-
Name getTypeName(Module& wasm, HeapType type) {
134-
if (auto it = wasm.typeNames.find(type); it != wasm.typeNames.end()) {
135-
return it->second.name;
136-
}
137-
return Name("(unnamed)");
138-
}
139-
#endif
140-
141132
// A tree (or rather a forest) of types with the ability to query and set
142133
// supertypes in constant time and efficiently iterate over supertypes and
143134
// subtypes.
@@ -291,13 +282,13 @@ struct TypeTree {
291282
#if UNSUBTYPING_DEBUG
292283
void dump(Module& wasm) {
293284
for (auto& node : nodes) {
294-
std::cerr << getTypeName(wasm, node.type);
285+
std::cerr << ModuleHeapType(wasm, node.type);
295286
if (auto super = getSupertype(node.type)) {
296-
std::cerr << " <: " << getTypeName(wasm, *super);
287+
std::cerr << " <: " << ModuleHeapType(wasm, *super);
297288
}
298289
std::cerr << ", children:";
299290
for (auto child : node.children) {
300-
std::cerr << " " << getTypeName(wasm, nodes[child].type);
291+
std::cerr << " " << ModuleHeapType(wasm, nodes[child].type);
301292
}
302293
std::cerr << '\n';
303294
}
@@ -360,8 +351,8 @@ struct Unsubtyping : Pass {
360351
if (sub == super || sub.isBottom()) {
361352
return;
362353
}
363-
DBG(std::cerr << "noting " << getTypeName(*wasm, sub)
364-
<< " <: " << getTypeName(*wasm, super) << '\n');
354+
DBG(std::cerr << "noting " << ModuleHeapType(*wasm, sub)
355+
<< " <: " << ModuleHeapType(*wasm, super) << '\n');
365356
work.push_back({sub, super});
366357
}
367358

@@ -520,8 +511,8 @@ struct Unsubtyping : Pass {
520511
}
521512

522513
void process(HeapType sub, HeapType super) {
523-
DBG(std::cerr << "processing " << getTypeName(*wasm, sub)
524-
<< " <: " << getTypeName(*wasm, super) << '\n');
514+
DBG(std::cerr << "processing " << ModuleHeapType(*wasm, sub)
515+
<< " <: " << ModuleHeapType(*wasm, super) << '\n');
525516
assert(HeapType::isSubType(sub, super));
526517
auto oldSuper = types.getSupertype(sub);
527518
if (oldSuper) {

src/wasm.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2623,8 +2623,9 @@ class Module {
26232623
// Utility for printing an expression with named types.
26242624
using ModuleExpression = std::pair<Module&, Expression*>;
26252625

2626-
// Utility for printing an type with a name, if the module defines a name.
2626+
// Utilities for printing an type with a name, if the module defines a name.
26272627
using ModuleType = std::pair<Module&, Type>;
2628+
using ModuleHeapType = std::pair<Module&, HeapType>;
26282629

26292630
// Utility for printing only the top level of an expression. Named types will be
26302631
// used if `module` is non-null.
@@ -2648,6 +2649,7 @@ std::ostream& operator<<(std::ostream& o, wasm::Expression& expression);
26482649
std::ostream& operator<<(std::ostream& o, wasm::ModuleExpression pair);
26492650
std::ostream& operator<<(std::ostream& o, wasm::ShallowExpression expression);
26502651
std::ostream& operator<<(std::ostream& o, wasm::ModuleType pair);
2652+
std::ostream& operator<<(std::ostream& o, wasm::ModuleHeapType pair);
26512653

26522654
} // namespace std
26532655

test/lit/node/fuzz_shell_second.wast

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
(func $first (export "first") (result i32)
66
(i32.const 42)
77
)
8+
9+
(func $split (export "__fuzz_split_func") (result i32)
10+
(i32.const 999)
11+
)
812
)
913

1014
;; Build both files to binary.
@@ -18,6 +22,8 @@
1822
;;
1923
;; CHECK: [fuzz-exec] calling first
2024
;; CHECK: [fuzz-exec] note result: first => 42
25+
;; CHECK: [fuzz-exec] calling __fuzz_split_func
26+
;; CHECK: [fuzz-exec] note result: __fuzz_split_func => 999
2127
;; CHECK: [fuzz-exec] calling second
2228
;; CHECK: [fuzz-exec] note result: second => 1337
2329

@@ -29,4 +35,16 @@
2935
;; REVERSE: [fuzz-exec] note result: second => 1337
3036
;; REVERSE: [fuzz-exec] calling first
3137
;; REVERSE: [fuzz-exec] note result: first => 42
38+
;; REVERSE: [fuzz-exec] calling __fuzz_split_func
39+
;; REVERSE: [fuzz-exec] note result: __fuzz_split_func => 999
40+
41+
;; Run with --fuzz-split, which does not run exports from the second one,
42+
;; and also ignores exports starting with "__fuzz_split_"
43+
;;
44+
;; RUN: node %S/../../../scripts/fuzz_shell.js %t.wasm %t.second.wasm --fuzz-split | filecheck %s --check-prefix=WASM_SPLIT
45+
;;
46+
;; WASM_SPLIT: [fuzz-exec] calling first
47+
;; WASM_SPLIT: [fuzz-exec] note result: first => 42
48+
;; WASM_SPLIT-NOT: [fuzz-exec] calling second
49+
;; WASM_SPLIT-NOT: [fuzz-exec] calling __fuzz_split_func
3250

0 commit comments

Comments
 (0)