Preserve unreachable struct.set of non-nullable locals#8844
Conversation
Binaryen usually elides all instructions between unreachable instructions and the ends of their surrounding control flow structures. It also elides unnamed blocks. Meanwhile, WebAssembly validation rules require that gets of non-nullable locals are preceded by sets of the same locals in the same or outer blocks. The combination of these circumstances could previously cause Binaryen to emit invalid modules where local.sets after unreachables in unnamed blocks were elided but also required for validation by later local.gets after the unnamed blocks. Fix the problem by specifically finding and emitting local.sets of non-nullable locals after unreachable instructions, emitting unreachable LocalSets as local.set instead of local.tee, and eliding more unnamed blocks. Update the fuzzer to no longer run DCE unconditionally at the end of the pipeline, which was the previous workaround for this problem. Fixes #5599.
|
Fuzzer is happy with 85k+ iterations on this. |
|
cc @rmahdav This removes the need for the --dce workaround. |
| bool sawUnreachable = false; | ||
| for (auto* child : ValueChildIterator(curr)) { | ||
| if (sawUnreachable) { | ||
| emitUnreachableLocalSets(curr); |
There was a problem hiding this comment.
Should curr be child here? Otherwise this looks quadratic.
| for (auto* child : ValueChildIterator(curr)) { | ||
| if (sawUnreachable) { | ||
| emitUnreachableLocalSets(curr); | ||
| continue; |
There was a problem hiding this comment.
In addition, even if this was child, this recurses into all sub-children of that child? Is that what we want here?
There was a problem hiding this comment.
Oh yes, this should be child. We do want to walk the entire child tree, though, because there might be important sets arbitrarily far down in it. This is not quadratic because this is a recursive visitor, not a walker, so the child subtrees will only ever be visited this one time.
| ;; CHECK-BIN-NEXT: (unreachable) | ||
| ;; CHECK-BIN-NEXT: (local.set $x | ||
| ;; CHECK-BIN-NEXT: (unreachable) | ||
| ;; CHECK-BIN-NEXT: ) |
There was a problem hiding this comment.
Why emit a local.set here? The type is not a non-nullable reference.
There was a problem hiding this comment.
Because it's harmless and simpler than looking at the type of the local to determine whether to emit a set or a tee. In contrast, when we find and emit unreachable sets in the binary writer, there is a code size benefit to including only sets of non-nullable locals (although only in non-DCE'd code, so the benefit isn't very important).
There was a problem hiding this comment.
Then I'm not following something. This is in CHECK-BIN, but you're saying this is not testing the binary writer?
There was a problem hiding this comment.
Well there's two new things going on:
- Emitting the local.sets that come after unreachable instructions. This only happens in the binary writer (and Stack IR generator and poppifier).
- Emitting unreachable-typed LocalSets as local.sets instead of local.tees. This happens in both the binary writer and Printer.
Your question here was about (2), where we don't differentiate between defaultable and non-defaultable locals, and I was contrasting it to (1), where we only do the new thing for non-defaultable locals.
There was a problem hiding this comment.
Oh, actually I meant 1. Why is there a local.set here at all, is my question? I would expect no local.set to be emitted here at all, for code size. We don't need this local.set because of the type.
There was a problem hiding this comment.
Oh, I see. Good question. Will fix.
|
Looks like this does not handle all cases: (module
(type $0 (func (result f64)))
(type $1 (func))
(type $2 (func (result i32 (ref (exact $0)) f32)))
(func $0
(local $0 i32)
(local $1 i32)
(local $2 i32)
(local $3 i32)
(local $4 i32)
(local $5 i32)
(local $6 i32)
(local $7 i32)
(local $8 i32)
(local $9 i32)
(local $10 i32)
(local $11 i32)
(local $12 i32)
(local $13 i32)
(local $14 i32)
(local $15 i32)
(local $16 i32)
(local $17 i32)
(local $18 i32)
(local $19 i32)
(local $20 i32)
(local $21 i32)
(local $22 i32)
(local $23 i32)
(local $24 i32)
(local $25 i32)
(local $26 i32)
(local $27 i32)
(local $28 i32)
(local $29 i32)
(local $30 i32)
(local $31 i32)
(local $32 i32)
(local $33 i32)
(local $34 i32)
(local $35 i32)
(local $36 i32)
(local $37 i32)
(local $38 i32)
(local $39 i32)
(local $40 i32)
(local $41 i32)
(local $42 i32)
(local $43 i32)
(local $44 i32)
(local $45 i32)
(local $46 i32)
(local $47 i32)
(local $48 i32)
(local $49 i32)
(local $50 i32)
(local $51 i32)
(local $52 i32)
(local $53 i32)
(local $54 i32)
(local $55 i32)
(local $56 i32)
(local $57 i32)
(local $58 i32)
(local $59 i32)
(local $60 i32)
(local $61 i32)
(local $62 i32)
(local $63 i32)
(local $64 i32)
(local $65 i32)
(local $66 i32)
(local $67 i32)
(local $68 i32)
(local $69 i32)
(local $70 i32)
(local $71 i32)
(local $72 i32)
(local $73 i32)
(local $74 i32)
(local $75 i32)
(local $76 i32)
(local $77 i32)
(local $78 i32)
(local $79 i32)
(local $80 i32)
(local $81 i32)
(local $82 i32)
(local $83 f32)
(local $84 f32)
(local $85 f32)
(local $86 f32)
(local $87 f32)
(local $88 f32)
(local $89 f32)
(local $90 f32)
(local $91 f32)
(local $92 f32)
(local $93 f32)
(local $94 f32)
(local $95 f32)
(local $96 f32)
(local $97 f32)
(local $98 f32)
(local $99 f32)
(local $100 f32)
(local $101 f32)
(local $102 f32)
(local $103 f32)
(local $104 f32)
(local $105 f32)
(local $106 f32)
(local $107 f32)
(local $108 f32)
(local $109 f32)
(local $110 f32)
(local $111 f32)
(local $112 f32)
(local $113 f32)
(local $114 f32)
(local $115 f32)
(local $116 (ref (exact $0)))
(local $117 (ref (exact $0)))
(local $118 (ref (exact $0)))
(local $119 (ref (exact $0)))
(local $120 (ref (exact $0)))
(local $121 (ref (exact $0)))
(local $122 (ref (exact $0)))
(local $123 (ref (exact $0)))
(local $124 (ref (exact $0)))
(local $125 (ref (exact $0)))
(local $126 (ref (exact $0)))
(local $127 (ref (exact $0)))
(local $128 (ref (exact $0)))
(local $129 (ref (exact $0)))
(local $130 (ref (exact $0)))
(local $131 (ref (exact $0)))
(local $132 (ref (exact $0)))
(local $133 (ref (exact $0)))
(local $134 (ref (exact $0)))
(local $135 (ref (exact $0)))
(local $136 (ref (exact $0)))
(local $137 (ref (exact $0)))
(local $138 (ref (exact $0)))
(local $139 (ref (exact $0)))
(local $140 (ref (exact $0)))
(local $141 (ref (exact $0)))
(local $142 (ref (exact $0)))
(local $143 (ref (exact $0)))
(local $144 (ref (exact $0)))
(local $145 (ref (exact $0)))
(local $146 (ref (exact $0)))
(local $147 (ref (exact $0)))
(local $148 (ref (exact $0)))
(local $149 (ref (exact $0)))
(local $150 (ref (exact $0)))
(local $151 (ref (exact $0)))
(local $152 (ref (exact $0)))
(local $153 (ref (exact $0)))
(local $154 (ref (exact $0)))
(local $155 (ref (exact $0)))
(local $156 (ref (exact $0)))
(local $157 (ref (exact $0)))
(local $158 (ref (exact $0)))
(local $159 (ref (exact $0)))
(local $160 (ref (exact $0)))
(local $161 (ref (exact $0)))
(local $162 (ref (exact $0)))
(local $163 (ref (exact $0)))
(local $164 (ref (exact $0)))
(local $165 (ref (exact $0)))
(local $166 (ref (exact $0)))
(local $167 (ref (exact $0)))
(local $168 (ref (exact $0)))
(local $169 (ref (exact $0)))
(local $170 (ref (exact $0)))
(local $171 (ref (exact $0)))
(local $172 (ref (exact $0)))
(local $173 (ref (exact $0)))
(local $174 (ref (exact $0)))
(local $175 (ref (exact $0)))
(local $176 (ref (exact $0)))
(local $177 (ref (exact $0)))
(local $178 (ref (exact $0)))
(local $179 (ref (exact $0)))
(local $180 (ref (exact $0)))
(local $181 (ref (exact $0)))
(local $182 (ref (exact $0)))
(local $183 (ref null (exact $0)))
(local $184 (ref null (exact $0)))
(local $185 (ref null (exact $0)))
(local $186 (ref null (exact $0)))
(local $187 (ref null (exact $0)))
(local $188 (ref null (exact $0)))
(local $189 (ref null (exact $0)))
(local $190 (ref null (exact $0)))
(local $191 (ref null (exact $0)))
(local $192 (ref null (exact $0)))
(local $193 (ref null (exact $0)))
(local $194 (ref null (exact $0)))
(local $195 (ref null (exact $0)))
(local $196 (ref null (exact $0)))
(local $197 (ref null (exact $0)))
(local $198 (ref null (exact $0)))
(local $199 (ref null (exact $0)))
(local $scratch (tuple i32 (ref (exact $0)) f32))
(local $scratch_201 (ref (exact $0)))
(local $scratch_202 i32)
(local.set $82
(local.tee $81
(block (result i32)
(local.set $scratch_202
(tuple.extract 3 0
(local.tee $scratch
(loop (type $2) (result i32 (ref (exact $0)) f32)
(unreachable)
)
)
)
)
(local.set $181
(block (result (ref (exact $0)))
(local.set $scratch_201
(tuple.extract 3 1
(local.get $scratch)
)
)
(local.set $115
(tuple.extract 3 2
(local.get $scratch)
)
)
(local.get $scratch_201)
)
)
(local.get $scratch_202)
)
)
)
(local.set $182
(local.get $181)
)
(drop
(local.get $115)
)
(drop
(local.get $182)
)
(drop
(local.get $82)
)
)
)STR: bin/wasm-opt t.wasm --monomorphize --pass-arg=monomorphize-min-benefit@50 --minimize-rec-groups --optimize-casts --code-pushing --code-folding --roundtrip --monomorphize --pass-arg=monomorphize-min-benefit@0 --precompute-propagate --simplify-locals -all -o tt.wasm
echo $?
v8.optdebug --experimental-wasm-acquire-release --wasm-staging --fuzzing --experimental-wasm-custom-descriptors scripts/fuzz_shell.js -- tt.wasm &> o
echo $?
grep "uninitialized non-defaultable local" o | wc -lFails with |
|
Fix in #8858 |
Binaryen usually elides all instructions between unreachable instructions and the ends of their surrounding control flow structures. It also elides unnamed blocks. Meanwhile, WebAssembly validation rules require that gets of non-nullable locals are preceded by sets of the same locals in the same or outer blocks. The combination of these circumstances could previously cause Binaryen to emit invalid modules where local.sets after unreachables in unnamed blocks were elided but also required for validation by later local.gets after the unnamed blocks.
Fix the problem by specifically finding and emitting local.sets of non-nullable locals after unreachable instructions, emitting unreachable LocalSets as local.set instead of local.tee, and eliding more unnamed blocks. Update the fuzzer to no longer run DCE unconditionally at the end of the pipeline, which was the previous workaround for this problem.
Fixes #5599.