Skip to content

Preserve unreachable struct.set of non-nullable locals#8844

Merged
tlively merged 3 commits into
mainfrom
emit-unreachable-local-set
Jun 17, 2026
Merged

Preserve unreachable struct.set of non-nullable locals#8844
tlively merged 3 commits into
mainfrom
emit-unreachable-local-set

Conversation

@tlively

@tlively tlively commented Jun 16, 2026

Copy link
Copy Markdown
Member

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.

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.
@tlively tlively requested a review from a team as a code owner June 16, 2026 17:40
@tlively tlively requested review from kripken and removed request for a team June 16, 2026 17:40
@tlively

tlively commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

Fuzzer is happy with 85k+ iterations on this.

@tlively

tlively commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

cc @rmahdav This removes the need for the --dce workaround.

Comment thread src/wasm-stack.h Outdated
bool sawUnreachable = false;
for (auto* child : ValueChildIterator(curr)) {
if (sawUnreachable) {
emitUnreachableLocalSets(curr);

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.

Should curr be child here? Otherwise this looks quadratic.

Comment thread src/wasm-stack.h
for (auto* child : ValueChildIterator(curr)) {
if (sawUnreachable) {
emitUnreachableLocalSets(curr);
continue;

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.

In addition, even if this was child, this recurses into all sub-children of that child? Is that what we want here?

@tlively tlively Jun 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment thread test/lit/basic/polymorphic_stack.wast Outdated
;; CHECK-BIN-NEXT: (unreachable)
;; CHECK-BIN-NEXT: (local.set $x
;; CHECK-BIN-NEXT: (unreachable)
;; CHECK-BIN-NEXT: )

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.

Why emit a local.set here? The type is not a non-nullable reference.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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).

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.

Then I'm not following something. This is in CHECK-BIN, but you're saying this is not testing the binary writer?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Well there's two new things going on:

  1. Emitting the local.sets that come after unreachable instructions. This only happens in the binary writer (and Stack IR generator and poppifier).
  2. 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.

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.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh, I see. Good question. Will fix.

@tlively tlively merged commit adc1e13 into main Jun 17, 2026
16 checks passed
@tlively tlively deleted the emit-unreachable-local-set branch June 17, 2026 16:37
@kripken

kripken commented Jun 17, 2026

Copy link
Copy Markdown
Member

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 -l
$ bash d.sh
0
1
2

Fails with scripts/fuzz_shell.js:514: CompileError: WebAssembly.Module(): Compiling function #0 failed: uninitialized non-defaultable local: 191 @+55

@tlively

tlively commented Jun 18, 2026

Copy link
Copy Markdown
Member Author

Fix in #8858

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.

Invalid non-nullable local binary emitting related to unreachability

2 participants