Skip to content

Pop in catch emitted inside a block ("not enough arguments on the stack" from VM) #4237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
kripken opened this issue Oct 12, 2021 · 9 comments · Fixed by #4348
Closed

Pop in catch emitted inside a block ("not enough arguments on the stack" from VM) #4237

kripken opened this issue Oct 12, 2021 · 9 comments · Fixed by #4348
Assignees

Comments

@kripken
Copy link
Member

kripken commented Oct 12, 2021

(module
 (tag $tag (param i32))

 (func $foo
  (local $temp i32)
  (try
   (do
    (nop)
   )
   (catch $tag
    (call $call
     (block (result i32)
      (local.set $temp
       (pop i32)
      )
      (call $nop)
      (local.get $temp)
     )
    )
   )
  )
 )

 (func $call (param i32))
 (func $nop)
)

In this IR the pop exists in a place that Binaryen accepts but wasm does not: we use a pop to get the catch value, but we are inside a block so the wasm does not validate, and a VM will error on something like this:

CompileError: WebAssembly.instantiate(): Compiling function #0 failed: not enough arguments on the stack for local.set (need 1, got 0) @+43)

That is with just translating that wat into a binary, bin/wasm-opt a.wat -all -o a.out.wasm. However, if we add a --roundtrip in that command, then the IR becomes this:

   (catch $tag$0
    (local.set $temp
     (pop i32)
    )
    (call $call
     (block $label$3 (result i32)
      (call $nop)
      (local.get $temp)
     )
    )
   )

Note how the pop was moved into a proper position. So it seems like we have some logic in the binary reading code that handles this somehow, and so using --roundtrip is a workaround, at least, so this is probably not urgent.

I noticed this in wasm GC that had this:

   (catch $tag$0
    (throw $tag$0
     (ref.cast
      (pop (ref null $type$16))
      (global.get $global$1)
     )
    )
   )

That cast can be statically removed, leading to:

   (catch $tag$0
    (throw $tag$0
     (block (result (ref null $type$16))
      (local.set $4
       (pop (ref null $type$16))
      )
      (drop
       (global.get $global$1)
      )
      (local.get $4)
     )
    )
   )

And that is how the problem can occur.

This does not seem specific to wasm GC, but perhaps we have more peephole opts that end up emitting small blocks, which increase the risk.

@tlively
Copy link
Member

tlively commented Oct 12, 2021

Inserting a new block around a pop will have this problem until we support block param types. Perhaps a short-term fix would be to not add such blocks when there is a danglingPop effect on the body?

@kripken
Copy link
Member Author

kripken commented Oct 12, 2021

I considered that, but it would mean anywhere in the codebase that creates a block would need to check that - that would be invasive, I'm afraid, in many passes.

I hope a "fixup" process that runs during binary emitting might be practical somehow - maybe similar to what happens during --roundtrip (but I am not sure how that works).

@aheejin
Copy link
Member

aheejin commented Oct 13, 2021

