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

@ -66,13 +66,16 @@ type Endpoint struct {
// TODO(https://gvisor.dev/issue/6389): Use different fields for IPv4/IPv6.
// +checklocks:mu
multicastAddr tcpip.Address
// TODO(https://gvisor.dev/issue/6389): Use different fields for IPv4/IPv6.
// +checklocks:mu
multicastNICID tcpip.NICID
// +checklocks:mu
ipv6MulticastNICID tcpip.NICID
// +checklocks:mu
ipv4TOS uint8
// +checklocks:mu
ipv6TClass uint8
// +checklocks:mu
pmtud tcpip.PMTUDStrategy
// Lock ordering: mu > infoMu.
infoMu sync.RWMutex `state:"nosave"`
@ -181,7 +184,11 @@ func (e *Endpoint) Close() {
}
for mem := range e.multicastMemberships {
e.stack.LeaveGroup(e.netProto, mem.nicID, mem.multicastAddr)
proto, err := e.multicastNetProto(mem.multicastAddr)
if err != nil {
panic("non multicast address in an existing membership")
}
e.stack.LeaveGroup(proto, mem.nicID, mem.multicastAddr)
}
e.multicastMemberships = nil
@ -229,6 +236,7 @@ type WriteContext struct {
route *stack.Route
ttl uint8
tos uint8
df bool
}
func (c *WriteContext) MTU() uint32 {
@ -274,33 +282,33 @@ func (c *WriteContext) TryNewPacketBuffer(reserveHdrBytes int, data buffer.Buffe
if !e.hasSendSpaceRLocked() {
return nil
}
return c.newPacketBufferLocked(reserveHdrBytes, data)
mark := e.ops.GetMark()
return c.newPacketBufferLocked(reserveHdrBytes, data, mark)
}
// TryNewPacketBufferFromPayloader returns a new packet buffer iff the endpoint's send buffer
// TryNewPacketBufferFromPayloader returns a new packet buffer if the endpoint's send buffer
// is not full. Otherwise, data from `payloader` isn't read.
//
// If this method returns nil, the caller should wait for the endpoint to become
// writable.
func (c *WriteContext) TryNewPacketBufferFromPayloader(reserveHdrBytes int, payloader tcpip.Payloader) *stack.PacketBuffer {
func (c *WriteContext) TryNewPacketBufferFromPayloader(reserveHdrBytes int, payloader tcpip.Payloader) (*stack.PacketBuffer, tcpip.Error) {
e := c.e
e.sendBufferSizeInUseMu.Lock()
defer e.sendBufferSizeInUseMu.Unlock()
if !e.hasSendSpaceRLocked() {
return nil
return nil, &tcpip.ErrWouldBlock{}
}
var data buffer.Buffer
if _, err := data.WriteFromReader(payloader, int64(payloader.Len())); err != nil {
data.Release()
return nil
return nil, &tcpip.ErrBadBuffer{}
}
return c.newPacketBufferLocked(reserveHdrBytes, data)
mark := e.ops.GetMark()
return c.newPacketBufferLocked(reserveHdrBytes, data, mark), nil
}
// +checklocks:c.e.sendBufferSizeInUseMu
func (c *WriteContext) newPacketBufferLocked(reserveHdrBytes int, data buffer.Buffer) *stack.PacketBuffer {
func (c *WriteContext) newPacketBufferLocked(reserveHdrBytes int, data buffer.Buffer, mark uint32) *stack.PacketBuffer {
e := c.e
// Note that we allow oversubscription - if there is any space at all in the
// send buffer, we accept the full packet which may be larger than the space
@ -323,6 +331,7 @@ func (c *WriteContext) newPacketBufferLocked(reserveHdrBytes int, data buffer.Bu
return stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: reserveHdrBytes,
Payload: data,
Mark: mark,
OnRelease: func() {
e.sendBufferSizeInUseMu.Lock()
if got := e.sendBufferSizeInUse; got < pktSize {
@ -360,6 +369,7 @@ func (c *WriteContext) WritePacket(pkt *stack.PacketBuffer, headerIncluded bool)
Protocol: c.e.transProto,
TTL: c.ttl,
TOS: c.tos,
DF: c.df,
ExperimentOptionValue: expOptVal,
}, pkt)
@ -433,8 +443,8 @@ func (e *Endpoint) AcquireContextForWrite(opts tcpip.WriteOptions) (WriteContext
route := e.connectedRoute
to := opts.To
info := e.Info()
switch {
case to == nil:
switch to {
case nil:
// If the user doesn't specify a destination, they should have
// connected to another address.
if e.State() != transport.DatagramEndpointStateConnected {
@ -566,11 +576,28 @@ func (e *Endpoint) AcquireContextForWrite(opts tcpip.WriteOptions) (WriteContext
panic(fmt.Sprintf("invalid protocol number = %d", netProto))
}
// Set the DF (Don't Fragment) bit based on the PMTUD strategy,
// matching TCP behavior in connect.go.
// Note: In gVisor, WANT and DO are treated identically (both set DF).
// Linux kernel differentiates them (WANT allows local fragmentation,
// DO returns EMSGSIZE), but gVisor's IPv4 layer always allows local
// fragmentation for locally-generated packets regardless of DF
// (see gvisor.dev/issue/5919).
//
// PROBE also sets DF, matching Linux ip_dont_fragment(). In Linux,
// PROBE differs from DO only in that it ignores incoming ICMP
// "Fragmentation Needed" messages (i.e. does not update the cached
// route PMTU). Since gVisor does not implement ICMP-based PMTU
// feedback for transport sockets, PROBE and DO are functionally
// equivalent here.
df := e.pmtud == tcpip.PMTUDiscoveryWant || e.pmtud == tcpip.PMTUDiscoveryDo || e.pmtud == tcpip.PMTUDiscoveryProbe
return WriteContext{
e: e,
route: route,
ttl: ttl,
tos: tos,
df: df,
}, nil
}
@ -613,7 +640,7 @@ func (e *Endpoint) connectRouteRLocked(nicID tcpip.NICID, localAddr tcpip.Addres
localAddr = tcpip.Address{}
}
if header.IsV4MulticastAddress(addr.Addr) || header.IsV6MulticastAddress(addr.Addr) {
if header.IsV4MulticastAddress(addr.Addr) {
if nicID == 0 {
nicID = e.multicastNICID
}
@ -621,6 +648,9 @@ func (e *Endpoint) connectRouteRLocked(nicID tcpip.NICID, localAddr tcpip.Addres
localAddr = e.multicastAddr
}
}
if header.IsV6MulticastAddress(addr.Addr) && nicID == 0 {
nicID = e.ipv6MulticastNICID
}
}
// Find a route to the desired destination.
@ -835,9 +865,18 @@ func (e *Endpoint) GetRemoteAddress() (tcpip.FullAddress, bool) {
func (e *Endpoint) SetSockOptInt(opt tcpip.SockOptInt, v int) tcpip.Error {
switch opt {
case tcpip.MTUDiscoverOption:
// Return not supported if the value is not disabling path
// MTU discovery.
if tcpip.PMTUDStrategy(v) != tcpip.PMTUDiscoveryDont {
// Store PMTU discovery settings. The DF bit on outgoing
// packets is set accordingly in AcquireContextForWrite.
// PROBE is accepted alongside DO/WANT/DONT. In Linux,
// PROBE sets DF but ignores ICMP-based PMTU updates;
// since gVisor lacks ICMP PMTU feedback, it behaves
// identically to DO.
switch tcpip.PMTUDStrategy(v) {
case tcpip.PMTUDiscoveryWant, tcpip.PMTUDiscoveryDont, tcpip.PMTUDiscoveryDo, tcpip.PMTUDiscoveryProbe:
e.mu.Lock()
e.pmtud = tcpip.PMTUDStrategy(v)
e.mu.Unlock()
default:
return &tcpip.ErrNotSupported{}
}
@ -865,6 +904,18 @@ func (e *Endpoint) SetSockOptInt(opt tcpip.SockOptInt, v int) tcpip.Error {
e.mu.Lock()
e.ipv6TClass = uint8(v)
e.mu.Unlock()
case tcpip.IPv6MulticastInterfaceOption:
if v != 0 && !e.stack.CheckNIC(tcpip.NICID(v)) {
return &tcpip.ErrUnknownNICID{}
}
e.mu.Lock()
defer e.mu.Unlock()
nic := tcpip.NICID(v)
if info := e.Info(); info.BindNICID != 0 && info.BindNICID != nic {
return &tcpip.ErrInvalidEndpointState{}
}
e.ipv6MulticastNICID = nic
}
return nil
@ -874,8 +925,10 @@ func (e *Endpoint) SetSockOptInt(opt tcpip.SockOptInt, v int) tcpip.Error {
func (e *Endpoint) GetSockOptInt(opt tcpip.SockOptInt) (int, tcpip.Error) {
switch opt {
case tcpip.MTUDiscoverOption:
// The only supported setting is path MTU discovery disabled.
return int(tcpip.PMTUDiscoveryDont), nil
e.mu.Lock()
v := int(e.pmtud)
e.mu.Unlock()
return v, nil
case tcpip.MulticastTTLOption:
e.mu.Lock()
@ -907,11 +960,30 @@ func (e *Endpoint) GetSockOptInt(opt tcpip.SockOptInt) (int, tcpip.Error) {
e.mu.RUnlock()
return v, nil
case tcpip.IPv6MulticastInterfaceOption:
e.mu.RLock()
v := int(e.ipv6MulticastNICID)
e.mu.RUnlock()
return v, nil
default:
return -1, &tcpip.ErrUnknownProtocolOption{}
}
}
// multicastNetProto returns the network protocol of a given multicast address.
// Returns an error if the address is not a multicast address.
func (e *Endpoint) multicastNetProto(addr tcpip.Address) (tcpip.NetworkProtocolNumber, tcpip.Error) {
switch {
case header.IsV4MulticastAddress(addr):
return header.IPv4ProtocolNumber, nil
case header.IsV6MulticastAddress(addr):
return header.IPv6ProtocolNumber, nil
default:
return 0, &tcpip.ErrInvalidOptionValue{}
}
}
// SetSockOpt sets the socket option.
func (e *Endpoint) SetSockOpt(opt tcpip.SettableSocketOption) tcpip.Error {
switch v := opt.(type) {
@ -952,21 +1024,23 @@ func (e *Endpoint) SetSockOpt(opt tcpip.SettableSocketOption) tcpip.Error {
e.multicastAddr = addr
case *tcpip.AddMembershipOption:
if !(header.IsV4MulticastAddress(v.MulticastAddr) && e.netProto == header.IPv4ProtocolNumber) && !(header.IsV6MulticastAddress(v.MulticastAddr) && e.netProto == header.IPv6ProtocolNumber) {
return &tcpip.ErrInvalidOptionValue{}
// Allowing IP_ADD_MEMBERSHIP on an ipv6 socket matches Linux behavior:
// https://github.com/torvalds/linux/blob/cec1e6e5d1a/net/ipv6/ipv6_sockglue.c#L964
proto, err := e.multicastNetProto(v.MulticastAddr)
if err != nil {
return err
}
nicID := v.NIC
if v.InterfaceAddr.Unspecified() {
if nicID == 0 {
if r, err := e.stack.FindRoute(0, tcpip.Address{}, v.MulticastAddr, e.netProto, false /* multicastLoop */); err == nil {
if r, err := e.stack.FindRoute(0, tcpip.Address{}, v.MulticastAddr, proto, false /* multicastLoop */); err == nil {
nicID = r.NICID()
r.Release()
}
}
} else {
nicID = e.stack.CheckLocalAddress(nicID, e.netProto, v.InterfaceAddr)
nicID = e.stack.CheckLocalAddress(nicID, proto, v.InterfaceAddr)
}
if nicID == 0 {
return &tcpip.ErrUnknownDevice{}
@ -981,27 +1055,28 @@ func (e *Endpoint) SetSockOpt(opt tcpip.SettableSocketOption) tcpip.Error {
return &tcpip.ErrPortInUse{}
}
if err := e.stack.JoinGroup(e.netProto, nicID, v.MulticastAddr); err != nil {
if err := e.stack.JoinGroup(proto, nicID, v.MulticastAddr); err != nil {
return err
}
e.multicastMemberships[memToInsert] = struct{}{}
case *tcpip.RemoveMembershipOption:
if !(header.IsV4MulticastAddress(v.MulticastAddr) && e.netProto == header.IPv4ProtocolNumber) && !(header.IsV6MulticastAddress(v.MulticastAddr) && e.netProto == header.IPv6ProtocolNumber) {
return &tcpip.ErrInvalidOptionValue{}
proto, err := e.multicastNetProto(v.MulticastAddr)
if err != nil {
return err
}
nicID := v.NIC
if v.InterfaceAddr.Unspecified() {
if nicID == 0 {
if r, err := e.stack.FindRoute(0, tcpip.Address{}, v.MulticastAddr, e.netProto, false /* multicastLoop */); err == nil {
if r, err := e.stack.FindRoute(0, tcpip.Address{}, v.MulticastAddr, proto, false /* multicastLoop */); err == nil {
nicID = r.NICID()
r.Release()
}
}
} else {
nicID = e.stack.CheckLocalAddress(nicID, e.netProto, v.InterfaceAddr)
nicID = e.stack.CheckLocalAddress(nicID, proto, v.InterfaceAddr)
}
if nicID == 0 {
return &tcpip.ErrUnknownDevice{}
@ -1016,7 +1091,7 @@ func (e *Endpoint) SetSockOpt(opt tcpip.SettableSocketOption) tcpip.Error {
return &tcpip.ErrBadLocalAddress{}
}
if err := e.stack.LeaveGroup(e.netProto, nicID, v.MulticastAddr); err != nil {
if err := e.stack.LeaveGroup(proto, nicID, v.MulticastAddr); err != nil {
return err
}