-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Description
When a Unix socket listener is closed while there are pending connections (connections that have been established but not yet accepted), gVisor returns EOF (0 bytes read) instead of ECONNRESET error, which differs from Linux kernel behavior.
Expected behavior (Linux kernel):
read()returnsECONNRESETerror (errno 104)getsockopt(SO_ERROR)returns 104 (ECONNRESET)epollreportsEPOLLHUP | EPOLLERRevents
Observed behavior (gVisor):
read()returns 0 (EOF)getsockopt(SO_ERROR)returns 0 (no error)epollreports onlyEPOLLHUP(missingEPOLLERR)
This causes issues with async frameworks like tokio that rely on these semantics to detect connection failures.
Related issue: tokio-rs/tokio#3879
Analysis
The issue is in pkg/sentry/socket/unix/transport/connectioned.go. When a listener closes:
- Linux kernel behavior: Sends RST to pending connections, setting
SO_ERRORtoECONNRESETand triggeringEPOLLERR - gVisor behavior: Calls
Close()on pending connections which sends FIN (graceful close), resulting in EOF
Relevant code
In connectionedEndpoint.Close():
if acceptedChan != nil {
for n := range acceptedChan {
n.Close(ctx) // This does graceful close (EOF), not reset (ECONNRESET)
}
}Potential fix
When closing pending connections, gVisor should:
- Set
SO_ERRORtoECONNRESETon the client endpoint - Notify
EventErrso epoll reportsEPOLLERR - Make
read()returnECONNRESET(first call) following Linux's read-once-and-clear semantics
Steps to reproduce
Option 1: Using tokio test suite
# Create Dockerfile
cat > Dockerfile << 'EOF'
FROM rust:1.84-bookworm
WORKDIR /testbed
RUN git clone --depth 1 https://github.com/tokio-rs/tokio.git . && \
cargo test --test uds_stream --no-run || true
EOF
# Build image
docker build -t tokio-uds-test .
# Test with runc (passes)
docker run --rm --runtime=runc tokio-uds-test cargo test --test uds_stream epollhup
# Test with gVisor (fails)
docker run --rm --runtime=runsc tokio-uds-test cargo test --test uds_stream epollhupExpected output (runc):
running 1 test
test epollhup ... ok
Actual output (gVisor):
running 1 test
test epollhup ... FAILED
---- epollhup stdout ----
thread 'epollhup' panicked at tokio/tests/uds_stream.rs:412:29:
called `Result::unwrap_err()` on an `Ok` value: PollEvented { io: Some(UnixStream { ... }) }
Option 2: Minimal C reproduction
cat > /tmp/test_uds.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/epoll.h>
#include <errno.h>
#define SOCK_PATH "/tmp/test_uds.sock"
int main() {
unlink(SOCK_PATH);
// Create listener
int listener = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = {.sun_family = AF_UNIX};
strcpy(addr.sun_path, SOCK_PATH);
bind(listener, (struct sockaddr*)&addr, sizeof(addr));
listen(listener, 5);
// Create client and connect
int client = socket(AF_UNIX, SOCK_STREAM, 0);
connect(client, (struct sockaddr*)&addr, sizeof(addr));
// Close listener while connection is pending (not accepted)
close(listener);
// Check epoll events
int epfd = epoll_create1(0);
struct epoll_event ev = {.events = EPOLLIN|EPOLLOUT|EPOLLERR|EPOLLHUP, .data.fd = client};
epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev);
struct epoll_event events[1];
epoll_wait(epfd, events, 1, 100);
printf("epoll events: 0x%x\n", events[0].events);
printf(" EPOLLIN: %s\n", (events[0].events & EPOLLIN) ? "yes" : "no");
printf(" EPOLLOUT: %s\n", (events[0].events & EPOLLOUT) ? "yes" : "no");
printf(" EPOLLERR: %s\n", (events[0].events & EPOLLERR) ? "yes" : "no"); // Missing in gVisor!
printf(" EPOLLHUP: %s\n", (events[0].events & EPOLLHUP) ? "yes" : "no");
// First read - should return ECONNRESET error
char buf[10];
ssize_t n = read(client, buf, sizeof(buf));
if (n < 0) {
printf("read #1: error %d (%s)\n", errno, errno == 104 ? "ECONNRESET - correct" : strerror(errno));
} else {
printf("read #1: %zd bytes (EOF) - WRONG, should be ECONNRESET\n", n);
}
// Second read - should return EOF (error already consumed by first read)
n = read(client, buf, sizeof(buf));
if (n < 0) {
printf("read #2: error %d (%s)\n", errno, strerror(errno));
} else if (n == 0) {
printf("read #2: EOF - correct (error consumed)\n");
} else {
printf("read #2: %zd bytes - unexpected\n", n);
}
close(client);
close(epfd);
unlink(SOCK_PATH);
return 0;
}
EOF
# Compile
gcc -o /tmp/test_uds /tmp/test_uds.c
# Run in container
docker run --rm -v /tmp/test_uds:/test_uds --runtime=runc ubuntu:22.04 /test_uds
docker run --rm -v /tmp/test_uds:/test_uds --runtime=runsc ubuntu:22.04 /test_udsrunc output (correct):
epoll events: 0x1d
EPOLLIN: yes
EPOLLOUT: yes
EPOLLERR: yes
EPOLLHUP: yes
read #1: error 104 (ECONNRESET - correct)
read #2: EOF - correct (error consumed)
gVisor output (incorrect):
epoll events: 0x15
EPOLLIN: yes
EPOLLOUT: yes
EPOLLERR: no <-- Missing!
EPOLLHUP: yes
read #1: 0 bytes (EOF) - WRONG, should be ECONNRESET
read #2: EOF - correct (error consumed)
runsc version
runsc version release-20260126.0
spec: 1.1.0-rc.1docker version (if using docker)
Docker version 26.1.3, build b72abbbuname
Linux 6.8.0-52-generic #53-Ubuntu SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
kubectl (if using Kubernetes)
repo state (if built from source)
No response