Skip to content

libtailscale: key connection cleanup by the Go-owned socket - #69

Open
jamesyong-42 wants to merge 1 commit into
tailscale:mainfrom
jamesyong-42:fix-conn-cleanup-fd-reuse
Open

jamesyong-42 wants to merge 1 commit into
tailscale:mainfrom
jamesyong-42:fix-conn-cleanup-fd-reuse

Conversation

@jamesyong-42

Copy link
Copy Markdown

newConn keyed conns by the C side of each connection's socketpair. TsnetListen closes that fd right after sendmsg, so the next accepted connection usually reuses its number and overwrites the entry while the first connection is still alive. The first connection's connCleanup then skips cleanup: its net.Conn is never closed, and its socketpair end is only closed by the os.File finalizer. conns still drains to empty, so TestConn doesn't notice.

This keys the entry by fds[1], the Go-owned end. That fd stays open until connCleanup deletes the entry and closes it, so its number can't be reused while the connection is alive. This also covers TsnetDial, where C can close its fd, and the number can be reused, before cleanup runs.

newConn now returns the C fd instead of writing through *C.int, so the new test can call it directly (test files can't use cgo types).

TestConnCleanupAfterFDReuse fails on main (all eight C-side fds get the same number, and connection 0 is never closed) and passes with this change. go test -race -count=3 ./... passes on macOS.

Found while moving truffle's TailscaleKit build onto main. Truffle carries an equivalent fix as a local patch until this lands.

Fixes tailscale/tailscale#21934

🤖 Generated with Claude Code

newConn registered each connection in conns under fds[0], the C side
of its socketpair, and connCleanup only acts if the entry under that
number still belongs to it. TsnetListen closes fds[0] right after
passing it to C with SCM_RIGHTS, so the next accepted connection's
socketpair usually gets the same number and overwrites the entry while
the first connection is still alive. When the first connection ends,
its connCleanup finds the newer entry and returns: its net.Conn is
never closed, and its end of the socketpair is only closed once the
os.File is garbage collected. conns still drains to empty, so TestConn
cannot see it. TsnetDial has the same race when C closes its fd and the
number is reused before cleanup runs.

Key the entry by fds[1] instead. The Go side keeps that fd open until
connCleanup deletes the entry and closes it, so its number cannot be
reused while the connection is alive.

newConn now returns the C fd instead of writing through a *C.int, so
the new test can call it directly (test files cannot use cgo types).
TestConnCleanupAfterFDReuse fails on main and passes with this change.

Fixes tailscale/tailscale#21934

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jamesyong-42 added a commit to vibecook-dev/truffle that referenced this pull request Oct 25, 2026
The Apple provenance record and upgrade log said the conn-cleanup fix
was prepared for upstream. It is now tailscale/libtailscale#69, for
tailscale/tailscale#21934. Truffle's patch is the same diff; drop it
once the PR lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

libtailscale: accepted connections are not cleaned up when their fd number is reused

1 participant