gvisor-lx/pkg/fdnotifier/fdnotifier.go
Leadaxe 117243aa02 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-паникой, что в полевом крашдампе; с ним зелёный.
2026-08-05 14:53:31 +03:00

244 lines
5.7 KiB
Go

// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
// +build linux
// Package fdnotifier contains an adapter that translates IO events (e.g., a
// file became readable/writable) from native FDs to the notifications in the
// waiter package. It uses epoll in edge-triggered mode to receive notifications
// for registered FDs.
package fdnotifier
import (
"fmt"
"github.com/sagernet/gvisor/pkg/sync"
"github.com/sagernet/gvisor/pkg/waiter"
"golang.org/x/sys/unix"
)
type fdInfo struct {
queue *waiter.Queue
waiting bool
}
// notifier holds all the state necessary to issue notifications when IO events
// occur in the observed FDs.
type notifier struct {
// epFD is the epoll file descriptor used to register for io
// notifications.
epFD int
// pauseMu synchronizes notifications with save/restore.
pauseMu sync.Mutex
// mu protects fdMap.
mu sync.Mutex
// fdMap maps file descriptors to their notification queues and waiting
// status.
fdMap map[int32]*fdInfo
}
// newNotifier creates a new notifier object.
func newNotifier() (*notifier, error) {
epfd, err := unix.EpollCreate1(0)
if err != nil {
return nil, err
}
w := &notifier{
epFD: epfd,
fdMap: make(map[int32]*fdInfo),
}
go w.waitAndNotify() // S/R-SAFE: no waiter exists during save / load.
return w, nil
}
// waitFD waits on mask for fd. The fdMap mutex must be hold.
func (n *notifier) waitFD(fd int32, fi *fdInfo, mask waiter.EventMask) error {
if !fi.waiting && mask == 0 {
return nil
}
e := unix.EpollEvent{
Events: mask.ToLinux() | unix.EPOLLET,
Fd: fd,
}
switch {
case !fi.waiting && mask != 0:
if err := unix.EpollCtl(n.epFD, unix.EPOLL_CTL_ADD, int(fd), &e); err != nil {
return err
}
fi.waiting = true
case fi.waiting && mask == 0:
unix.EpollCtl(n.epFD, unix.EPOLL_CTL_DEL, int(fd), nil)
fi.waiting = false
case fi.waiting && mask != 0:
if err := unix.EpollCtl(n.epFD, unix.EPOLL_CTL_MOD, int(fd), &e); err != nil {
return err
}
}
return nil
}
// addFD adds an FD to the list of FDs observed by n.
func (n *notifier) addFD(fd int32, queue *waiter.Queue) error {
n.mu.Lock()
defer n.mu.Unlock()
// Panic if we're already notifying on this FD.
if _, ok := n.fdMap[fd]; ok {
panic(fmt.Sprintf("File descriptor %v added twice", fd))
}
info := &fdInfo{queue: queue}
// We might already have something in queue to wait for.
if err := n.waitFD(fd, info, queue.Events()); err != nil {
return err
}
// Add it to the map.
n.fdMap[fd] = info
return nil
}
// updateFD updates the set of events the fd needs to be notified on.
func (n *notifier) updateFD(fd int32) error {
n.mu.Lock()
defer n.mu.Unlock()
if fi, ok := n.fdMap[fd]; ok {
return n.waitFD(fd, fi, fi.queue.Events())
}
return nil
}
// RemoveFD removes an FD from the list of FDs observed by n.
func (n *notifier) removeFD(fd int32) {
n.mu.Lock()
defer n.mu.Unlock()
// Remove from map, then from epoll object.
n.waitFD(fd, n.fdMap[fd], 0)
delete(n.fdMap, fd)
}
// hasFD returns true if the fd is in the list of observed FDs.
func (n *notifier) hasFD(fd int32) bool {
n.mu.Lock()
defer n.mu.Unlock()
_, ok := n.fdMap[fd]
return ok
}
// waitAndNotify run is its own goroutine and loops waiting for io event
// notifications from the epoll object. Once notifications arrive, they are
// dispatched to the registered queue.
func (n *notifier) waitAndNotify() error {
e := make([]unix.EpollEvent, 100)
for {
v, err := epollWait(n.epFD, e, -1)
if err == unix.EINTR {
continue
}
if err != nil {
return err
}
notified := false
n.pauseMu.Lock()
n.mu.Lock()
for i := 0; i < v; i++ {
if fi, ok := n.fdMap[e[i].Fd]; ok {
fi.queue.Notify(waiter.EventMaskFromLinux(e[i].Events))
notified = true
}
}
n.mu.Unlock()
n.pauseMu.Unlock()
if notified {
// Let goroutines woken by Notify get a chance to run before we
// epoll_wait again.
sync.Goyield()
}
}
}
// pause suspends notifications until resume is called.
func (n *notifier) pause() {
n.pauseMu.Lock()
}
// resume ends the effect of a previous call to pause.
func (n *notifier) resume() {
n.pauseMu.Unlock()
}
var shared struct {
notifier *notifier
once sync.Once
initErr error
}
func ensureSharedNotifier() {
shared.once.Do(func() {
shared.notifier, shared.initErr = newNotifier()
})
}
// AddFD adds an FD to the list of observed FDs.
func AddFD(fd int32, queue *waiter.Queue) error {
ensureSharedNotifier()
if shared.initErr != nil {
return shared.initErr
}
return shared.notifier.addFD(fd, queue)
}
// UpdateFD updates the set of events the fd needs to be notified on.
func UpdateFD(fd int32) error {
return shared.notifier.updateFD(fd)
}
// RemoveFD removes an FD from the list of observed FDs.
func RemoveFD(fd int32) {
shared.notifier.removeFD(fd)
}
// HasFD returns true if the FD is in the list of observed FDs.
//
// This should only be used by tests to assert that FDs are correctly registered.
func HasFD(fd int32) bool {
return shared.notifier.hasFD(fd)
}
// Pause suspends notifications until Resume is called.
func Pause() {
ensureSharedNotifier()
shared.notifier.pause()
}
// Resume ends the effect of a previous call to Pause.
func Resume() {
shared.notifier.resume()
}