snapshot: sagernet/gvisor v0.0.0-20260727.0-sing-box-mod.1 + SPEC 048 guard

Обновление снапшота с v0.0.0-20250811.0 на пин, которого требует
sing-box после мержа 235 коммитов (upstream d620bbbf2 "Update gvisor to
20260727.0"). Прежний снапшот был взят 2026-08-04 ровно с той версии,
на которой тогда стоял апстрим; разрыв возник 2026-08-05 вместе с его
бампом.

За год апстрим-gvisor изменил ~14 000 строк в 292 файлах. Значимое для
нас — сетевой стек: tcp/connect.go (PMTU-discovery + исправление
начального RTT/RTO: раньше задержка ACK внутри стека завышала стартовый
таймаут на несколько RTT), tcp/snd.go, tcp/rcv.go, stack/conntrack.go,
stack/packet_buffer.go. Всего 30 файлов в TCP и 37 в stack.

Баг SPEC 048 апстрим НЕ исправил — проверено по коду новой версии:
handleConnecting по-прежнему проверяет состояние endpoint'а, но не ep.h,
а performHandshake так же зануляет h и отпускает мьютекс до Close().
Поэтому guard перенесён (12 строк) вместе со своим тестом (45 строк).

Red/green проверен на новой базе: без guard'а тест падает с той же
nil-паникой, что в полевом крашдампе; с ним зелёный.
This commit is contained in:
Leadaxe 2026-08-05 14:53:31 +03:00
parent ffebe42860
commit 117243aa02
293 changed files with 16413 additions and 2842 deletions

View file

@ -23,7 +23,6 @@ import (
"github.com/sagernet/gvisor/pkg/sync"
"github.com/sagernet/gvisor/pkg/tcpip"
"github.com/sagernet/gvisor/pkg/tcpip/header"
"github.com/sagernet/gvisor/pkg/tcpip/ports"
"github.com/sagernet/gvisor/pkg/tcpip/seqnum"
"github.com/sagernet/gvisor/pkg/tcpip/stack"
)
@ -33,7 +32,7 @@ var logDisconnectOnce sync.Once
func logDisconnect() {
logDisconnectOnce.Do(func() {
log.Infof("One or more TCP connections terminated during save")
log.Infof("One or more TCP connections terminated during save restore")
})
}
@ -49,17 +48,24 @@ func (e *Endpoint) beforeSave() {
switch {
case epState == StateInitial || epState == StateBound:
case epState.connected() || epState.handshake():
if !e.route.HasSaveRestoreCapability() {
if !e.route.HasDisconnectOkCapability() {
panic(&tcpip.ErrSaveRejection{
Err: fmt.Errorf("endpoint cannot be saved in connected state: local %s:%d, remote %s:%d", e.TransportEndpointInfo.ID.LocalAddress, e.TransportEndpointInfo.ID.LocalPort, e.TransportEndpointInfo.ID.RemoteAddress, e.TransportEndpointInfo.ID.RemotePort),
})
// Terminate valid connections only for restore.
if !e.stack.GetAllowConnectedOnSave() && !e.route.HasSaveRestoreCapability() {
if e.stack.GetRemoveConf() {
// Terminate the endpoint when resume=false.
e.terminateAtRestore = false
if !e.stack.AllowLiveTCPMigration() {
logDisconnect()
e.resetConnectionLocked(&tcpip.ErrConnectionAborted{})
e.mu.Unlock()
e.Close()
e.mu.Lock()
}
} else {
// This is set only when resume=true, the termination
// of this endpoint will happen during restore of the
// saved snapshot.
e.terminateAtRestore = true
}
logDisconnect()
e.resetConnectionLocked(&tcpip.ErrConnectionAborted{})
e.mu.Unlock()
e.Close()
e.mu.Lock()
}
fallthrough
case epState == StateListen:
@ -133,10 +139,41 @@ func (e *Endpoint) afterLoad(ctx context.Context) {
// Restore the endpoint to InitialState as it will be moved to
// its origEndpointState during Restore.
e.state = atomicbitops.FromUint32(uint32(StateInitial))
if e.stack.IsSaveRestoreEnabled() {
e.stack.RegisterRestoredEndpoint(e)
e.stack.RegisterRestoredEndpoint(e)
}
// Close the endpoint during restore if terminateAtRestore was set for the endpoint.
func (e *Endpoint) closeEndpointAtRestore() {
e.mu.Lock()
defer e.mu.Unlock()
epState := EndpointState(e.origEndpointState)
if !epState.connected() && !epState.handshake() {
log.Debugf("endpoint was marked to terminate at restore in a wrong state, ID: %+v state: %v", e.ID, epState)
return
}
if epState.handshake() {
connectedLoading.Wait()
listenLoading.Wait()
}
// Put the endpoint in the error state and do cleanup. Do not
// attempt to send RST as route will be nil.
e.purgeReadQueue()
if epState.connected() {
e.purgeWriteQueue()
e.purgePendingRcvQueue()
e.cleanupLocked()
}
e.state.Store(uint32(StateError))
e.closeNoShutdownLocked()
tcpip.DeleteDanglingEndpoint(e)
if epState.connected() {
connectedLoading.Done()
} else {
stack.RestoreStackFromContext(ctx).RegisterRestoredEndpoint(e)
connectingLoading.Done()
}
}
@ -151,48 +188,52 @@ func (e *Endpoint) Restore(s *stack.Stack) {
snd.probeTimer.init(s.Clock(), timerHandler(e, e.snd.probeTimerExpired))
snd.corkTimer.init(s.Clock(), timerHandler(e, e.snd.corkTimerExpired))
}
saveRestoreEnabled := e.stack.IsSaveRestoreEnabled()
if !saveRestoreEnabled {
e.stack = s
e.protocol = protocolFromStack(s)
}
e.ops.InitHandler(e, e.stack, GetTCPSendBufferLimits, GetTCPReceiveBufferLimits)
e.segmentQueue.thaw()
e.mu.Lock()
id := e.ID
terminateAtRestore := e.terminateAtRestore
e.mu.Unlock()
bind := func() {
e.mu.Lock()
defer e.mu.Unlock()
if !saveRestoreEnabled {
addr, _, err := e.checkV4MappedLocked(tcpip.FullAddress{Addr: e.BindAddr, Port: e.TransportEndpointInfo.ID.LocalPort}, true /* bind */)
if err != nil {
panic("unable to parse BindAddr: " + err.String())
}
portRes := ports.Reservation{
Networks: e.effectiveNetProtos,
Transport: ProtocolNumber,
Addr: addr.Addr,
Port: addr.Port,
Flags: e.boundPortFlags,
BindToDevice: e.boundBindToDevice,
Dest: e.boundDest,
}
if ok := e.stack.ReserveTuple(portRes); !ok {
panic(fmt.Sprintf("unable to re-reserve tuple (%v, %q, %d, %+v, %d, %v)", e.effectiveNetProtos, addr.Addr, addr.Port, e.boundPortFlags, e.boundBindToDevice, e.boundDest))
}
}
e.isPortReserved = true
// Mark endpoint as bound.
e.setEndpointState(StateBound)
}
if terminateAtRestore && !e.stack.AllowLiveTCPMigration() {
e.closeEndpointAtRestore()
return
}
epState := EndpointState(e.origEndpointState)
switch {
case epState.connected():
if e.stack.AllowLiveTCPMigration() {
// Handle dual stack addresses.
netProto := e.NetProto
switch e.TransportEndpointInfo.ID.LocalAddress.BitLen() {
case header.IPv4AddressSizeBits:
netProto = header.IPv4ProtocolNumber
case header.IPv6AddressSizeBits:
netProto = header.IPv6ProtocolNumber
}
// Get the new local NIC for source IP and do a FindRoute here to
// identify if the network config is same. Then only attempt restore,
// else close the connection on our end.
r, err := e.stack.FindRoute(0, e.TransportEndpointInfo.ID.LocalAddress, e.TransportEndpointInfo.ID.RemoteAddress, netProto, false /* multicastLoop */)
if err != nil {
e.closeEndpointAtRestore()
log.Infof("Cannot find the route %+v", e.TransportEndpointInfo.ID)
return
}
e.boundNICID = r.NICID()
r.Release()
}
bind()
if e.connectingAddress.BitLen() == 0 {
e.connectingAddress = e.TransportEndpointInfo.ID.RemoteAddress
@ -210,10 +251,8 @@ func (e *Endpoint) Restore(s *stack.Stack) {
// Reset the scoreboard to reinitialize the sack information as
// we do not restore SACK information.
e.scoreboard.Reset()
if saveRestoreEnabled {
// Unregister the endpoint before registering again during Connect.
e.stack.UnregisterTransportEndpoint(e.effectiveNetProtos, header.TCPProtocolNumber, e.TransportEndpointInfo.ID, e, e.boundPortFlags, e.boundBindToDevice)
}
// Unregister the endpoint before registering again during Connect.
e.stack.UnregisterTransportEndpoint(e.effectiveNetProtos, header.TCPProtocolNumber, e.TransportEndpointInfo.ID, e, e.boundPortFlags, e.boundBindToDevice)
e.mu.Lock()
err := e.connect(tcpip.FullAddress{NIC: e.boundNICID, Addr: e.connectingAddress, Port: e.TransportEndpointInfo.ID.RemotePort}, false /* handshake */)
if _, ok := err.(*tcpip.ErrConnectStarted); !ok {
@ -224,6 +263,7 @@ func (e *Endpoint) Restore(s *stack.Stack) {
return
}
e.state.Store(e.origEndpointState)
log.Infof("connect success: %+v", e.TransportEndpointInfo.ID)
// For FIN-WAIT-2 and TIME-WAIT we need to start the appropriate timers so
// that the socket is closed correctly.
switch epState {
@ -239,42 +279,24 @@ func (e *Endpoint) Restore(s *stack.Stack) {
e.snd.corkTimer.enable(MinRTO)
}
e.mu.Unlock()
e.requeueOnRestore()
connectedLoading.Done()
case epState == StateListen:
tcpip.AsyncLoading.Add(1)
if !saveRestoreEnabled {
go func() {
connectedLoading.Wait()
bind()
e.acceptMu.Lock()
backlog := e.acceptQueue.capacity
e.acceptMu.Unlock()
if err := e.Listen(backlog); err != nil {
panic("endpoint listening failed: " + err.String())
}
e.LockUser()
if e.shutdownFlags != 0 {
e.shutdownLocked(e.shutdownFlags)
}
e.UnlockUser()
listenLoading.Done()
tcpip.AsyncLoading.Done()
}()
} else {
go func() {
connectedLoading.Wait()
e.LockUser()
// All endpoints will be moved to initial state after
// restore. Set endpoint to its originial listen state.
e.setEndpointState(StateListen)
// Initialize the listening context.
rcvWnd := seqnum.Size(e.receiveBufferAvailable())
e.listenCtx = newListenContext(e.stack, e.protocol, e, rcvWnd, e.ops.GetV6Only(), e.NetProto)
e.UnlockUser()
listenLoading.Done()
tcpip.AsyncLoading.Done()
}()
}
go func() {
connectedLoading.Wait()
e.LockUser()
// All endpoints will be moved to initial state after
// restore. Set endpoint to its originial listen state.
e.setEndpointState(StateListen)
// Initialize the listening context.
rcvWnd := seqnum.Size(e.receiveBufferAvailable())
e.listenCtx = newListenContext(e.stack, e.protocol, e, rcvWnd, e.ops.GetV6Only(), e.NetProto)
e.UnlockUser()
e.requeueOnRestore()
listenLoading.Done()
tcpip.AsyncLoading.Done()
}()
case epState == StateConnecting:
// Initial SYN hasn't been sent yet so initiate a connect.
tcpip.AsyncLoading.Add(1)
@ -319,6 +341,7 @@ func (e *Endpoint) Restore(s *stack.Stack) {
connectingLoading.Done()
tcpip.AsyncLoading.Done()
e.mu.Unlock()
e.requeueOnRestore()
}()
case epState == StateBound:
tcpip.AsyncLoading.Add(1)
@ -345,3 +368,12 @@ func (e *Endpoint) Restore(s *stack.Stack) {
func (e *Endpoint) Resume() {
e.segmentQueue.thaw()
}
// requeueOnRestore re-adds the endpoint to its processor's run-queue if it has
// queued segments. The run-queue is not saved across checkpoint/restore.
func (e *Endpoint) requeueOnRestore() {
if e.segmentQueue.empty() || e.isOwnedByUser() {
return
}
e.protocol.dispatcher.selectProcessor(e.TransportEndpointInfo.ID).queueEndpoint(e)
}