Skip to content

Commit a321015

Browse files
authored
Fix TOCTOU race validating attestations (#16105)
A TOCTOU issue was reported by EF security in which two attestations being validated at the same time may result in both of them being forwarded. The spec says that we need to forward only the first one.
1 parent 1536d59 commit a321015

File tree

6 files changed

+95
-16
lines changed

6 files changed

+95
-16
lines changed

beacon-chain/sync/pending_attestations_queue.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ func (s *Service) processVerifiedAttestation(
265265
if key, err := generateUnaggregatedAttCacheKey(broadcastAtt); err != nil {
266266
log.WithError(err).Error("Failed to generate cache key for attestation tracking")
267267
} else {
268-
s.setSeenUnaggregatedAtt(key)
268+
_ = s.setSeenUnaggregatedAtt(key)
269269
}
270270

271271
valCount, err := helpers.ActiveValidatorCount(ctx, preState, slots.ToEpoch(data.Slot))
@@ -320,7 +320,7 @@ func (s *Service) processAggregate(ctx context.Context, aggregate ethpb.SignedAg
320320
return
321321
}
322322

323-
s.setAggregatorIndexEpochSeen(att.GetData().Target.Epoch, aggregate.AggregateAttestationAndProof().GetAggregatorIndex())
323+
_ = s.setAggregatorIndexEpochSeen(att.GetData().Target.Epoch, aggregate.AggregateAttestationAndProof().GetAggregatorIndex())
324324

325325
if err := s.cfg.p2p.Broadcast(ctx, aggregate); err != nil {
326326
log.WithError(err).Debug("Could not broadcast aggregated attestation")

beacon-chain/sync/validate_aggregate_proof.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ func (s *Service) validateAggregateAndProof(ctx context.Context, pid peer.ID, ms
137137
return validationRes, err
138138
}
139139

140-
s.setAggregatorIndexEpochSeen(data.Target.Epoch, m.AggregateAttestationAndProof().GetAggregatorIndex())
140+
if first := s.setAggregatorIndexEpochSeen(data.Target.Epoch, m.AggregateAttestationAndProof().GetAggregatorIndex()); !first {
141+
return pubsub.ValidationIgnore, nil
142+
}
141143

142144
msg.ValidatorData = m
143145

@@ -265,13 +267,19 @@ func (s *Service) hasSeenAggregatorIndexEpoch(epoch primitives.Epoch, aggregator
265267
}
266268

267269
// Set aggregate's aggregator index target epoch as seen.
268-
func (s *Service) setAggregatorIndexEpochSeen(epoch primitives.Epoch, aggregatorIndex primitives.ValidatorIndex) {
270+
// Returns true if this is the first time seeing this aggregator index and epoch.
271+
func (s *Service) setAggregatorIndexEpochSeen(epoch primitives.Epoch, aggregatorIndex primitives.ValidatorIndex) bool {
269272
b := append(bytesutil.Bytes32(uint64(epoch)), bytesutil.Bytes32(uint64(aggregatorIndex))...)
270273

271274
s.seenAggregatedAttestationLock.Lock()
272275
defer s.seenAggregatedAttestationLock.Unlock()
273276

277+
_, seen := s.seenAggregatedAttestationCache.Get(string(b))
278+
if seen {
279+
return false
280+
}
274281
s.seenAggregatedAttestationCache.Add(string(b), true)
282+
return true
275283
}
276284

277285
// This validates the bitfield is correct and aggregator's index in state is within the beacon committee.

beacon-chain/sync/validate_aggregate_proof_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,3 +801,27 @@ func TestValidateAggregateAndProof_RejectWhenAttEpochDoesntEqualTargetEpoch(t *t
801801
assert.NotNil(t, err)
802802
assert.Equal(t, pubsub.ValidationReject, res)
803803
}
804+
805+
func Test_SetAggregatorIndexEpochSeen(t *testing.T) {
806+
db := dbtest.SetupDB(t)
807+
p := p2ptest.NewTestP2P(t)
808+
809+
r := &Service{
810+
cfg: &config{
811+
p2p: p,
812+
beaconDB: db,
813+
},
814+
seenAggregatedAttestationCache: lruwrpr.New(10),
815+
}
816+
817+
aggIndex := primitives.ValidatorIndex(42)
818+
epoch := primitives.Epoch(7)
819+
820+
require.Equal(t, false, r.hasSeenAggregatorIndexEpoch(epoch, aggIndex))
821+
first := r.setAggregatorIndexEpochSeen(epoch, aggIndex)
822+
require.Equal(t, true, first)
823+
require.Equal(t, true, r.hasSeenAggregatorIndexEpoch(epoch, aggIndex))
824+
825+
second := r.setAggregatorIndexEpochSeen(epoch, aggIndex)
826+
require.Equal(t, false, second)
827+
}

beacon-chain/sync/validate_beacon_attestation.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ func (s *Service) validateCommitteeIndexBeaconAttestation(
104104
}
105105

106106
if !s.slasherEnabled {
107-
// Verify this the first attestation received for the participating validator for the slot.
107+
// Verify this the first attestation received for the participating validator for the slot. This verification is here to return early if we've already seen this attestation.
108+
// This verification is carried again later after all other validations to avoid TOCTOU issues.
108109
if s.hasSeenUnaggregatedAtt(attKey) {
109110
return pubsub.ValidationIgnore, nil
110111
}
@@ -228,7 +229,10 @@ func (s *Service) validateCommitteeIndexBeaconAttestation(
228229
Data: eventData,
229230
})
230231

231-
s.setSeenUnaggregatedAtt(attKey)
232+
if first := s.setSeenUnaggregatedAtt(attKey); !first {
233+
// Another concurrent validation processed the same attestation meanwhile
234+
return pubsub.ValidationIgnore, nil
235+
}
232236

233237
// Attach final validated attestation to the message for further pipeline use
234238
msg.ValidatorData = attForValidation
@@ -385,11 +389,16 @@ func (s *Service) hasSeenUnaggregatedAtt(key string) bool {
385389
}
386390

387391
// Set an incoming attestation as seen for the participating validator for the slot.
388-
func (s *Service) setSeenUnaggregatedAtt(key string) {
392+
// Returns false if the attestation was already seen.
393+
func (s *Service) setSeenUnaggregatedAtt(key string) bool {
389394
s.seenUnAggregatedAttestationLock.Lock()
390395
defer s.seenUnAggregatedAttestationLock.Unlock()
391-
396+
_, seen := s.seenUnAggregatedAttestationCache.Get(key)
397+
if seen {
398+
return false
399+
}
392400
s.seenUnAggregatedAttestationCache.Add(key, true)
401+
return true
393402
}
394403

395404
// hasBlockAndState returns true if the beacon node knows about a block and associated state in the

beacon-chain/sync/validate_beacon_attestation_test.go

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -499,33 +499,50 @@ func TestService_setSeenUnaggregatedAtt(t *testing.T) {
499499
Data: &ethpb.AttestationData{Slot: 2, CommitteeIndex: 0},
500500
AggregationBits: bitfield.Bitlist{0b1001},
501501
}
502+
s3c0a0 := &ethpb.Attestation{
503+
Data: &ethpb.AttestationData{Slot: 3, CommitteeIndex: 0},
504+
AggregationBits: bitfield.Bitlist{0b1001},
505+
}
502506

503507
t.Run("empty cache", func(t *testing.T) {
504508
key := generateKey(t, s0c0a0)
505509
assert.Equal(t, false, s.hasSeenUnaggregatedAtt(key))
506510
})
507511
t.Run("ok", func(t *testing.T) {
508512
key := generateKey(t, s0c0a0)
509-
s.setSeenUnaggregatedAtt(key)
513+
first := s.setSeenUnaggregatedAtt(key)
514+
assert.Equal(t, true, s.hasSeenUnaggregatedAtt(key))
515+
assert.Equal(t, true, first)
516+
})
517+
t.Run("already seen", func(t *testing.T) {
518+
key := generateKey(t, s3c0a0)
519+
first := s.setSeenUnaggregatedAtt(key)
520+
assert.Equal(t, true, s.hasSeenUnaggregatedAtt(key))
521+
assert.Equal(t, true, first)
522+
first = s.setSeenUnaggregatedAtt(key)
510523
assert.Equal(t, true, s.hasSeenUnaggregatedAtt(key))
524+
assert.Equal(t, false, first)
511525
})
512526
t.Run("different slot", func(t *testing.T) {
513527
key1 := generateKey(t, s1c0a0)
514528
key2 := generateKey(t, s2c0a0)
515-
s.setSeenUnaggregatedAtt(key1)
529+
first := s.setSeenUnaggregatedAtt(key1)
516530
assert.Equal(t, false, s.hasSeenUnaggregatedAtt(key2))
531+
assert.Equal(t, true, first)
517532
})
518533
t.Run("different committee index", func(t *testing.T) {
519534
key1 := generateKey(t, s0c1a0)
520535
key2 := generateKey(t, s0c2a0)
521-
s.setSeenUnaggregatedAtt(key1)
536+
first := s.setSeenUnaggregatedAtt(key1)
522537
assert.Equal(t, false, s.hasSeenUnaggregatedAtt(key2))
538+
assert.Equal(t, true, first)
523539
})
524540
t.Run("different bit", func(t *testing.T) {
525541
key1 := generateKey(t, s0c0a1)
526542
key2 := generateKey(t, s0c0a2)
527-
s.setSeenUnaggregatedAtt(key1)
543+
first := s.setSeenUnaggregatedAtt(key1)
528544
assert.Equal(t, false, s.hasSeenUnaggregatedAtt(key2))
545+
assert.Equal(t, true, first)
529546
})
530547
t.Run("0 bits set is considered not seen", func(t *testing.T) {
531548
a := &ethpb.Attestation{AggregationBits: bitfield.Bitlist{0b1000}}
@@ -576,33 +593,51 @@ func TestService_setSeenUnaggregatedAtt(t *testing.T) {
576593
CommitteeId: 0,
577594
AttesterIndex: 0,
578595
}
596+
s3c0a0 := &ethpb.SingleAttestation{
597+
Data: &ethpb.AttestationData{Slot: 2},
598+
CommitteeId: 0,
599+
AttesterIndex: 0,
600+
}
579601

580602
t.Run("empty cache", func(t *testing.T) {
581603
key := generateKey(t, s0c0a0)
582604
assert.Equal(t, false, s.hasSeenUnaggregatedAtt(key))
583605
})
584606
t.Run("ok", func(t *testing.T) {
585607
key := generateKey(t, s0c0a0)
586-
s.setSeenUnaggregatedAtt(key)
608+
first := s.setSeenUnaggregatedAtt(key)
587609
assert.Equal(t, true, s.hasSeenUnaggregatedAtt(key))
610+
assert.Equal(t, true, first)
588611
})
589612
t.Run("different slot", func(t *testing.T) {
590613
key1 := generateKey(t, s1c0a0)
591614
key2 := generateKey(t, s2c0a0)
592-
s.setSeenUnaggregatedAtt(key1)
615+
first := s.setSeenUnaggregatedAtt(key1)
593616
assert.Equal(t, false, s.hasSeenUnaggregatedAtt(key2))
617+
assert.Equal(t, true, first)
618+
})
619+
t.Run("already seen", func(t *testing.T) {
620+
key := generateKey(t, s3c0a0)
621+
first := s.setSeenUnaggregatedAtt(key)
622+
assert.Equal(t, true, s.hasSeenUnaggregatedAtt(key))
623+
assert.Equal(t, true, first)
624+
first = s.setSeenUnaggregatedAtt(key)
625+
assert.Equal(t, true, s.hasSeenUnaggregatedAtt(key))
626+
assert.Equal(t, false, first)
594627
})
595628
t.Run("different committee index", func(t *testing.T) {
596629
key1 := generateKey(t, s0c1a0)
597630
key2 := generateKey(t, s0c2a0)
598-
s.setSeenUnaggregatedAtt(key1)
631+
first := s.setSeenUnaggregatedAtt(key1)
599632
assert.Equal(t, false, s.hasSeenUnaggregatedAtt(key2))
633+
assert.Equal(t, true, first)
600634
})
601635
t.Run("different attester", func(t *testing.T) {
602636
key1 := generateKey(t, s0c0a1)
603637
key2 := generateKey(t, s0c0a2)
604-
s.setSeenUnaggregatedAtt(key1)
638+
first := s.setSeenUnaggregatedAtt(key1)
605639
assert.Equal(t, false, s.hasSeenUnaggregatedAtt(key2))
640+
assert.Equal(t, true, first)
606641
})
607642
t.Run("single attestation is considered not seen", func(t *testing.T) {
608643
a := &ethpb.AttestationElectra{}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Fixed
2+
3+
- Fixed possible race when validating two attestations at the same time.

0 commit comments

Comments
 (0)