derp: clone regions when merging DERP maps

In-place node shuffling aliased regions shared with the served map.
This commit is contained in:
Kristoffer Dalby
2026-06-05 15:19:27 +00:00
committed by Kristoffer Dalby
parent 0921972f96
commit e413919810
2 changed files with 34 additions and 2 deletions
+6 -2
View File
@@ -6,7 +6,6 @@ import (
"encoding/json"
"hash/crc64"
"io"
"maps"
"math/rand"
"net/http"
"net/url"
@@ -85,7 +84,12 @@ func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
}
for _, derpMap := range derpMaps {
maps.Copy(result.Regions, derpMap.Regions)
// Clone each region: copying the pointer would let a later in-place
// shuffle (shuffleRegionNoClone) alias regions shared with the source
// map or a previously served map, racing concurrent readers.
for id, region := range derpMap.Regions {
result.Regions[id] = region.Clone()
}
}
for id, region := range result.Regions {
+28
View File
@@ -0,0 +1,28 @@
package derp
import (
"testing"
"github.com/stretchr/testify/assert"
"tailscale.com/tailcfg"
)
// TestMergeDERPMapsClonesRegions ensures merged DERP maps own their regions
// rather than aliasing the source pointers, so a later in-place node shuffle
// cannot mutate a shared or previously served map.
func TestMergeDERPMapsClonesRegions(t *testing.T) {
src := &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {RegionID: 1, Nodes: []*tailcfg.DERPNode{{Name: "a"}, {Name: "b"}}},
},
}
merged := mergeDERPMaps([]*tailcfg.DERPMap{src})
assert.NotSame(t, src.Regions[1], merged.Regions[1],
"merged region must not alias the source region pointer")
merged.Regions[1].Nodes[0] = &tailcfg.DERPNode{Name: "mutated"}
assert.Equal(t, "a", src.Regions[1].Nodes[0].Name,
"source region was mutated through a shared pointer")
}