Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/wasm-ir-builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,10 @@ class IRBuilder : public UnifiedExpressionVisitor<IRBuilder, Result<>> {
}
}
bool isNone() { return std::get_if<NoScope>(&scope); }
bool isInputLocal(Index local) const {
return std::find(inputLocals.begin(), inputLocals.end(), local) !=
inputLocals.end();
}
Comment on lines +514 to +517

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

Using std::find in the header file introduces a dependency on <algorithm>. Since inputLocals is typically a very small vector, we can replace it with a simple range-based for loop to avoid this dependency and keep the header self-contained.

    bool isInputLocal(Index local) const {
      for (auto inputLocal : inputLocals) {
        if (inputLocal == local) {
          return true;
        }
      }
      return false;
    }

Function* getFunction() {
if (auto* funcScope = std::get_if<FuncScope>(&scope)) {
return funcScope->func;
Expand Down
24 changes: 13 additions & 11 deletions src/wasm/wasm-ir-builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ MaybeResult<IRBuilder::HoistedVal> IRBuilder::hoistLastValue(bool greedy) {
// No need for a scratch local to hoist an unreachable.
return HoistedVal{Index(hoistIndex), nullptr};
}
if (auto* get = expr->dynCast<LocalGet>();
get && getScope().isInputLocal(get->index)) {
// Input scratch locals are never modified while their initial `local.get`s
// remain on the stack, so we can hoist them directly across subsequent
// none-typed expressions without allocating a new scratch local.

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.

  1. Perhaps it makes sense to document the invariant depended on here in wasm-ir-builder.h?
  2. This invariant seems right to me, but I asked Gemini to verify, and it came up with... this:
(module
  (func (export "test") (result i32)
    i32.const 10
    block (param i32) (result i32)
      i32.const 99
      local.set 0 ;; (or `local.set $scratch` in WAT) — accepted & passes validation!
    end
  )
)
$ bin/wasm-opt a.wat --print
(module
 (type $0 (func (result i32)))
 (export "test" (func $0))
 (func $0 (result i32)
  (local $scratch i32)
  (local $scratch_1 i32)
  (local.set $scratch
   (i32.const 10)
  )
  (block (result i32)
   (local.set $scratch_1
    (local.get $scratch)
   )
   (local.set $scratch
    (i32.const 99)
   )
   (local.get $scratch_1)
  )
 )
)

This should not even validate (seems like a bug) but it manages to write to a scratch local in an unexpected way. Not to the live local, but still, this looks concerning...

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 yeah, we shouldn't allow accessing scratch locals (introduced for any reason) by index like that. But fixing that mostly-unrelated bug will restore this invariant.

  1. Perhaps it makes sense to document the invariant depended on here in wasm-ir-builder.h?

This is all internal implementation details of the control flow input lowering, so I'm not sure why the header would be the right location for a comment.

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.

Fair enough about a comment. Otherwise lgtm, but I think we need to fix that bug before this lands, as the fuzzer should be able to error until then.

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.

I don't think the fuzzer would be affected. It should never be parsing modules that incorrectly access scratch locals via out-of-bounds local indices.

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.

Hmm, I guess it would take a text-level fuzzer, yeah, good point. Needs to be an invalid module.

stack.erase(stack.begin() + valIndex);
stack.push_back(get);
return HoistedVal{Index(stack.size() - 1), nullptr};
}
// Hoist with a scratch local. Normally the scratch local is the same type as
// the hoisted expression, but we may need to adjust it given the enabled
// features. Otherwise, if the expression has a tuple type with a more refined
Expand Down Expand Up @@ -919,24 +928,17 @@ Result<> IRBuilder::pushScope(ScopeCtx&& scope) {
inputs.push_back(getScope().exprStack.back());
getScope().exprStack.pop_back();
}
const auto& parentLocals = getScope().inputLocals;
// The new scope may only receive a suffix of the outer scope's parameters,
// so match inputs starting from the remaining size of the outer stack
// rather than from the beginning.
size_t baseIndex = getScope().exprStack.size();
for (size_t i = 0; i < inputs.size(); ++i) {
// Move an enclosing scope's input expression (a scratch local.get or a
// catch pop) directly into this scope rather than allocating a new
// scratch local.
auto* expr = inputs[i];
size_t parentIndex = baseIndex + i;
bool isInputGet = expr->is<LocalGet>() &&
expr->type == scope.inputType[i] &&
parentIndex < parentLocals.size();
auto* get = expr->dynCast<LocalGet>();
bool isInputGet = get && get->type == scope.inputType[i] &&
getScope().isInputLocal(get->index);
bool isPop = isUnnamedBlock && inputs.size() == 1 && expr->is<Pop>();
if (isInputGet) {
assert(expr->cast<LocalGet>()->index == parentLocals[parentIndex]);
scope.inputLocals.push_back(parentLocals[parentIndex]);
scope.inputLocals.push_back(get->index);
} else if (!isPop) {
auto scratch = addScratchLocal(scope.inputType[i]);
CHECK_ERR(scratch);
Comment on lines +936 to 944

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

When hasInput is false, scope.inputType is Type::none (which has a size of 0). If inputs is populated (e.g., in the isUnnamedBlock and Pop case), accessing scope.inputType[i] results in an out-of-bounds indexing on Type. Although this is currently short-circuited because get is nullptr for Pop, it is extremely fragile. We should safely guard the indexing by checking the bounds of scope.inputType.

      auto* get = expr->dynCast<LocalGet>();
      Type type = i < scope.inputType.size() ? scope.inputType[i] : Type::none;
      bool isInputGet = get && get->type == type &&
                        getScope().isInputLocal(get->index);
      bool isPop = isUnnamedBlock && inputs.size() == 1 && expr->is<Pop>();
      if (isInputGet) {
        scope.inputLocals.push_back(get->index);
      } else if (!isPop) {
        auto scratch = addScratchLocal(type);
        CHECK_ERR(scratch);

Expand Down
Loading
Loading