Files
Shailend ChandandgVisor bot d894fdaee6 Return EOF, not EBADF, when receiving on a severed host UDS
SCMSender.beforeSave disconnects the endpoint on save: it shuts down and
closes the host fd, leaving fd = -1. Send already handles this state via
its IsSendClosed check (beforeSave sets sendShutdown), so writes fail
with EPIPE, but Recv had no guard and passed -1 to recvmsg(2), leaking
EBADF to applications for an fd that is still valid in their fd table.
Return ErrClosedForReceive instead, which the socket layer already
surfaces as read() == 0, matching Linux EOF semantics for a socket whose
read side is shut down and making a severed host endpoint behave like
any socket whose peer has gone away.

The guard keys on fd < 0 rather than IsRecvClosed: after a local SHUT_RD
on a live fd, the host socket may still hold queued data that must be
drained before EOF, and host recvmsg(2) handles that correctly.

PiperOrigin-RevId: 976063849
2026-09-03 20:21:53 -07:00

137 lines
4.7 KiB
Go

// Copyright 2026 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.
package transport
import (
"testing"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/syserr"
"gvisor.dev/gvisor/pkg/waiter"
)
// TestSCMSenderBeforeSaveIdempotent verifies that calling beforeSave
// on an SCMSender whose host file descriptor is already closed (fd < 0)
// is a safe no-op and does not panic in fdnotifier.RemoveFD.
func TestSCMSenderBeforeSaveIdempotent(t *testing.T) {
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
t.Fatalf("Socketpair failed: %v", err)
}
defer unix.Close(fds[1])
q := &waiter.Queue{}
ep, sysErr := NewSCMSender(fds[0], q, "test-addr")
if sysErr != nil {
unix.Close(fds[0])
t.Fatalf("NewSCMSender failed: %v", sysErr)
}
if err := ep.Init(); err != nil {
unix.Close(fds[0])
t.Fatalf("Init failed: %v", err)
}
// First beforeSave cleans up the host FD and sets ep.fd = -1.
ep.beforeSave()
if ep.fd != -1 {
t.Fatalf("after beforeSave, ep.fd = %d, want -1", ep.fd)
}
// Second beforeSave must be idempotent and must not panic on fdnotifier.RemoveFD(-1).
ep.beforeSave()
}
// TestHostSenderClosedHostReadiness verifies that HostReadiness reports
// hangup and readable events when the host socket is closed (fd < 0) so that pollers
// are notified of EOF / disconnection.
func TestHostSenderClosedHostReadiness(t *testing.T) {
c := &HostSender{fd: -1}
mask := waiter.ReadableEvents | waiter.EventHUp | waiter.EventRdHUp | waiter.WritableEvents
got := c.HostReadiness(mask)
want := (waiter.ReadableEvents | waiter.WritableEvents | waiter.EventHUp | waiter.EventRdHUp) & mask
if got != want {
t.Fatalf("HostReadiness(%v) when fd < 0 = %v, want %v", mask, got, want)
}
// A severed socket reports writable.
if gotOut := c.HostReadiness(waiter.EventOut); gotOut != waiter.EventOut {
t.Errorf("HostReadiness(EventOut) when fd < 0 = %v, want %v", gotOut, waiter.EventOut)
}
// Querying EventHUp explicitly must return EventHUp.
if gotHup := c.HostReadiness(waiter.EventHUp); gotHup != waiter.EventHUp {
t.Errorf("HostReadiness(EventHUp) when fd < 0 = %v, want %v", gotHup, waiter.EventHUp)
}
// Querying EventIn explicitly must return EventIn.
if gotIn := c.HostReadiness(waiter.EventIn); gotIn != waiter.EventIn {
t.Errorf("HostReadiness(EventIn) when fd < 0 = %v, want %v", gotIn, waiter.EventIn)
}
}
// TestConnectionedEndpointClosedHostReadiness verifies that connectionedEndpoint.Readiness
// properly propagates hangup and readable events when backed by a closed HostSender.
func TestConnectionedEndpointClosedHostReadiness(t *testing.T) {
c := &HostSender{fd: -1}
ep := &connectionedEndpoint{
baseEndpoint: baseEndpoint{
receiver: c,
peer: c,
},
}
mask := waiter.EventIn | waiter.EventHUp | waiter.EventRdHUp | waiter.EventOut
got := ep.Readiness(mask)
want := (waiter.ReadableEvents | waiter.WritableEvents | waiter.EventHUp | waiter.EventRdHUp) & mask
if got != want {
t.Fatalf("Readiness(%v) for closed host endpoint = %v, want %v", mask, got, want)
}
}
// TestSeveredRecvSend verifies that an SCMSender whose host fd was severed by
// beforeSave reports EOF on receive and EPIPE on send, never EBADF: the
// application's fd is still valid even though the sentry's host fd is gone.
func TestSeveredRecvSend(t *testing.T) {
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
t.Fatalf("Socketpair failed: %v", err)
}
defer unix.Close(fds[1])
ep, serr := NewSCMSender(fds[0], &waiter.Queue{}, "test-addr")
if serr != nil {
unix.Close(fds[0])
t.Fatalf("NewSCMSender failed: %v", serr)
}
if err := ep.Init(); err != nil {
unix.Close(fds[0])
t.Fatalf("Init failed: %v", err)
}
ep.beforeSave()
if ep.fd != -1 {
t.Fatalf("after beforeSave, ep.fd = %d, want -1", ep.fd)
}
buf := [][]byte{make([]byte, 8)}
if _, _, serr := ep.Recv(nil, buf, RecvArgs{}); serr != syserr.ErrClosedForReceive {
t.Errorf("Recv on severed endpoint returned %v, want %v", serr, syserr.ErrClosedForReceive)
}
if _, _, serr := ep.Send(nil, [][]byte{{1}}, ControlMessages{}, Address{}); serr != syserr.ErrClosedForSend {
t.Errorf("Send on severed endpoint returned %v, want %v", serr, syserr.ErrClosedForSend)
}
}