In PR #66, we tried to address a memory leak by avoiding runtime.SetFinalizer for autodrainingInboundQueue and autodrainingOutboundQueue unless there was something to do. However, when there is work to be done, these finalizers still leak memory because they’re still holding on to a cyclical reference to q. This applies to any platform that relies on a bounded device.WaitPool, like Android and iOS which both declare PreallocatedBuffersPerPool. This patch converts this logic to runtime.AddCleanup which is designed to avoid this problem. Updates tailscale/corp#42776 Signed-off-by: Simon Law <sfllaw@tailscale.com>
149 lines
4 KiB
Go
149 lines
4 KiB
Go
/* SPDX-License-Identifier: MIT
|
|
*
|
|
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
|
|
*/
|
|
|
|
package device
|
|
|
|
import (
|
|
"runtime"
|
|
"sync"
|
|
)
|
|
|
|
// An outboundQueue is a channel of QueueOutboundElements awaiting encryption.
|
|
// An outboundQueue is ref-counted using its wg field.
|
|
// An outboundQueue created with newOutboundQueue has one reference.
|
|
// Every additional writer must call wg.Add(1).
|
|
// Every completed writer must call wg.Done().
|
|
// When no further writers will be added,
|
|
// call wg.Done to remove the initial reference.
|
|
// When the refcount hits 0, the queue's channel is closed.
|
|
type outboundQueue struct {
|
|
c chan *QueueOutboundElementsContainer
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func newOutboundQueue() *outboundQueue {
|
|
q := &outboundQueue{
|
|
c: make(chan *QueueOutboundElementsContainer, QueueOutboundSize),
|
|
}
|
|
q.wg.Add(1)
|
|
go func() {
|
|
q.wg.Wait()
|
|
close(q.c)
|
|
}()
|
|
return q
|
|
}
|
|
|
|
// A inboundQueue is similar to an outboundQueue; see those docs.
|
|
type inboundQueue struct {
|
|
c chan *QueueInboundElementsContainer
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func newInboundQueue() *inboundQueue {
|
|
q := &inboundQueue{
|
|
c: make(chan *QueueInboundElementsContainer, QueueInboundSize),
|
|
}
|
|
q.wg.Add(1)
|
|
go func() {
|
|
q.wg.Wait()
|
|
close(q.c)
|
|
}()
|
|
return q
|
|
}
|
|
|
|
// A handshakeQueue is similar to an outboundQueue; see those docs.
|
|
type handshakeQueue struct {
|
|
c chan QueueHandshakeElement
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func newHandshakeQueue() *handshakeQueue {
|
|
q := &handshakeQueue{
|
|
c: make(chan QueueHandshakeElement, QueueHandshakeSize),
|
|
}
|
|
q.wg.Add(1)
|
|
go func() {
|
|
q.wg.Wait()
|
|
close(q.c)
|
|
}()
|
|
return q
|
|
}
|
|
|
|
type autodrainingInboundQueue struct {
|
|
c chan *QueueInboundElementsContainer
|
|
}
|
|
|
|
// newAutodrainingInboundQueue returns a channel that will be drained when it gets GC'd.
|
|
// It is useful in cases in which is it hard to manage the lifetime of the channel.
|
|
// The returned channel must not be closed. Senders should signal shutdown using
|
|
// some other means, such as sending a sentinel nil values.
|
|
func newAutodrainingInboundQueue(device *Device) *autodrainingInboundQueue {
|
|
q := &autodrainingInboundQueue{
|
|
c: make(chan *QueueInboundElementsContainer, QueueInboundSize),
|
|
}
|
|
if device.needsInboundQueueFinalizer() {
|
|
runtime.AddCleanup(q, device.flushInboundQueue, q.c)
|
|
}
|
|
return q
|
|
}
|
|
|
|
func (device *Device) needsInboundQueueFinalizer() bool {
|
|
return device.pool.messageBuffers.hasAccounting()
|
|
}
|
|
|
|
func (device *Device) flushInboundQueue(c <-chan *QueueInboundElementsContainer) {
|
|
for {
|
|
select {
|
|
case elemsContainer := <-c:
|
|
elemsContainer.filling.Wait()
|
|
for _, elem := range elemsContainer.elems {
|
|
device.PutMessageBuffer(elem.buffer)
|
|
device.PutInboundElement(elem)
|
|
}
|
|
device.PutInboundElementsContainer(elemsContainer)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type autodrainingOutboundQueue struct {
|
|
c chan *QueueOutboundElementsContainer
|
|
}
|
|
|
|
// newAutodrainingOutboundQueue returns a channel that will be drained when it gets GC'd.
|
|
// It is useful in cases in which is it hard to manage the lifetime of the channel.
|
|
// The returned channel must not be closed. Senders should signal shutdown using
|
|
// some other means, such as sending a sentinel nil values.
|
|
// All sends to the channel must be best-effort, because there may be no receivers.
|
|
func newAutodrainingOutboundQueue(device *Device) *autodrainingOutboundQueue {
|
|
q := &autodrainingOutboundQueue{
|
|
c: make(chan *QueueOutboundElementsContainer, QueueOutboundSize),
|
|
}
|
|
if device.needsOutboundQueueFinalizer() {
|
|
runtime.AddCleanup(q, device.flushOutboundQueue, q.c)
|
|
}
|
|
return q
|
|
}
|
|
|
|
func (device *Device) needsOutboundQueueFinalizer() bool {
|
|
return device.pool.messageBuffers.hasAccounting()
|
|
}
|
|
|
|
func (device *Device) flushOutboundQueue(c <-chan *QueueOutboundElementsContainer) {
|
|
for {
|
|
select {
|
|
case elemsContainer := <-c:
|
|
elemsContainer.filling.Wait()
|
|
for _, elem := range elemsContainer.elems {
|
|
device.PutOutboundBuffer(elem.buffer)
|
|
device.PutOutboundElement(elem)
|
|
}
|
|
device.PutOutboundElementsContainer(elemsContainer)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|