Skip to content

Commit 1989d14

Browse files
committed
eth/fetcher: handle and (crude) test block memory DOS
1 parent d36c25b commit 1989d14

File tree

2 files changed

+105
-23
lines changed

2 files changed

+105
-23
lines changed

eth/fetcher/fetcher.go

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const (
2020
fetchTimeout = 5 * time.Second // Maximum alloted time to return an explicitly requested block
2121
maxUncleDist = 7 // Maximum allowed backward distance from the chain head
2222
maxQueueDist = 32 // Maximum allowed distance from the chain head to queue
23-
announceLimit = 256 // Maximum number of unique blocks a peer may have announced
23+
hashLimit = 256 // Maximum number of unique blocks a peer may have announced
24+
blockLimit = 64 // Maximum number of unique blocks a per may have delivered
2425
)
2526

2627
var (
@@ -80,8 +81,9 @@ type Fetcher struct {
8081
fetching map[common.Hash]*announce // Announced blocks, currently fetching
8182

8283
// Block cache
83-
queue *prque.Prque // Queue containing the import operations (block number sorted)
84-
queued map[common.Hash]struct{} // Presence set of already queued blocks (to dedup imports)
84+
queue *prque.Prque // Queue containing the import operations (block number sorted)
85+
queues map[string]int // Per peer block counts to prevent memory exhaustion
86+
queued map[common.Hash]*inject // Set of already queued blocks (to dedup imports)
8587

8688
// Callbacks
8789
getBlock blockRetrievalFn // Retrieves a block from the local chain
@@ -104,7 +106,8 @@ func New(getBlock blockRetrievalFn, validateBlock blockValidatorFn, broadcastBlo
104106
announced: make(map[common.Hash][]*announce),
105107
fetching: make(map[common.Hash]*announce),
106108
queue: prque.New(),
107-
queued: make(map[common.Hash]struct{}),
109+
queues: make(map[string]int),
110+
queued: make(map[common.Hash]*inject),
108111
getBlock: getBlock,
109112
validateBlock: validateBlock,
110113
broadcastBlock: broadcastBlock,
@@ -192,22 +195,24 @@ func (f *Fetcher) loop() {
192195
// Clean up any expired block fetches
193196
for hash, announce := range f.fetching {
194197
if time.Since(announce.time) > fetchTimeout {
195-
f.forgetBlock(hash)
198+
f.forgetHash(hash)
196199
}
197200
}
198201
// Import any queued blocks that could potentially fit
199202
height := f.chainHeight()
200203
for !f.queue.Empty() {
201204
op := f.queue.PopItem().(*inject)
202-
number := op.block.NumberU64()
203205

204206
// If too high up the chain or phase, continue later
207+
number := op.block.NumberU64()
205208
if number > height+1 {
206209
f.queue.Push(op, -float32(op.block.NumberU64()))
207210
break
208211
}
209212
// Otherwise if fresh and still unknown, try and import
210-
if number+maxUncleDist < height || f.getBlock(op.block.Hash()) != nil {
213+
hash := op.block.Hash()
214+
if number+maxUncleDist < height || f.getBlock(hash) != nil {
215+
f.forgetBlock(hash)
211216
continue
212217
}
213218
f.insert(op.origin, op.block)
@@ -221,8 +226,8 @@ func (f *Fetcher) loop() {
221226
case notification := <-f.notify:
222227
// A block was announced, make sure the peer isn't DOSing us
223228
count := f.announces[notification.origin] + 1
224-
if count > announceLimit {
225-
glog.V(logger.Debug).Infof("Peer %s: exceeded outstanding announces (%d)", notification.origin, announceLimit)
229+
if count > hashLimit {
230+
glog.V(logger.Debug).Infof("Peer %s: exceeded outstanding announces (%d)", notification.origin, hashLimit)
226231
break
227232
}
228233
// All is well, schedule the announce if block's not yet downloading
@@ -241,8 +246,8 @@ func (f *Fetcher) loop() {
241246

242247
case hash := <-f.done:
243248
// A pending import finished, remove all traces of the notification
249+
f.forgetHash(hash)
244250
f.forgetBlock(hash)
245-
delete(f.queued, hash)
246251

247252
case <-fetch.C:
248253
// At least one block's timer ran out, check for needing retrieval
@@ -252,7 +257,7 @@ func (f *Fetcher) loop() {
252257
if time.Since(announces[0].time) > arriveTimeout-gatherSlack {
253258
// Pick a random peer to retrieve from, reset all others
254259
announce := announces[rand.Intn(len(announces))]
255-
f.forgetBlock(hash)
260+
f.forgetHash(hash)
256261

257262
// If the block still didn't arrive, queue for fetching
258263
if f.getBlock(hash) == nil {
@@ -296,7 +301,7 @@ func (f *Fetcher) loop() {
296301
if f.getBlock(hash) == nil {
297302
explicit = append(explicit, block)
298303
} else {
299-
f.forgetBlock(hash)
304+
f.forgetHash(hash)
300305
}
301306
} else {
302307
download = append(download, block)
@@ -339,15 +344,26 @@ func (f *Fetcher) reschedule(fetch *time.Timer) {
339344
func (f *Fetcher) enqueue(peer string, block *types.Block) {
340345
hash := block.Hash()
341346

347+
// Ensure the peer isn't DOSing us
348+
count := f.queues[peer] + 1
349+
if count > blockLimit {
350+
glog.V(logger.Debug).Infof("Peer %s: discarded block #%d [%x], exceeded allowance (%d)", peer, block.NumberU64(), hash.Bytes()[:4], blockLimit)
351+
return
352+
}
342353
// Discard any past or too distant blocks
343354
if dist := int64(block.NumberU64()) - int64(f.chainHeight()); dist < -maxUncleDist || dist > maxQueueDist {
344355
glog.V(logger.Debug).Infof("Peer %s: discarded block #%d [%x], distance %d", peer, block.NumberU64(), hash.Bytes()[:4], dist)
345356
return
346357
}
347358
// Schedule the block for future importing
348359
if _, ok := f.queued[hash]; !ok {
349-
f.queued[hash] = struct{}{}
350-
f.queue.Push(&inject{origin: peer, block: block}, -float32(block.NumberU64()))
360+
op := &inject{
361+
origin: peer,
362+
block: block,
363+
}
364+
f.queues[peer] = count
365+
f.queued[hash] = op
366+
f.queue.Push(op, -float32(block.NumberU64()))
351367

352368
if glog.V(logger.Debug) {
353369
glog.Infof("Peer %s: queued block #%d [%x], total %v", peer, block.NumberU64(), hash.Bytes()[:4], f.queue.Size())
@@ -389,8 +405,9 @@ func (f *Fetcher) insert(peer string, block *types.Block) {
389405
}()
390406
}
391407

392-
// forgetBlock removes all traces of a block from the fetcher's internal state.
393-
func (f *Fetcher) forgetBlock(hash common.Hash) {
408+
// forgetHash removes all traces of a block announcement from the fetcher's
409+
// internal state.
410+
func (f *Fetcher) forgetHash(hash common.Hash) {
394411
// Remove all pending announces and decrement DOS counters
395412
for _, announce := range f.announced[hash] {
396413
f.announces[announce.origin]--
@@ -409,3 +426,15 @@ func (f *Fetcher) forgetBlock(hash common.Hash) {
409426
delete(f.fetching, hash)
410427
}
411428
}
429+
430+
// forgetBlock removes all traces of a queued block frmo the fetcher's internal
431+
// state.
432+
func (f *Fetcher) forgetBlock(hash common.Hash) {
433+
if insert := f.queued[hash]; insert != nil {
434+
f.queues[insert.origin]--
435+
if f.queues[insert.origin] == 0 {
436+
delete(f.queues, insert.origin)
437+
}
438+
delete(f.queued, hash)
439+
}
440+
}

eth/fetcher/fetcher_test.go

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -399,15 +399,15 @@ func TestDistantDiscarding(t *testing.T) {
399399
// Tests that a peer is unable to use unbounded memory with sending infinite
400400
// block announcements to a node, but that even in the face of such an attack,
401401
// the fetcher remains operational.
402-
func TestAnnounceMemoryExhaustionAttack(t *testing.T) {
402+
func TestHashMemoryExhaustionAttack(t *testing.T) {
403403
tester := newTester()
404404

405405
// Create a valid chain and an infinite junk chain
406-
hashes := createHashes(announceLimit+2*maxQueueDist, knownHash)
406+
hashes := createHashes(hashLimit+2*maxQueueDist, knownHash)
407407
blocks := createBlocksFromHashes(hashes)
408408
valid := tester.makeFetcher(blocks)
409409

410-
attack := createHashes(announceLimit+2*maxQueueDist, unknownHash)
410+
attack := createHashes(hashLimit+2*maxQueueDist, unknownHash)
411411
attacker := tester.makeFetcher(nil)
412412

413413
// Feed the tester a huge hashset from the attacker, and a limited from the valid peer
@@ -417,8 +417,8 @@ func TestAnnounceMemoryExhaustionAttack(t *testing.T) {
417417
}
418418
tester.fetcher.Notify("attacker", attack[i], time.Now().Add(arriveTimeout/2), attacker)
419419
}
420-
if len(tester.fetcher.announced) != announceLimit+maxQueueDist {
421-
t.Fatalf("queued announce count mismatch: have %d, want %d", len(tester.fetcher.announced), announceLimit+maxQueueDist)
420+
if len(tester.fetcher.announced) != hashLimit+maxQueueDist {
421+
t.Fatalf("queued announce count mismatch: have %d, want %d", len(tester.fetcher.announced), hashLimit+maxQueueDist)
422422
}
423423
// Wait for synchronisation to complete and check success for the valid peer
424424
time.Sleep(2 * arriveTimeout)
@@ -431,10 +431,63 @@ func TestAnnounceMemoryExhaustionAttack(t *testing.T) {
431431
tester.fetcher.Notify("valid", hashes[i], time.Now().Add(time.Millisecond), valid)
432432
i--
433433
}
434-
time.Sleep(256 * time.Millisecond)
434+
time.Sleep(500 * time.Millisecond)
435435
}
436-
time.Sleep(256 * time.Millisecond)
436+
time.Sleep(500 * time.Millisecond)
437437
if imported := len(tester.blocks); imported != len(hashes) {
438438
t.Fatalf("fully synchronised block mismatch: have %v, want %v", imported, len(hashes))
439439
}
440440
}
441+
442+
// Tests that blocks sent to the fetcher (either through propagation or via hash
443+
// announces and retrievals) don't pile up indefinitely, exhausting available
444+
// system memory.
445+
func TestBlockMemoryExhaustionAttack(t *testing.T) {
446+
tester := newTester()
447+
448+
// Create a valid chain and a batch of dangling (but in range) blocks
449+
hashes := createHashes(blockLimit, knownHash)
450+
blocks := createBlocksFromHashes(hashes)
451+
452+
attack := make(map[common.Hash]*types.Block)
453+
for i := 0; i < 16; i++ {
454+
hashes := createHashes(maxQueueDist-1, unknownHash)
455+
blocks := createBlocksFromHashes(hashes)
456+
for _, hash := range hashes[:maxQueueDist-2] {
457+
attack[hash] = blocks[hash]
458+
}
459+
}
460+
// Try to feed all the attacker blocks make sure only a limited batch is accepted
461+
for _, block := range attack {
462+
tester.fetcher.Enqueue("attacker", block)
463+
}
464+
time.Sleep(100 * time.Millisecond)
465+
if queued := tester.fetcher.queue.Size(); queued != blockLimit {
466+
t.Fatalf("queued block count mismatch: have %d, want %d", queued, blockLimit)
467+
}
468+
// Queue up a batch of valid blocks, and check that a new peer is allowed to do so
469+
for i := 0; i < maxQueueDist-1; i++ {
470+
tester.fetcher.Enqueue("valid", blocks[hashes[len(hashes)-3-i]])
471+
}
472+
time.Sleep(100 * time.Millisecond)
473+
if queued := tester.fetcher.queue.Size(); queued != blockLimit+maxQueueDist-1 {
474+
t.Fatalf("queued block count mismatch: have %d, want %d", queued, blockLimit+maxQueueDist-1)
475+
}
476+
// Insert the missing piece (and sanity check the import)
477+
tester.fetcher.Enqueue("valid", blocks[hashes[len(hashes)-2]])
478+
time.Sleep(500 * time.Millisecond)
479+
if imported := len(tester.blocks); imported != maxQueueDist+1 {
480+
t.Fatalf("synchronised block mismatch: have %v, want %v", imported, maxQueueDist+1)
481+
}
482+
// Insert the remaining blocks in chunks to ensure clean DOS protection
483+
for i := maxQueueDist; i < len(hashes)-1; i++ {
484+
tester.fetcher.Enqueue("valid", blocks[hashes[len(hashes)-2-i]])
485+
if i%maxQueueDist == 0 {
486+
time.Sleep(500 * time.Millisecond)
487+
}
488+
}
489+
time.Sleep(500 * time.Millisecond)
490+
if imported := len(tester.blocks); imported != len(hashes) {
491+
t.Fatalf("synchronised block mismatch: have %v, want %v", imported, len(hashes))
492+
}
493+
}

0 commit comments

Comments
 (0)