Skip to content

TypeSSA: Don't refine types that are passed exactly to call.without.effects - #9392

Open
stevenfontanella wants to merge 4 commits into
mainfrom
call-without-effects-typessa
Open

stevenfontanella wants to merge 4 commits into
mainfrom
call-without-effects-typessa

Conversation

@stevenfontanella

@stevenfontanella stevenfontanella commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

Prevent TypeSSA from refining a struct that's passed as an exact ref via call.without.effects. Previously in the lit test exact-target-arg was refined to a new subtype even though cwe-exact demands an exact $exact-target-arg which results in invalid code:
repro in binaryen-explorer

Fixes #9393

@stevenfontanella
stevenfontanella force-pushed the call-without-effects-typessa branch from 732ea73 to d5f9781 Compare November 13, 2026 21:21
@stevenfontanella stevenfontanella changed the title Fix for call.without.effects in TypeSSA TypeSSA: Don't refine types that are passed exactly to call.without.effects Nov 13, 2026
@stevenfontanella
stevenfontanella marked this pull request as ready for review November 13, 2026 21:38
@tlively
tlively self-requested a review November 13, 2026 21:54

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Google Antigravity Code Review

I have reviewed the changes in this Pull Request and left inline feedback.

Comment thread src/ir/intrinsics.cpp Outdated
return call;
}
}
if (auto* call = curr->dynCast<Call>(); isCallWithoutEffects(call)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL | Confidence: 5/5 | BUG]

In isCallWithoutEffects(Expression* curr), the C++17 selection statement with initializer if (auto* call = curr->dynCast<Call>(); isCallWithoutEffects(call)) does not check the truthiness of call before evaluating isCallWithoutEffects(call). If curr is not a Call (or is nullptr), curr->dynCast<Call>() evaluates to nullptr. isCallWithoutEffects(call) is then called with call == nullptr, which unconditionally accesses call->target on line 40, causing an immediate null pointer dereference (SIGSEGV).

Additionally, isCallWithoutEffects(Call* call) should guard against nullptr defensively.

Proposed fix:

bool Intrinsics::isCallWithoutEffects(Call* call) {
  if (!call) {
    return false;
  }
  if (auto* func = module.getFunctionOrNull(call->target)) {
    return isCallWithoutEffects(func);
  }

  return false;
}

Call* Intrinsics::isCallWithoutEffects(Expression* curr) {
  if (auto* call = curr->dynCast<Call>(); call && isCallWithoutEffects(call)) {
    return call;
  }

  return nullptr;
}

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.

Good point, done. I disagree with checking for null in the callee since most of our pointers are assumed to be non-null. It'd be nice to add nullability annotations to the codebase but we haven't done that yet.

Comment thread src/ir/intrinsics.cpp Outdated
Comment on lines +48 to +49
if (auto* call = curr->dynCast<Call>(); call && isCallWithoutEffects(call)) {
return call;

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.

The report that this is not covered by tests makes me wonder if we need it at all. Is there something fishy going on there?

Even if that's a false positive somehow, I wonder if we need to have versions that take both Expression* and Call*.

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.

Good point, that's why nothing segfaulted before my fix: c6fef88. In practice all of the callers are Call* so the new overload replaces the Expression* overload. Removed it now.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Google Antigravity Code Review

I have reviewed the changes in this Pull Request and left inline feedback.

Comment thread src/passes/TypeSSA.cpp
Super::visitCall(curr);

if (Intrinsics(wasm).isCallWithoutEffects(curr)) {
if (!curr->operands.back()->type.isSignature()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH | Confidence: 5/5 | BUG]

Calling curr->operands.back() without first checking curr->operands.empty() is undefined behavior. In unvalidated or malformed IR (such as during fuzzing with wasm-opt), a call to call.without.effects may have no operands, which will lead to an out-of-bounds read or crash on std::vector::back().

Consider verifying that operands is not empty first:

Suggested change
if (!curr->operands.back()->type.isSignature()) {
if (curr->operands.empty() || !curr->operands.back()->type.isSignature()) {
return;
}

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.

Validation ensures this never happens

Comment thread src/passes/TypeSSA.cpp

const auto& params =
curr->operands.back()->type.getHeapType().getSignature().params;
assert(params.size() == curr->operands.size() - 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MEDIUM | Confidence: 5/5 | BUG]

Relying on assert(params.size() == curr->operands.size() - 1) can cause out-of-bounds indexing into curr->operands in release builds (-DNDEBUG), where assertions are omitted. If a module has mismatched parameter and operand counts, the loop for (size_t i = 0; i < params.size(); ++i) can execute with i >= curr->operands.size(), leading to out-of-bounds vector access in &curr->operands[i].

Consider handling arity mismatches with an early return:

Suggested change
assert(params.size() == curr->operands.size() - 1);
if (params.size() != curr->operands.size() - 1) {
return;
}

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.

Validation ensures this never happens

This branch has not been deployed

No deployments
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.

TypeSSA incorrectly refines structs that are passed exactly via call.without.effects

2 participants