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

@ -203,10 +203,6 @@ type Options struct {
// include CapabilitySaveRestore
SaveRestore bool
// DisconnectOk if true, indicates that this NIC capability set should
// include CapabilityDisconnectOk.
DisconnectOk bool
// GSOMaxSize is the maximum GSO packet size. It is zero if GSO is
// disabled.
GSOMaxSize uint32
@ -240,19 +236,21 @@ type Options struct {
// ProcessorsPerChannel is the number of goroutines used to handle packets
// from each FD.
ProcessorsPerChannel int
// IsPacketSocket indicates whether each FD is a packet socket.
// If nil, getsockname will be called.
IsPacketSocket []bool
// PreConfigured indicates that socket setup (getsockname, setsockopt)
// has already been performed on the host.
PreConfigured bool
}
// fanoutID is used for AF_PACKET based endpoints to enable PACKET_FANOUT
// support in the host kernel. This allows us to use multiple FD's to receive
// from the same underlying NIC. The fanoutID needs to be the same for a given
// set of FD's that point to the same NIC. Trying to set the PACKET_FANOUT
// option for an FD with a fanoutID already in use by another FD for a different
// NIC will return an EINVAL.
//
// Since fanoutID must be unique within the network namespace, we start with
// the PID to avoid collisions. The only way to be sure of avoiding collisions
// is to run in a new network namespace.
var fanoutID atomicbitops.Int32 = atomicbitops.FromInt32(int32(unix.Getpid()))
// fallbackFanoutID is used only when PACKET_FANOUT_FLAG_UNIQUEID is not
// supported by the host kernel. It preserves the PID-seeded best-effort behavior:
// seed from unix.Getpid() and increment per endpoint. This is not
// collision-free across sentries that share a network namespace.
var fallbackFanoutID atomicbitops.Int32 = atomicbitops.FromInt32(int32(unix.Getpid()))
// New creates a new fd-based endpoint.
//
@ -279,10 +277,6 @@ func New(opts *Options) (stack.LinkEndpoint, error) {
caps |= stack.CapabilitySaveRestore
}
if opts.DisconnectOk {
caps |= stack.CapabilityDisconnectOk
}
if len(opts.FDs) == 0 {
return nil, fmt.Errorf("opts.FD is empty, at least one FD must be specified")
}
@ -307,17 +301,18 @@ func New(opts *Options) (stack.LinkEndpoint, error) {
}
}
// Increment fanoutID to ensure that we don't re-use the same fanoutID
// for the next endpoint.
fid := fanoutID.Add(1)
// Fanout id allocated by the kernel for this endpoint. All AF_PACKET FDs
// belonging to this endpoint must use the same id. -1 means no AF_PACKET
// FD has allocated an id yet; 0 is a valid fanout id.
fid := int32(-1)
// Create per channel dispatchers.
for _, fd := range opts.FDs {
for i, fd := range opts.FDs {
if err := unix.SetNonblock(fd, true); err != nil {
return nil, fmt.Errorf("unix.SetNonblock(%v) failed: %v", fd, err)
}
isSocket, err := isSocketFD(fd)
isSocket, err := IsSocketFD(fd)
if err != nil {
return nil, err
}
@ -334,7 +329,34 @@ func New(opts *Options) (stack.LinkEndpoint, error) {
opts.ProcessorsPerChannel = max(1, runtime.GOMAXPROCS(0)/len(opts.FDs))
}
inboundDispatcher, err := createInboundDispatcher(e, fd, isSocket, fid, opts)
var isPacket bool
if opts.PreConfigured {
if opts.IsPacketSocket != nil && i < len(opts.IsPacketSocket) {
isPacket = opts.IsPacketSocket[i]
} else {
return nil, fmt.Errorf("PreConfigured is true but IsPacketSocket is missing or too short (index %d, len %d)", i, len(opts.IsPacketSocket))
}
} else {
var err error
isPacket, err = IsPacketSocket(fd, isSocket)
if err != nil {
return nil, err
}
}
if isPacket && !opts.PreConfigured {
var err error
if fid < 0 {
fid, err = CreatePacketFanoutGroup(fd)
} else {
err = JoinPacketFanoutGroup(fd, fid)
}
if err != nil {
return nil, fmt.Errorf("failed to enable PACKET_FANOUT option: %v", err)
}
}
inboundDispatcher, err := createInboundDispatcher(e, fd, isSocket, opts)
if err != nil {
return nil, fmt.Errorf("createInboundDispatcher(...) = %v", err)
}
@ -344,7 +366,7 @@ func New(opts *Options) (stack.LinkEndpoint, error) {
return e, nil
}
func createInboundDispatcher(e *endpoint, fd int, isSocket bool, fID int32, opts *Options) (linkDispatcher, error) {
func createInboundDispatcher(e *endpoint, fd int, isSocket bool, opts *Options) (linkDispatcher, error) {
// By default use the readv() dispatcher as it works with all kinds of
// FDs (tap/tun/unix domain sockets and af_packet).
inboundDispatcher, err := newReadVDispatcher(fd, e, opts)
@ -353,38 +375,6 @@ func createInboundDispatcher(e *endpoint, fd int, isSocket bool, fID int32, opts
}
if isSocket {
sa, err := unix.Getsockname(fd)
if err != nil {
return nil, fmt.Errorf("unix.Getsockname(%d) = %v", fd, err)
}
switch sa.(type) {
case *unix.SockaddrLinklayer:
// Enable PACKET_FANOUT mode if the underlying socket is of type
// AF_PACKET. We do not enable PACKET_FANOUT_FLAG_DEFRAG as that will
// prevent gvisor from receiving fragmented packets and the host does the
// reassembly on our behalf before delivering the fragments. This makes it
// hard to test fragmentation reassembly code in Netstack.
//
// See: include/uapi/linux/if_packet.h (struct fanout_args).
//
// NOTE: We are using SetSockOptInt here even though the underlying
// option is actually a struct. The code follows the example in the
// kernel documentation as described at the link below:
//
// See: https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt
//
// This works out because the actual implementation for the option zero
// initializes the structure and will initialize the max_members field
// to a proper value if zero.
//
// See: https://github.com/torvalds/linux/blob/7acac4b3196caee5e21fb5ea53f8bc124e6a16fc/net/packet/af_packet.c#L3881
const fanoutType = unix.PACKET_FANOUT_HASH
fanoutArg := (int(fID) & 0xffff) | fanoutType<<16
if err := unix.SetsockoptInt(fd, unix.SOL_PACKET, unix.PACKET_FANOUT, fanoutArg); err != nil {
return nil, fmt.Errorf("failed to enable PACKET_FANOUT option: %v", err)
}
}
switch e.packetDispatchMode {
case PacketMMap:
inboundDispatcher, err = newPacketMMapDispatcher(fd, e, opts)
@ -407,7 +397,82 @@ func createInboundDispatcher(e *endpoint, fd int, isSocket bool, fID int32, opts
return inboundDispatcher, nil
}
func isSocketFD(fd int) (bool, error) {
// IsPacketSocket checks if the FD is an AF_PACKET socket.
func IsPacketSocket(fd int, isSocket bool) (bool, error) {
if !isSocket {
return false, nil
}
sa, err := unix.Getsockname(fd)
if err != nil {
return false, fmt.Errorf("unix.Getsockname(%d) = %v", fd, err)
}
_, ok := sa.(*unix.SockaddrLinklayer)
return ok, nil
}
// CreatePacketFanoutGroup enables PACKET_FANOUT for the first AF_PACKET socket
// in an endpoint and returns the fanout id the group joined.
//
// All AF_PACKET FDs that back the same endpoint must join the same fanout
// group so the host kernel consistently hashes packets for a flow to one FD.
// Fanout ids are unique within the Linux network namespace that owns the
// sockets; reusing an id for a different NIC in that namespace fails with
// EINVAL.
//
// We ask the kernel to allocate the id via PACKET_FANOUT_FLAG_UNIQUEID so the
// id is guaranteed unique within the namespace even when multiple sentries
// share it. If that setsockopt fails (e.g. the host kernel predates
// PACKET_FANOUT_FLAG_UNIQUEID), we fall back to the PID-seeded
// fallbackFanoutID, which is best-effort and not collision-free across
// sentries that share a network namespace.
//
// We do not enable PACKET_FANOUT_FLAG_DEFRAG as that will prevent gvisor from
// receiving fragmented packets and the host does the reassembly on our behalf
// before delivering the fragments. This makes it hard to test fragmentation
// reassembly code in Netstack.
//
// See: include/uapi/linux/if_packet.h (struct fanout_args).
//
// NOTE: We are using SetSockOptInt here even though the underlying option is
// actually a struct. The code follows the example in the kernel documentation
// as described at the link below:
//
// See: https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt
//
// This works out because the actual implementation for the option zero
// initializes the structure and will initialize the max_members field to a
// proper value if zero.
//
// See: https://github.com/torvalds/linux/blob/7acac4b3196caee5e21fb5ea53f8bc124e6a16fc/net/packet/af_packet.c#L3881
func CreatePacketFanoutGroup(fd int) (int32, error) {
const fanoutType = unix.PACKET_FANOUT_HASH
fanoutArg := (fanoutType | unix.PACKET_FANOUT_FLAG_UNIQUEID) << 16
if err := unix.SetsockoptInt(fd, unix.SOL_PACKET, unix.PACKET_FANOUT, fanoutArg); err != nil {
uniqueIDErr := err
fallbackID := fallbackFanoutID.Add(1)
fanoutArg = (int(fallbackID) & 0xffff) | fanoutType<<16
if err := unix.SetsockoptInt(fd, unix.SOL_PACKET, unix.PACKET_FANOUT, fanoutArg); err != nil {
return 0, fmt.Errorf("UNIQUEID failed (%v); fallback fanout id %d also failed: %v", uniqueIDErr, fanoutArg&0xffff, err)
}
return int32(fanoutArg & 0xffff), nil
}
fanoutArg, err := unix.GetsockoptInt(fd, unix.SOL_PACKET, unix.PACKET_FANOUT)
if err != nil {
return 0, fmt.Errorf("getsockopt(PACKET_FANOUT) failed: %v", err)
}
return int32(fanoutArg & 0xffff), nil
}
// JoinPacketFanoutGroup joins the FD to the specified fanout group.
func JoinPacketFanoutGroup(fd int, fID int32) error {
const fanoutType = unix.PACKET_FANOUT_HASH
fanoutArg := (int(fID) & 0xffff) | fanoutType<<16
return unix.SetsockoptInt(fd, unix.SOL_PACKET, unix.PACKET_FANOUT, fanoutArg)
}
// IsSocketFD checks if the FD is a socket.
func IsSocketFD(fd int) (bool, error) {
var stat unix.Stat_t
if err := unix.Fstat(fd, &stat); err != nil {
return false, fmt.Errorf("unix.Fstat(%v,...) failed: %v", fd, err)
@ -892,7 +957,7 @@ func (e *InjectableEndpoint) InjectInbound(protocol tcpip.NetworkProtocolNumber,
// NewInjectable creates a new fd-based InjectableEndpoint.
func NewInjectable(fd int, mtu uint32, capabilities stack.LinkEndpointCapabilities) (*InjectableEndpoint, error) {
unix.SetNonblock(fd, true)
isSocket, err := isSocketFD(fd)
isSocket, err := IsSocketFD(fd)
if err != nil {
return nil, err
}

View file

@ -92,5 +92,5 @@ func endpointinitLockNames() {}
func init() {
endpointinitLockNames()
endpointprefixIndex = locking.NewMutexClass(reflect.TypeOf(endpointRWMutex{}), endpointlockNames)
endpointprefixIndex = locking.NewMutexClass(reflect.TypeFor[endpointRWMutex](), endpointlockNames)
}

View file

@ -112,7 +112,6 @@ func (o *Options) StateFields() []string {
"ClosedFunc",
"Address",
"SaveRestore",
"DisconnectOk",
"GSOMaxSize",
"GVisorGSOEnabled",
"PacketDispatchMode",
@ -122,6 +121,8 @@ func (o *Options) StateFields() []string {
"InterfaceIndex",
"GRO",
"ProcessorsPerChannel",
"IsPacketSocket",
"PreConfigured",
}
}
@ -136,16 +137,17 @@ func (o *Options) StateSave(stateSinkObject state.Sink) {
stateSinkObject.Save(3, &o.ClosedFunc)
stateSinkObject.Save(4, &o.Address)
stateSinkObject.Save(5, &o.SaveRestore)
stateSinkObject.Save(6, &o.DisconnectOk)
stateSinkObject.Save(7, &o.GSOMaxSize)
stateSinkObject.Save(8, &o.GVisorGSOEnabled)
stateSinkObject.Save(9, &o.PacketDispatchMode)
stateSinkObject.Save(10, &o.TXChecksumOffload)
stateSinkObject.Save(11, &o.RXChecksumOffload)
stateSinkObject.Save(12, &o.MaxSyscallHeaderBytes)
stateSinkObject.Save(13, &o.InterfaceIndex)
stateSinkObject.Save(14, &o.GRO)
stateSinkObject.Save(15, &o.ProcessorsPerChannel)
stateSinkObject.Save(6, &o.GSOMaxSize)
stateSinkObject.Save(7, &o.GVisorGSOEnabled)
stateSinkObject.Save(8, &o.PacketDispatchMode)
stateSinkObject.Save(9, &o.TXChecksumOffload)
stateSinkObject.Save(10, &o.RXChecksumOffload)
stateSinkObject.Save(11, &o.MaxSyscallHeaderBytes)
stateSinkObject.Save(12, &o.InterfaceIndex)
stateSinkObject.Save(13, &o.GRO)
stateSinkObject.Save(14, &o.ProcessorsPerChannel)
stateSinkObject.Save(15, &o.IsPacketSocket)
stateSinkObject.Save(16, &o.PreConfigured)
}
func (o *Options) afterLoad(context.Context) {}
@ -158,16 +160,17 @@ func (o *Options) StateLoad(ctx context.Context, stateSourceObject state.Source)
stateSourceObject.Load(3, &o.ClosedFunc)
stateSourceObject.Load(4, &o.Address)
stateSourceObject.Load(5, &o.SaveRestore)
stateSourceObject.Load(6, &o.DisconnectOk)
stateSourceObject.Load(7, &o.GSOMaxSize)
stateSourceObject.Load(8, &o.GVisorGSOEnabled)
stateSourceObject.Load(9, &o.PacketDispatchMode)
stateSourceObject.Load(10, &o.TXChecksumOffload)
stateSourceObject.Load(11, &o.RXChecksumOffload)
stateSourceObject.Load(12, &o.MaxSyscallHeaderBytes)
stateSourceObject.Load(13, &o.InterfaceIndex)
stateSourceObject.Load(14, &o.GRO)
stateSourceObject.Load(15, &o.ProcessorsPerChannel)
stateSourceObject.Load(6, &o.GSOMaxSize)
stateSourceObject.Load(7, &o.GVisorGSOEnabled)
stateSourceObject.Load(8, &o.PacketDispatchMode)
stateSourceObject.Load(9, &o.TXChecksumOffload)
stateSourceObject.Load(10, &o.RXChecksumOffload)
stateSourceObject.Load(11, &o.MaxSyscallHeaderBytes)
stateSourceObject.Load(12, &o.InterfaceIndex)
stateSourceObject.Load(13, &o.GRO)
stateSourceObject.Load(14, &o.ProcessorsPerChannel)
stateSourceObject.Load(15, &o.IsPacketSocket)
stateSourceObject.Load(16, &o.PreConfigured)
}
func (e *InjectableEndpoint) StateTypeName() string {

View file

@ -92,5 +92,5 @@ func injectableEndpointinitLockNames() {}
func init() {
injectableEndpointinitLockNames()
injectableEndpointprefixIndex = locking.NewMutexClass(reflect.TypeOf(injectableEndpointRWMutex{}), injectableEndpointlockNames)
injectableEndpointprefixIndex = locking.NewMutexClass(reflect.TypeFor[injectableEndpointRWMutex](), injectableEndpointlockNames)
}

View file

@ -60,5 +60,5 @@ func processorinitLockNames() {}
func init() {
processorinitLockNames()
processorprefixIndex = locking.NewMutexClass(reflect.TypeOf(processorMutex{}), processorlockNames)
processorprefixIndex = locking.NewMutexClass(reflect.TypeFor[processorMutex](), processorlockNames)
}

View file

@ -46,7 +46,6 @@ type processor struct {
func (p *processor) start(wg *sync.WaitGroup) {
defer wg.Done()
defer p.sleeper.Done()
for {
switch w := p.sleeper.Fetch(true); {
case w == &p.packetWaker:
@ -128,9 +127,10 @@ func (m *processorManager) start() {
}
// afterLoad is invoked by stateify.
func (m *processorManager) afterLoad(context.Context) {
m.wg.Add(len(m.processors))
m.start()
func (m *processorManager) afterLoad(ctx context.Context) {
// Close all the old/saved processors. There are new NICs and
// processors created during restore.
m.close()
}
func (m *processorManager) connectionHash(cid *connectionID) uint32 {
@ -215,34 +215,47 @@ func tcpipConnectionID(pkt *stack.PacketBuffer) (connectionID, bool) {
return cid, true
}
ipHdr := header.IPv6(h)
cid.srcAddr = ipHdr.SourceAddressSlice()
cid.dstAddr = ipHdr.DestinationAddressSlice()
cid.proto = header.IPv6ProtocolNumber
var tcpHdr header.TCP
if tcpip.TransportProtocolNumber(ipHdr.NextHeader()) == header.TCPProtocolNumber {
tcpHdr = header.TCP(h[header.IPv6FixedHeaderSize:][:tcpSrcDstPortLen])
if !header.IsExtensionHeader(ipHdr.NextHeader()) {
// Known transport protocols(not just TCP) store the src and dst ports
// in the first 4 bytes after the IPv6 fixed header.
tcpHdr := header.TCP(h[header.IPv6FixedHeaderSize:][:tcpSrcDstPortLen])
cid.srcPort = tcpHdr.SourcePort()
cid.dstPort = tcpHdr.DestinationPort()
} else {
// Slow path for IPv6 extension headers :(.
dataBuf := pkt.Data().ToBuffer()
dataBuf.TrimFront(header.IPv6MinimumSize)
it := header.MakeIPv6PayloadIterator(header.IPv6ExtensionHeaderIdentifier(ipHdr.NextHeader()), dataBuf)
defer it.Release()
// All fragment packets need to be processed by the same goroutine, so
// only record the ports if this is not a fragment packet.
var isFragment bool
for {
hdr, done, err := it.Next()
if done || err != nil {
break
}
if fh, ok := hdr.(header.IPv6FragmentExtHdr); ok && !fh.IsAtomic() {
isFragment = true
}
hdr.Release()
}
h, ok = pkt.Data().PullUp(int(it.HeaderOffset()) + tcpSrcDstPortLen)
if !ok {
return cid, true
if !isFragment {
h, ok = pkt.Data().PullUp(int(it.HeaderOffset()) + tcpSrcDstPortLen)
if !ok {
return cid, true
}
// Known transport protocols store the src and dst ports
// in the first 4 bytes after the IPv6 fixed header.
tcpHdr := header.TCP(h[it.HeaderOffset():][:tcpSrcDstPortLen])
cid.srcPort = tcpHdr.SourcePort()
cid.dstPort = tcpHdr.DestinationPort()
}
tcpHdr = header.TCP(h[it.HeaderOffset():][:tcpSrcDstPortLen])
}
cid.srcAddr = ipHdr.SourceAddressSlice()
cid.dstAddr = ipHdr.DestinationAddressSlice()
cid.srcPort = tcpHdr.SourcePort()
cid.dstPort = tcpHdr.DestinationPort()
cid.proto = header.IPv6ProtocolNumber
default:
return cid, true
}