CVE-2023-25568 (Go, published 2023-05-10T14:15Z)
Description: value: Boxo, formerly known as go-libipfs, is a library for building IPFS applications and implementations. In versions 0.4.0 and 0.5.0, if an attacker is able allocate arbitrary many bytes in the Bitswap server, those allocations are lasting even if the connection is closed. This affects users accepting untrusted connections with the Bitswap server and also affects users using the old API stubs at `github.com/ipfs/go-libipfs/bitswap` because users then transitively import `github.com/ipfs/go-libipfs/bitswap/server`. Boxo versions 0.6.0 and 0.4.1 contain a patch for this issue. As a workaround, those who are using the stub object at `github.com/ipfs/go-libipfs/bitswap` not taking advantage of the features provided by the server can refactor their code to use the new split API that will allow them to run in a client only mode: `github.com/ipfs/go-libipfs/bitswap/client`.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-770 \u2014 Allocation of Resources Without Limits or Throttling
The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
Repository: https://github.com/ipfs/boxo
Files changed:
bitswap/internal/defaults/defaults.go
bitswap/options.go
bitswap/server/internal/decision/engine.go
bitswap/server/internal/decision/engine_test.go
bitswap/server/internal/decision/peer_ledger.go
bitswap/server/server.go
examples/go.sum
go.mod
go.sum
Commit message: bitswap/server/internal/decision: rewrite ledger inversion


