diff --git a/core/types/bal/access_list.go b/core/types/bal/access_list.go index e563fa22e2..88542aa25f 100644 --- a/core/types/bal/access_list.go +++ b/core/types/bal/access_list.go @@ -72,7 +72,9 @@ func (s *StateAccessList) Merge(other *StateAccessList) { for addr, otherSlots := range other.list { slots, exists := s.list[addr] if !exists { - s.list[addr] = otherSlots + // Clone the borrowed map so later mutations on either list + // don't leak into the other. + s.list[addr] = maps.Clone(otherSlots) continue } maps.Copy(slots, otherSlots) diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 58ba639ff0..688c9f1750 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -252,3 +252,27 @@ func TestBlockAccessListValidation(t *testing.T) { t.Fatalf("Unexpected validation error: %v", err) } } + +// TestStateAccessListMergeIsolation verifies that StateAccessList.Merge does +// not alias the source's inner storage map into the receiver. Regression test +// for a bug where mutations to a merged list leaked back into the source. +func TestStateAccessListMergeIsolation(t *testing.T) { + addr := common.BytesToAddress([]byte{0x01}) + slotA := common.BytesToHash([]byte{0x0A}) + slotB := common.BytesToHash([]byte{0x0B}) + + src := NewStateAccessList() + src.AddState(addr, slotA) + + dst := NewStateAccessList() + dst.Merge(src) + + // Mutate only the receiver. Before the fix this also mutated src because + // Merge assigned src's inner map by reference when the receiver lacked + // the address. + dst.AddState(addr, slotB) + + if _, leaked := src.list[addr][slotB]; leaked { + t.Fatalf("Merge aliased source map: slotB leaked into src after dst-only mutation") + } +}