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

@ -371,6 +371,7 @@ func (h *handshake) synSentState(s *segment) tcpip.Error {
seq: h.iss,
ack: h.ackNum,
rcvWnd: h.rcvWnd,
df: h.ep.pmtud == tcpip.PMTUDiscoveryWant || h.ep.pmtud == tcpip.PMTUDiscoveryDo || h.ep.pmtud == tcpip.PMTUDiscoveryProbe,
expOptVal: h.ep.getExperimentOptionValue(h.ep.route),
}, synOpts)
return nil
@ -458,6 +459,7 @@ func (h *handshake) synRcvdState(s *segment) tcpip.Error {
seq: h.iss,
ack: h.ackNum,
rcvWnd: h.rcvWnd,
df: h.ep.pmtud == tcpip.PMTUDiscoveryWant || h.ep.pmtud == tcpip.PMTUDiscoveryDo || h.ep.pmtud == tcpip.PMTUDiscoveryProbe,
expOptVal: h.ep.getExperimentOptionValue(h.ep.route),
}, synOpts)
return nil
@ -555,6 +557,7 @@ func (h *handshake) processSegments() tcpip.Error {
// start sends the first SYN/SYN-ACK. It does not block, even if link address
// resolution is required.
// +checklocks:h.ep.mu
func (h *handshake) start() {
h.startTime = h.ep.stack.Clock().NowMonotonic()
h.ep.amss = calculateAdvertisedMSS(h.ep.userMSS, h.ep.route)
@ -595,6 +598,7 @@ func (h *handshake) start() {
seq: h.iss,
ack: h.ackNum,
rcvWnd: h.rcvWnd,
df: h.ep.pmtud == tcpip.PMTUDiscoveryWant || h.ep.pmtud == tcpip.PMTUDiscoveryDo || h.ep.pmtud == tcpip.PMTUDiscoveryProbe,
expOptVal: h.ep.getExperimentOptionValue(h.ep.route),
}, synOpts)
}
@ -632,6 +636,7 @@ func (h *handshake) retransmitHandlerLocked() tcpip.Error {
seq: h.iss,
ack: h.ackNum,
rcvWnd: h.rcvWnd,
df: h.ep.pmtud == tcpip.PMTUDiscoveryWant || h.ep.pmtud == tcpip.PMTUDiscoveryDo || h.ep.pmtud == tcpip.PMTUDiscoveryProbe,
expOptVal: e.getExperimentOptionValue(e.route),
}, h.sendSYNOpts)
// If we have ever retransmitted the SYN-ACK or
@ -642,11 +647,10 @@ func (h *handshake) retransmitHandlerLocked() tcpip.Error {
return nil
}
// transitionToStateEstablisedLocked transitions the endpoint of the handshake
// transitionToStateEstablishedLocked transitions the endpoint of the handshake
// to an established state given the last segment received from peer. It also
// initializes sender/receiver.
// +checklocks:h.ep.mu
// +checklocksalias:h.ep.snd.ep.mu=h.ep.mu
func (h *handshake) transitionToStateEstablishedLocked(s *segment) {
// Stop the SYN retransmissions now that handshake is complete.
if h.retransmitTimer != nil {
@ -656,16 +660,20 @@ func (h *handshake) transitionToStateEstablishedLocked(s *segment) {
// Transfer handshake state to TCP connection. We disable
// receive window scaling if the peer doesn't support it
// (indicated by a negative send window scale).
h.ep.snd = newSender(h.ep, h.iss, h.ackNum-1, h.sndWnd, h.mss, h.sndWndScale)
initSender(h.ep, h.iss, h.ackNum-1, h.sndWnd, h.mss, h.sndWndScale)
now := h.ep.stack.Clock().NowMonotonic()
// Use the final handshake ACK's ingress time (s.rcvdTime) rather than the
// current clock to seed the initial RTT/RTO. If the ACK was delayed inside
// the stack before processing, the processing-time clock would inflate the
// initial RTO, which then persists for several RTTs.
rcvd := s.rcvdTime
var rtt time.Duration
if h.ep.SendTSOk && s.parsedOptions.TSEcr != 0 {
rtt = h.ep.elapsed(now, s.parsedOptions.TSEcr)
rtt = h.ep.elapsed(rcvd, s.parsedOptions.TSEcr)
}
if !h.sampleRTTWithTSOnly && rtt == 0 {
rtt = now.Sub(h.startTime)
rtt = rcvd.Sub(h.startTime)
}
if rtt > 0 {
@ -824,7 +832,10 @@ func (e *Endpoint) sendSynTCP(r *stack.Route, tf tcpFields, opts header.TCPSynOp
if r.NetProto() == header.IPv6ProtocolNumber && tf.expOptVal != 0 {
hdrSize += header.IPv6ExperimentHdrLength
}
p := stack.NewPacketBuffer(stack.PacketBufferOptions{ReserveHeaderBytes: hdrSize})
p := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: hdrSize,
Mark: e.ops.GetMark(),
})
defer p.DecRef()
if err := e.sendTCP(r, tf, p, stack.GSO{}); err != nil {
e.stats.SendErrors.SynSendToNetworkFailed.Increment()
@ -900,7 +911,10 @@ func sendTCPBatch(r *stack.Route, tf tcpFields, pkt *stack.PacketBuffer, gso sta
// Reserve extra bytes for the experiment option.
hdrSize += header.IPv6ExperimentHdrLength
}
splitPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ReserveHeaderBytes: hdrSize})
splitPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: hdrSize,
Mark: pkt.Mark,
})
splitPkt.Data().ReadFromPacketData(pkt.Data(), packetSize)
pkt = splitPkt
}
@ -1007,9 +1021,10 @@ func (e *Endpoint) makeOptions(sackBlocks []header.SACKBlock) []byte {
// sendEmptyRaw sends a TCP segment with no payload to the endpoint's peer.
//
// +checklocks:e.mu
// +checklocksalias:e.snd.ep.mu=e.mu
func (e *Endpoint) sendEmptyRaw(flags header.TCPFlags, seq, ack seqnum.Value, rcvWnd seqnum.Size) tcpip.Error {
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{})
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
Mark: e.ops.GetMark(),
})
defer pkt.DecRef()
return e.sendRaw(pkt, flags, seq, ack, rcvWnd)
}
@ -1018,7 +1033,6 @@ func (e *Endpoint) sendEmptyRaw(flags header.TCPFlags, seq, ack seqnum.Value, rc
// ownership of pkt. pkt must not have any headers set.
//
// +checklocks:e.mu
// +checklocksalias:e.snd.ep.mu=e.mu
func (e *Endpoint) sendRaw(pkt *stack.PacketBuffer, flags header.TCPFlags, seq, ack seqnum.Value, rcvWnd seqnum.Size) tcpip.Error {
var sackBlocks []header.SACKBlock
if e.EndpointState() == StateEstablished && e.rcv.pendingRcvdSegments.Len() > 0 && (flags&header.TCPFlagAck != 0) {
@ -1033,21 +1047,21 @@ func (e *Endpoint) sendRaw(pkt *stack.PacketBuffer, flags header.TCPFlags, seq,
}
pkt.ReserveHeaderBytes(hdrSize)
return e.sendTCP(e.route, tcpFields{
id: e.TransportEndpointInfo.ID,
ttl: calculateTTL(e.route, e.ipv4TTL, e.ipv6HopLimit),
tos: e.sendTOS,
flags: flags,
seq: seq,
ack: ack,
rcvWnd: rcvWnd,
opts: options,
df: e.pmtud == tcpip.PMTUDiscoveryWant || e.pmtud == tcpip.PMTUDiscoveryDo,
id: e.TransportEndpointInfo.ID,
ttl: calculateTTL(e.route, e.ipv4TTL, e.ipv6HopLimit),
tos: e.sendTOS,
flags: flags,
seq: seq,
ack: ack,
rcvWnd: rcvWnd,
opts: options,
// PROBE sets DF like DO; see network/endpoint.go for details.
df: e.pmtud == tcpip.PMTUDiscoveryWant || e.pmtud == tcpip.PMTUDiscoveryDo || e.pmtud == tcpip.PMTUDiscoveryProbe,
expOptVal: expOptVal,
}, pkt, e.gso)
}
// +checklocks:e.mu
// +checklocksalias:e.snd.ep.mu=e.mu
func (e *Endpoint) sendData(next *segment) {
// Initialize the next segment to write if it's currently nil.
if e.snd.writeNext == nil {
@ -1065,7 +1079,6 @@ func (e *Endpoint) sendData(next *segment) {
// error code and sends a RST if and only if the error is not ErrConnectionReset
// indicating that the connection is being reset due to receiving a RST.
// +checklocks:e.mu
// +checklocksalias:e.snd.ep.mu=e.mu
func (e *Endpoint) resetConnectionLocked(err tcpip.Error) {
// Only send a reset if the connection is being aborted for a reason
// other than receiving a reset.
@ -1080,12 +1093,26 @@ func (e *Endpoint) resetConnectionLocked(err tcpip.Error) {
//
// See: https://www.snellman.net/blog/archive/2016-02-01-tcp-rst/ for more
// information.
sndWndEnd := e.snd.SndUna.Add(e.snd.SndWnd)
resetSeqNum := sndWndEnd
if !sndWndEnd.LessThan(e.snd.SndNxt) || e.snd.SndNxt.Size(sndWndEnd) < (1<<e.snd.SndWndScale) {
resetSeqNum = e.snd.SndNxt
//
// e.snd and e.rcv may be nil if the endpoint is in a handshake
// state (e.g. SynSent) where the sender and receiver have not
// yet been initialized. Per Linux behavior, use a sequence
// number of zero when no ACK has been received (snd is nil),
// and a receive window of zero when rcv is nil since the
// connection will be immediately terminated.
var resetSeqNum seqnum.Value
var ackNum seqnum.Value
if e.snd != nil {
sndWndEnd := e.snd.SndUna.Add(e.snd.SndWnd)
resetSeqNum = sndWndEnd
if !sndWndEnd.LessThan(e.snd.SndNxt) || e.snd.SndNxt.Size(sndWndEnd) < (1<<e.snd.SndWndScale) {
resetSeqNum = e.snd.SndNxt
}
}
e.sendEmptyRaw(header.TCPFlagAck|header.TCPFlagRst, resetSeqNum, e.rcv.RcvNxt, 0)
if e.rcv != nil {
ackNum = e.rcv.RcvNxt
}
e.sendEmptyRaw(header.TCPFlagAck|header.TCPFlagRst, resetSeqNum, ackNum, 0)
}
// Don't purge read queues here. If there's buffered data, it's still allowed
// to be read.
@ -1162,52 +1189,66 @@ func (e *Endpoint) drainClosingSegmentQueue() {
}
}
// handleReset processes an inbound segment carrying the RST flag.
//
// Acceptance follows RFC 5961 section 3.2:
// - If the segment sequence number is out of window, the segment is
// silently dropped.
// - If the segment sequence number is in window but not exactly equal
// to RCV.NXT, the implementation sends a challenge ACK and drops
// the segment.
// - Only an exact match against RCV.NXT causes the connection to be
// reset.
//
// This is stricter than RFC 793 page 37, which accepted any in-window RST.
// The strict-match rule defends against off-path blind RST injection.
// Linux has implemented it since version 3.6 (2012); see
// net/ipv4/tcp_input.c tcp_validate_incoming().
//
// +checklocks:e.mu
func (e *Endpoint) handleReset(s *segment) (ok bool, err tcpip.Error) {
if e.rcv.acceptable(s.sequenceNumber, 0) {
// RFC 793, page 37 states that "in all states
// except SYN-SENT, all reset (RST) segments are
// validated by checking their SEQ-fields." So
// we only process it if it's acceptable.
switch e.EndpointState() {
// In case of a RST in CLOSE-WAIT linux moves
// the socket to closed state with an error set
// to indicate EPIPE.
//
// Technically this seems to be at odds w/ RFC.
// As per https://tools.ietf.org/html/rfc793#section-2.7
// page 69 the behavior for a segment arriving
// w/ RST bit set in CLOSE-WAIT is inlined below.
//
// ESTABLISHED
// FIN-WAIT-1
// FIN-WAIT-2
// CLOSE-WAIT
// If the RST bit is set then, any outstanding RECEIVEs and
// SEND should receive "reset" responses. All segment queues
// should be flushed. Users should also receive an unsolicited
// general "connection reset" signal. Enter the CLOSED state,
// delete the TCB, and return.
case StateCloseWait:
e.transitionToStateCloseLocked()
e.hardError = &tcpip.ErrAborted{}
return false, nil
default:
// RFC 793, page 37 states that "in all states
// except SYN-SENT, all reset (RST) segments are
// validated by checking their SEQ-fields." So
// we only process it if it's acceptable.
return false, &tcpip.ErrConnectionReset{}
}
if !e.rcv.acceptable(s.sequenceNumber, 0) {
// Out of window. Silent drop.
return true, nil
}
if s.sequenceNumber != e.rcv.RcvNxt {
// In window but not an exact match. Send a challenge ACK and drop the
// segment per RFC 5961 section 3.2. The challenge ACK helper rate-limits
// challenge transmission per RFC 5961 section 7.
e.snd.maybeSendOutOfWindowAck(s)
return true, nil
}
switch e.EndpointState() {
// In case of a RST in CLOSE-WAIT linux moves the socket to closed state
// with an error set to indicate EPIPE.
//
// As per https://tools.ietf.org/html/rfc793#section-2.7 page 69 the
// behavior for a segment arriving w/ RST bit set in CLOSE-WAIT is
// inlined below.
//
// ESTABLISHED
// FIN-WAIT-1
// FIN-WAIT-2
// CLOSE-WAIT
//
// If the RST bit is set then, any outstanding RECEIVEs and SEND should
// receive "reset" responses. All segment queues should be flushed.
// Users should also receive an unsolicited general "connection reset"
// signal. Enter the CLOSED state, delete the TCB, and return.
case StateCloseWait:
e.transitionToStateCloseLocked()
e.hardError = &tcpip.ErrAborted{}
return false, nil
default:
return false, &tcpip.ErrConnectionReset{}
}
return true, nil
}
// handleSegments processes all inbound segments.
//
// +checklocks:e.mu
// +checklocksalias:e.snd.ep.mu=e.mu
func (e *Endpoint) handleSegmentsLocked() tcpip.Error {
sndUna := e.snd.SndUna
for i := 0; i < maxSegmentsPerWake; i++ {
@ -1262,8 +1303,6 @@ func (e *Endpoint) probeSegmentLocked() {
// if the connection should be terminated.
//
// +checklocks:e.mu
// +checklocksalias:e.rcv.ep.mu=e.mu
// +checklocksalias:e.snd.ep.mu=e.mu
func (e *Endpoint) handleSegmentLocked(s *segment) (cont bool, err tcpip.Error) {
// Invoke the tcp probe if installed. The tcp probe function will update
// the TCPEndpointState after the segment is processed.
@ -1337,7 +1376,6 @@ func (e *Endpoint) handleSegmentLocked(s *segment) (cont bool, err tcpip.Error)
// keepalive packets periodically when the connection is idle. If we don't hear
// from the other side after a number of tries, we terminate the connection.
// +checklocks:e.mu
// +checklocksalias:e.snd.ep.mu=e.mu
func (e *Endpoint) keepaliveTimerExpired() tcpip.Error {
userTimeout := e.userTimeout
@ -1379,7 +1417,6 @@ func (e *Endpoint) keepaliveTimerExpired() tcpip.Error {
// whether it is enabled for this endpoint.
//
// +checklocks:e.mu
// +checklocksalias:e.snd.ep.mu=e.mu
func (e *Endpoint) resetKeepaliveTimer(receivedData bool) {
e.keepalive.Lock()
defer e.keepalive.Unlock()
@ -1442,7 +1479,6 @@ func (e *Endpoint) handshakeFailed(err tcpip.Error) {
// handleTimeWaitSegments processes segments received during TIME_WAIT
// state.
// +checklocks:e.mu
// +checklocksalias:e.rcv.ep.mu=e.mu
func (e *Endpoint) handleTimeWaitSegments() (extendTimeWait bool, reuseTW func()) {
for i := 0; i < maxSegmentsPerWake; i++ {
s := e.segmentQueue.dequeue()