Vulnerable code:
	// ledgerMap lists block-related Ledgers by their Partner key.
	ledgerMap map[peer.ID]*ledger

		ledgerMap:                       make(map[peer.ID]*ledger),
	partner := e.findOrCreate(p)

	partner.lk.Lock()
	entries := partner.wantList.Entries()
	partner.lk.Unlock()
	return entries
	response := make([]peer.ID, 0, len(e.ledgerMap))

	for _, ledger := range e.ledgerMap {
		response = append(response, ledger.Partner)
	}
	return response
	for _, entry := range wants {
		e.peerLedger.Wants(p, entry.Cid)
	}
	for _, entry := range cancels {
		e.peerLedger.CancelWant(p, entry.Cid)
	}
	e.lock.Unlock()
	// Get the ledger for the peer
	l := e.findOrCreate(p)
	l.lk.Lock()
	defer l.lk.Unlock()

	// If the peer sent a full wantlist, replace the ledger's wantlist
		l.wantList = wl.New()
	var activeEntries []peertask.Task
	// Remove cancelled blocks from the queue
		if l.CancelWant(entry.Cid) {
		// Add each want-have / want-block to the ledger
		l.Wants(c, entry.Priority, entry.WantType)

		e.peerRequestQueue.PushTasks(p, activeEntries...)
	l := e.findOrCreate(from)

	l.lk.Lock()
	defer l.lk.Unlock()
		e.scoreLedger.AddToReceivedBytes(l.Partner, len(blk.RawData()))
	missingWants := make(map[peer.ID][]cid.Cid)
		for _, p := range peers {
			e.lock.RLock()
			ledger, ok := e.ledgerMap[p]
			e.lock.RUnlock()

			if !ok {
				// This can happen if the peer has disconnected while we're processing this list.
				log.Debugw("failed to find peer in ledger", "peer", p)
				missingWants[p] = append(missingWants[p], k)
				continue
			}
			ledger.lk.RLock()
			entry, ok := ledger.WantListContains(k)
			ledger.lk.RUnlock()
			if !ok {
				// This can happen if the peer has canceled their want while we're processing this message.
				log.Debugw("wantlist index doesn't match peer's wantlist", "peer", p)
				missingWants[p] = append(missingWants[p], k)
				continue
			}
			e.peerRequestQueue.PushTasks(p, peertask.Task{
				Topic:    entry.Cid,
	// If we found missing wants (e.g., because the peer disconnected, we have some races here)
	// remove them from the list. Unfortunately, we still have to re-check because the user
	// could have re-connected in the meantime.
	if len(missingWants) > 0 {
		e.lock.Lock()
		for p, wl := range missingWants {
			if ledger, ok := e.ledgerMap[p]; ok {
				ledger.lk.RLock()
				for _, k := range wl {
					if _, has := ledger.WantListContains(k); has {
						continue
					}
					e.peerLedger.CancelWant(p, k)
				}
				ledger.lk.RUnlock()
			} else {
				for _, k := range wl {
					e.peerLedger.CancelWant(p, k)
				}
			}
		}
		e.lock.Unlock()
	}

	l := e.findOrCreate(p)
	l.lk.Lock()
	defer l.lk.Unlock()
		e.scoreLedger.AddToSentBytes(l.Partner, len(block.RawData()))
		l.wantList.RemoveType(block.Cid(), pb.Message_Wantlist_Block)
			l.wantList.RemoveType(bp.Cid, pb.Message_Wantlist_Have)
	_, ok := e.ledgerMap[p]
	if !ok {
		e.ledgerMap[p] = newLedger(p)
	}

	ledger, ok := e.ledgerMap[p]
	if ok {
		ledger.lk.RLock()
		entries := ledger.Entries()
		ledger.lk.RUnlock()

		for _, entry := range entries {
			e.peerLedger.CancelWant(p, entry.Cid)
		}
	}
	delete(e.ledgerMap, p)

// ledger lazily instantiates a ledger
func (e *Engine) findOrCreate(p peer.ID) *ledger {
	// Take a read lock (as it's less expensive) to check if we have a ledger
	// for the peer
	e.lock.RLock()
	l, ok := e.ledgerMap[p]
	e.lock.RUnlock()
	if ok {
		return l
	}

	// There's no ledger, so take a write lock, then check again and create the
	// ledger if necessary
	e.lock.Lock()
	defer e.lock.Unlock()
	l, ok = e.ledgerMap[p]
	if !ok {
		l = newLedger(p)
		e.ledgerMap[p] = l
	}
	return l
}

func TestPeerIsAddedToPeersWhenMessageReceivedOrSent(t *testing.T) {
	sanfrancisco.Engine.MessageSent(seattle.Peer, m)
	if !peerIsPartner(seattle.Peer, sanfrancisco.Engine) {
		t.Fatal("Peer wasn't added as a Partner")
	}

	if entries[0].Priority != 4 || entries[1].Priority != 3 || entries[2].Priority != 2 || entries[3].Priority != 1 {
		t.Fatal("expected wantlist to be sorted")
	}

	cids map[cid.Cid]map[peer.ID]struct{}
	return &peerLedger{cids: make(map[cid.Cid]map[peer.ID]struct{})}
func (l *peerLedger) Wants(p peer.ID, k cid.Cid) {
	m, ok := l.cids[k]
		m = make(map[peer.ID]struct{})
		l.cids[k] = m
	m[p] = struct{}{}
func (l *peerLedger) CancelWant(p peer.ID, k cid.Cid) {
func (l *peerLedger) Peers(k cid.Cid) []peer.ID {
	peers := make([]peer.ID, 0, len(m))
	for p := range m {
github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY=
github.com/ipfs/go-peertaskqueue v0.8.0 h1:JyNO144tfu9bx6Hpo119zvbEL9iQ760FHOiJYsUjqaU=
	github.com/ipfs/go-peertaskqueue v0.8.0
	github.com/ipfs/go-ipfs-pq v0.0.2 // indirect
github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY=
github.com/ipfs/go-peertaskqueue v0.8.0 h1:JyNO144tfu9bx6Hpo119zvbEL9iQ760FHOiJYsUjqaU=
github.com/ipfs/go-peertaskqueue v0.8.0/go.mod h1:cz8hEnnARq4Du5TGqiWKgMr/BOSQ5XOgMOh1K5YYKKM=

Fixed code:

	// Maximum size of the wantlist we are willing to keep in memory.
	MaxQueuedWantlistEntiresPerPeer = 1024
func MaxQueuedWantlistEntriesPerPeer(count uint) Option {
	return Option{server.MaxQueuedWantlistEntriesPerPeer(count)}
}

	"math/bits"

	maxQueuedWantlistEntriesPerPeer uint
// WithMaxQueuedWantlistEntriesPerPeer limits how much individual entries each peer is allowed to send.
// If a peer send us more than this we will truncate newest entries.
// It defaults to DefaultMaxQueuedWantlistEntiresPerPeer.
func WithMaxQueuedWantlistEntriesPerPeer(count uint) Option {
	return func(e *Engine) {
		e.maxQueuedWantlistEntriesPerPeer = count
	}
}

		maxQueuedWantlistEntriesPerPeer: defaults.MaxQueuedWantlistEntiresPerPeer,
	e.lock.RLock()
	defer e.lock.RUnlock()
	return e.peerLedger.WantlistForPeer(p)
	return e.peerLedger.CollectPeerIDs()
		e.peerLedger.ClearPeerWantlist(p)
	s := uint(e.peerLedger.WantlistSizeForPeer(p))
	if wouldBe := s + uint(len(wants)); wouldBe > e.maxQueuedWantlistEntriesPerPeer {
		log.Debugw("wantlist overflow", "local", e.self, "remote", p, "would be", wouldBe)
		// truncate wantlist to avoid overflow
		available, o := bits.Sub(e.maxQueuedWantlistEntriesPerPeer, s, 0)
		if o != 0 {
			available = 0
		}
		wants = wants[:available]
	}
	for _, entry := range wants {
		e.peerLedger.Wants(p, entry.Entry)
	}
		if e.peerLedger.CancelWant(p, entry.Cid) {
	e.lock.Unlock()

	var activeEntries []peertask.Task
		e.peerRequestQueue.PushTasksTruncated(e.maxQueuedWantlistEntriesPerPeer, p, activeEntries...)
		e.scoreLedger.AddToReceivedBytes(from, len(blk.RawData()))
		for _, entry := range peers {
			e.peerRequestQueue.PushTasksTruncated(e.maxQueuedWantlistEntriesPerPeer, entry.Peer, peertask.Task{
				Topic:    k,
	e.lock.Lock()
	defer e.lock.Unlock()
		e.scoreLedger.AddToSentBytes(p, len(block.RawData()))
		e.peerLedger.CancelWantWithType(p, block.Cid(), pb.Message_Wantlist_Block)
			e.peerLedger.CancelWantWithType(p, bp.Cid, pb.Message_Wantlist_Have)
	e.peerRequestQueue.Clear(p)

	e.peerLedger.PeerDisconnected(p)
func TestPeerIsAddedToPeersWhenMessageSent(t *testing.T) {
	// We need to request something for it to add us as partner.
	m.AddEntry(blocks.NewBlock([]byte("Hæ")).Cid(), 0, pb.Message_Wantlist_Block, true)

	wl "github.com/ipfs/go-libipfs/bitswap/client/wantlist"
	pb "github.com/ipfs/go-libipfs/bitswap/message/pb"

	// thoses two maps are inversions of each other
	peers map[peer.ID]map[cid.Cid]entry
	cids  map[cid.Cid]map[peer.ID]entry
	return &peerLedger{
		peers: make(map[peer.ID]map[cid.Cid]entry),
		cids:  make(map[cid.Cid]map[peer.ID]entry),
	}
func (l *peerLedger) Wants(p peer.ID, e wl.Entry) {
	cids, ok := l.peers[p]
	if !ok {
		cids = make(map[cid.Cid]entry)
		l.peers[p] = cids
	}
	cids[e.Cid] = entry{e.Priority, e.WantType}

	m, ok := l.cids[e.Cid]
	if !ok {
		m = make(map[peer.ID]entry)
		l.cids[e.Cid] = m
	}
	m[p] = entry{e.Priority, e.WantType}
}

// CancelWant returns true if the cid was present in the wantlist.
func (l *peerLedger) CancelWant(p peer.ID, k cid.Cid) bool {
	wants, ok := l.peers[p]
	if !ok {
		return false
	}
	delete(wants, k)
	if len(wants) == 0 {
		delete(l.peers, p)
	}

	l.removePeerFromCid(p, k)
	return true
}

// CancelWantWithType will not cancel WantBlock if we sent a HAVE message.
func (l *peerLedger) CancelWantWithType(p peer.ID, k cid.Cid, typ pb.Message_Wantlist_WantType) {
	wants, ok := l.peers[p]
		return
	}
	e, ok := wants[k]
	if !ok {
		return
	if typ == pb.Message_Wantlist_Have && e.WantType == pb.Message_Wantlist_Block {
		return
	}

	delete(wants, k)
	if len(wants) == 0 {
		delete(l.peers, p)
	}

	l.removePeerFromCid(p, k)
func (l *peerLedger) removePeerFromCid(p peer.ID, k cid.Cid) {
type entryForPeer struct {
	Peer peer.ID
	entry
}

type entry struct {
	Priority int32
	WantType pb.Message_Wantlist_WantType
}

func (l *peerLedger) Peers(k cid.Cid) []entryForPeer {
	peers := make([]entryForPeer, 0, len(m))
	for p, e := range m {
		peers = append(peers, entryForPeer{p, e})
	}
	return peers
}

func (l *peerLedger) CollectPeerIDs() []peer.ID {
	peers := make([]peer.ID, 0, len(l.peers))
	for p := range l.peers {

func (l *peerLedger) WantlistSizeForPeer(p peer.ID) int {
	return len(l.peers[p])
}

func (l *peerLedger) WantlistForPeer(p peer.ID) []wl.Entry {
	cids, ok := l.peers[p]
	if !ok {
		return nil
	}

	entries := make([]wl.Entry, 0, len(l.cids))
	for c, e := range cids {
		entries = append(entries, wl.Entry{
			Cid:      c,
			Priority: e.Priority,
			WantType: e.WantType,
		})
	}
	return entries
}

// ClearPeerWantlist does not take an effort to fully erase it from memory.
// This is intended when the peer is still connected and the map capacity could
// be reused. If the memory should be freed use PeerDisconnected instead.
func (l *peerLedger) ClearPeerWantlist(p peer.ID) {
	cids, ok := l.peers[p]
	if !ok {
		return
	}

	for c := range cids {
		l.removePeerFromCid(p, c)
	}
}

func (l *peerLedger) PeerDisconnected(p peer.ID) {
	l.ClearPeerWantlist(p)
	delete(l.peers, p)
}
// MaxQueuedWantlistEntriesPerPeer limits how much individual entries each peer is allowed to send.
// If a peer send us more than this we will truncate newest entries.
// It defaults to defaults.MaxQueuedWantlistEntiresPerPeer.
func MaxQueuedWantlistEntriesPerPeer(count uint) Option {
	o := decision.WithMaxQueuedWantlistEntriesPerPeer(count)
	return func(bs *Server) {
		bs.engineOptions = append(bs.engineOptions, o)
	}
}

github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE=
github.com/ipfs/go-peertaskqueue v0.8.1 h1:YhxAs1+wxb5jk7RvS0LHdyiILpNmRIRnZVztekOF0pg=
	github.com/ipfs/go-peertaskqueue v0.8.1
	github.com/ipfs/go-ipfs-pq v0.0.3 // indirect
github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE=
github.com/ipfs/go-ipfs-pq v0.0.3/go.mod h1:btNw5hsHBpRcSSgZtiNm/SLj5gYIZ18AKtv3kERkRb4=
github.com/ipfs/go-peertaskqueue v0.8.1 h1:YhxAs1+wxb5jk7RvS0LHdyiILpNmRIRnZVztekOF0pg=
github.com/ipfs/go-peertaskqueue v0.8.1/go.mod h1:Oxxd3eaK279FxeydSPPVGHzbwVeHjatZ2GA8XD+KbPU=