Yeah this is tricky... I had to fix many points because of this (e.g. #2885). I checked why --roundtrip fixes this and it looks it's not a feature but a bug. In your code, when reading local.set, it runs popExpression, which does not check if it goes across the current block boundary. So in this case it happens to generate correct code but I don't think this will fix things in general.

I'm not sure if we can come up with a general "fixup" pass.. Maybe we can.. But we have to make sure that those inner blocks generated between catch and pop are not targeted by any branches. My previous approach was just to find out all possibly violating optimizations and fix them, but I agree that it's not very scalable.

@tlively
Copy link
Member

tlively commented Oct 13, 2021

Maybe we should prioritize supporting block parameters to fix this problem "properly," then. I think the best way to do that would be to allow dangling pops at the beginning of arbitrary blocks, not just in catches, along with a new Type params field on Block.

@aheejin
Copy link
Member

aheejin commented Oct 13, 2021

@tlively That would be nice, but that mean EH works only when multivalue is enabled right? I don't think that will be a problem in practice, because all engines that implement EH have implemented multivalue already.. But multivalue support is flaky in many parts still, so it might entail more than we think.

@kripken
Copy link
Member Author

kripken commented Oct 13, 2021

Thinking more on this, I think a general fixup would not be that hard. Basically it could scan catch blocks and see if the pop is nested in control flow, and if so, add a local to pop it immediately instead.

Block parameters would be more efficient, but also more work I think?

@tlively
Copy link
Member

tlively commented Oct 14, 2021

The fixup would also have to introduce a new local to hold the result of the new pop and replace the old pop with a local.get of that local, but yeah, that should work.

@aheejin
Copy link
Member

aheejin commented Oct 14, 2021

@kripken I think that will work. Should that be a new pass, or something like TypeUpdating::handleNonDefaultableLocals that we run at the end of relevant passes?

@kripken
Copy link
Member Author

kripken commented Oct 14, 2021

@aheejin I think the second option sounds better. That way people running a pass don't need to always remember to run a fixup pass after it. If we do that, plus add validation of this, then we should be pretty safe I think.

@aheejin aheejin self-assigned this Nov 8, 2021
aheejin added a commit to aheejin/binaryen that referenced this issue Nov 19, 2021
This adds `EHUtils::handleBlockNestedPops`, which can be called at the
end of passes that has a possibility to put `pop`s inside `block`s. This
method assumes there exists a `pop` in a first-descendant line, even
though it can be nested within a block. This allows a `pop` to be nested
within a `block` or a `try`, but not a `loop`, since that means the
`pop` can run multile times. In case of `if`, `pop` can exist only in
its condition; if a `pop` is in its true or false body, that's not in
the first-descendant line.

To test this, this adds `passes/test_passes.cpp`, which is intended to
contain multiple test passes that test a single (or more) utility
functions separately. Without this kind of pass, it is hard to test
various cases in which nested `pop`s can be generated in existing
passes. This PR also adds `PassRegistry::registerTestPass`, which
registers a pass that's intended only for internal testing and does not
show up in `wasm-opt --help`.

Fixes WebAssembly#4237.
aheejin added a commit that referenced this issue Nov 22, 2021
This adds `EHUtils::handleBlockNestedPops`, which can be called at the
end of passes that has a possibility to put `pop`s inside `block`s. This
method assumes there exists a `pop` in a first-descendant line, even
though it can be nested within a block. This allows a `pop` to be nested
within a `block` or a `try`, but not a `loop`, since that means the
`pop` can run multile times. In case of `if`, `pop` can exist only in
its condition; if a `pop` is in its true or false body, that's not in
the first-descendant line.

This can be useful when optimization passes create blocks to do          
transformations. Wrapping expressions wiith a block does not change      
semantics most of the time, but if pops happen to be inside a block      
generated by those passes, they can result in invalid binaries.

To test this, this adds `passes/test_passes.cpp`, which is intended to
contain multiple test passes that test a single (or more) utility
functions separately. Without this kind of pass, it is hard to test
various cases in which nested `pop`s can be generated in existing
passes. This PR also adds `PassRegistry::registerTestPass`, which
registers a pass that's intended only for internal testing and does not
show up in `wasm-opt --help`.

Fixes #4237.
aheejin added a commit to aheejin/binaryen that referenced this issue Dec 23, 2021
`ref.cast` can be statically removed when the ref's type is a subtype of
the intended RTT type and either of `--ignore-implicit-traps` or
`--traps-never-happen` is given: https://github.com/WebAssembly/binaryen/blob/083ab9842ec3d4ca278c95e1a33112ae7cd4d9e5/src/passes/OptimizeInstructions.cpp#L1603-L1624

Some more context: WebAssembly#4097 (comment)

But this can create a block in which a `pop` is nested, which makes the
`catch` invalid. The test in this PR is the same as the example given by
@kripken in WebAssembly#4237. This calls the fixup function
`EHUtils::handleBlockNestedPops` at the end of the pass to fix this.

Also, because this pass creates a lot of blocks in other patterns, I
think it is possible there can be other patterns to cause this kind of
`pop` nesting.
aheejin added a commit to aheejin/binaryen that referenced this issue Dec 24, 2021
`ref.cast` can be statically removed when the ref's type is a subtype of
the intended RTT type and either of `--ignore-implicit-traps` or
`--traps-never-happen` is given: https://github.com/WebAssembly/binaryen/blob/083ab9842ec3d4ca278c95e1a33112ae7cd4d9e5/src/passes/OptimizeInstructions.cpp#L1603-L1624

Some more context: WebAssembly#4097 (comment)

But this can create a block in which a `pop` is nested, which makes the
`catch` invalid. The test in this PR is the same as the example given by
@kripken in WebAssembly#4237. This calls the fixup function
`EHUtils::handleBlockNestedPops` at the end of the pass to fix this.

Also, because this pass creates a lot of blocks in other patterns, I
think it is possible there can be other patterns to cause this kind of
`pop` nesting.
aheejin added a commit that referenced this issue Dec 29, 2021
`ref.cast` can be statically removed when the ref's type is a subtype of
the intended RTT type and either of `--ignore-implicit-traps` or
`--traps-never-happen` is given: https://github.com/WebAssembly/binaryen/blob/083ab9842ec3d4ca278c95e1a33112ae7cd4d9e5/src/passes/OptimizeInstructions.cpp#L1603-L1624

Some more context: #4097 (comment)

But this can create a block in which a `pop` is nested, which makes the
`catch` invalid. The test in this PR is the same as the example given by
@kripken in #4237. This calls the fixup function
`EHUtils::handleBlockNestedPops` at the end of the pass to fix this.

Also, because this pass creates a lot of blocks in other patterns, I
think it is possible there can be other patterns to cause this kind of
`pop` nesting.
aheejin added a commit to aheejin/binaryen that referenced this issue Dec 29, 2021
`ref.cast` can be statically removed when the ref's type is a subtype of
the intended RTT type and either of `--ignore-implicit-traps` or
`--traps-never-happen` is given: https://github.com/WebAssembly/binaryen/blob/083ab9842ec3d4ca278c95e1a33112ae7cd4d9e5/src/passes/OptimizeInstructions.cpp#L1603-L1624

Some more context: WebAssembly#4097 (comment)

But this can create a block in which a `pop` is nested, which makes the
`catch` invalid. The test in this PR is the same as the example given by
@kripken in WebAssembly#4237. This calls the fixup function
`EHUtils::handleBlockNestedPops` at the end of the pass to fix this.

Also, because this pass creates a lot of blocks in other patterns, I
think it is possible there can be other patterns to cause this kind of
`pop` nesting.
aheejin added a commit to aheejin/binaryen that referenced this issue Dec 29, 2021
`ref.cast` can be statically removed when the ref's type is a subtype of
the intended RTT type and either of `--ignore-implicit-traps` or
`--traps-never-happen` is given: https://github.com/WebAssembly/binaryen/blob/083ab9842ec3d4ca278c95e1a33112ae7cd4d9e5/src/passes/OptimizeInstructions.cpp#L1603-L1624

Some more context: WebAssembly#4097 (comment)

But this can create a block in which a `pop` is nested, which makes the
`catch` invalid. The test in this PR is the same as the example given by
@kripken in WebAssembly#4237. This calls the fixup function
`EHUtils::handleBlockNestedPops` at the end of the pass to fix this.

Also, because this pass creates a lot of blocks in other patterns, I
think it is possible there can be other patterns to cause this kind of
`pop` nesting.
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 a pull request may close this issue.

3 participants