From 88866b2a4c37940c549f0084265b708d0de3729d Mon Sep 17 00:00:00 2001 From: rayoo Date: Sat, 25 Apr 2026 15:58:44 +0800 Subject: [PATCH] core/types/bal: deep-copy borrowed storage map in Merge When Merge encountered an address that was absent from the receiver it assigned the source's inner StorageAccessList map by reference, so both lists ended up sharing the same map. Any subsequent AddState on either side would leak into the other. Clone the borrowed map on adoption to keep the two lists independent, mirroring the deep-copy discipline used by accessList, accessEvents and the newly added StateAccessList.Copy. The method currently has no production caller but is part of the public surface being prepared for BAL construction, so trap the bug before it wires up. Add a small regression test that fails on the previous code. --- core/types/bal/access_list.go | 4 +++- core/types/bal/bal_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) 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") + } +}