Skip to content

Handle sharedness in Heap2Local's Array2Struct#8515

Open
tlively wants to merge 3 commits intomainfrom
heap2local-shared-array
Open

Handle sharedness in Heap2Local's Array2Struct#8515
tlively wants to merge 3 commits intomainfrom
heap2local-shared-array

Conversation

@tlively
Copy link
Member

@tlively tlively commented Mar 24, 2026

Heap2Local uses an internal utility called Array2Struct to turn non-escaping array allocations into struct allocations so they can subsequently be optimized by the Heap2Local struct optimizations. It
previously used a non-shared struct type, even for shared array types, but this could cause problems when the non-shared struct type became a non-shared null in places that required a shared type to
validate. This could specifically occur when the allocation flowed into a StructCmpxchg expected field that needed to be a subtype of shared eqref.

As a drive-by to make the regression test parse correctly, fix child-typer for StructCmpxchg to correctly handle sharedness in the expected field as well.

Heap2Local uses an internal utility called Array2Struct to turn non-escaping array allocations into struct allocations so they can subsequently be optimized by the Heap2Local struct optimizations. It
previously used a non-shared struct type, even for shared array types, but this could cause problems when the non-shared struct type became a non-shared null in places that required a shared type to
validate. This could specifically occur when the allocation flowed into a StructCmpxchg `expected` field that needed to be a subtype of shared eqref.

As a drive-by to make the regression test parse correctly, fix child-typer for StructCmpxchg to correctly handle sharedness in the expected field as well.
@tlively tlively requested a review from a team as a code owner March 24, 2026 05:28
@tlively tlively requested review from aheejin and removed request for a team March 24, 2026 05:28
Copy link
Member

@aheejin aheejin left a comment

Choose a reason for hiding this comment

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

I'm not familiar with this pass (#3866 said I reviewed it, which I don't have any memory of... 🫠)

This could specifically occur when the allocation flowed into a StructCmpxchg expected field that needed to be a subtype of shared eqref.

Why is the expected operand in StructCmpxchg special?

As a drive-by to make the regression test parse correctly, fix child-typer for StructCmpxchg to correctly handle sharedness in the expected field as well.

How is this fix related to the Heap2Local fix? What happens if this child-typer is not fixed here?

auto expectedType = type;
if (expectedType.isRef()) {
expectedType =
Type(HeapTypes::eq.getBasic(type.getHeapType().getShared()), Nullable);
Copy link
Member

@aheejin aheejin Mar 24, 2026

Choose a reason for hiding this comment

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

This is preexisting, but why does this have to be Nullable?

Copy link
Member Author

Choose a reason for hiding this comment

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

When the type of the accessed struct field is a reference, the expected operand is allowed to be any subtype of (ref null eq) (i.e. eqref), or (ref null (shared eq)) if the field type is a shared reference. In either case, the expected operand can be nullable.

;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg $struct 0
;; CHECK-NEXT: (local.get $struct)
;; CHECK-NEXT: (block (result (ref null (shared none)))
;; CHECK-NEXT: (ref.null (shared none))
Copy link
Member

Choose a reason for hiding this comment

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

I get that Heap2Local can extract fields like i32s out of a struct if that struct doesn't escape, but that would just extract the fields into locals and wouldn't create a random i32 values (like 0) to set them, right? Why can we replace (array.new_fixed $array 0) with (ref.null (shared none)) here? Where did this value none come from?

Copy link
Member Author

Choose a reason for hiding this comment

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

The way Heap2Local works in general is that it places struct fields in locals and also replaces the struct itself with a null value. This generally makes it easier to update the IR than it would be if we removed the value entirely. For example, in this test it would not be valid to leave the expected operand with type none, so we would have had to fix up the struct.atomic.rmw.cmpxchg to maintain validity even though it is unreachable. But making the expected operand null is still valid, so we can less work to fix up the IR.

@tlively
Copy link
Member Author

tlively commented Mar 24, 2026

This could specifically occur when the allocation flowed into a StructCmpxchg expected field that needed to be a subtype of shared eqref.

Why is the expected operand in StructCmpxchg special?

StructCmpxchg is special because it can take 3 reference operands (ref, expected, and replacement), which is more than most other instructions. That allows for more complex combinations of operands being optimized or not or being unreachable or not than are possible for other types. This particular bug depends on ref having a struct type but not being optimizable, expected having an array type and being optimizable, and replacement being unreachable so that the cmpxchg parent is unreachable and therefore not optimized by this pass.

As a drive-by to make the regression test parse correctly, fix child-typer for StructCmpxchg to correctly handle sharedness in the expected field as well.

How is this fix related to the Heap2Local fix? What happens if this child-typer is not fixed here?

The original fuzzer test case depending on running --gufa before --heap2local to set up the IR that triggers the bug. But for checked-in regression test, we want to be able to parse the Wasm text directly into the IR that triggers the bug. Without the fix to child-typer, IRBuilder sees the unreachable replacement and thinks that preceding expected value has an invalid type, so it parses the text into this IR instead:

(drop
 (local.get $shared-struct)
)
(drop
 (struct.new_default $shared-struct)
)
(struct.atomic.rmw.cmpxchg <unprintable type> 0
 (unreachable)
 (unreachable)
 (unreachable)
)

But this IR does not trigger the bug.

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.

2 participants