Skip to content

[Bugfix][TransferEngine] Return EPIPE from handshake replies instead of dying on SIGPIPE - #4463

Merged
alogfans merged 2 commits into
kvcache-ai:mainfrom
he-yufeng:fix/handshake-sigpipe-nosignal
Oct 8, 2026
Merged

alogfans merged 2 commits into
kvcache-ai:mainfrom
he-yufeng:fix/handshake-sigpipe-nosignal

Conversation

@he-yufeng

@he-yufeng he-yufeng commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Fixes #4454.

The handshake daemon answers peers it has not vetted. When a peer resets the connection while the daemon is mid-reply, writeString() ended in a plain write() on the dead socket and the resulting SIGPIPE killed the whole process, host application included.

This adds writeFullySocket() next to writeFully(): same retry loop, but the bytes go out via send() with MSG_NOSIGNAL on Linux, and with SO_NOSIGPIPE set on the socket on macOS (which has no MSG_NOSIGNAL). A reset peer now surfaces as a plain EPIPE error return, and the daemon logs and moves on.

writeFully() is unchanged because its remaining callers write to regular files, where send() would fail with ENOTSOCK. writeString() switches over; every one of its call sites is an accepted handshake socket in transfer_metadata_plugin.cpp.

Two tests pin the behavior on a socketpair: bytes arrive intact on a healthy connection, and a closed peer yields -1/EPIPE with the process still alive. Without the suppression the second test never reaches its assert, because the process dies from SIGPIPE (exit status 141, reproduced locally with the same write pattern against a plain write()).

Module

  • Transfer Engine (mooncake-transfer-engine)

Type of Change

  • Bug fix

How Has This Been Tested?

The full CTest target needs Linux (libnuma, ibverbs), so locally I compiled mooncake-transfer-engine/tests/common_test.cpp directly against gtest on macOS arm64 and ran the suite. The Linux CTest run is covered by CI.

Test commands:

c++ -std=gnu++20 -arch arm64 tests/common_test.cpp -lglog -lgtest -lgtest_main
./common_test
./common_test --gtest_filter='WriteFullySocketTest.*'

Test results:

  • Unit tests pass (40 passed, 1 skipped: the upstream NUMA test skips on non-NUMA platforms as intended; both new WriteFullySocketTest cases pass)
  • Integration tests pass (if applicable)
  • Manual testing done (SIGPIPE kill reproduced without the fix, exit status 141; with the fix the error returns as EPIPE and the process survives)

Checklist

  • I have performed a self-review of my own code
  • I have formatted my code using ./scripts/code_format.sh
  • I have run pre-commit on the files changed in this PR and all hooks pass
  • I have updated the documentation (if applicable)
  • I have added tests to prove my changes are effective
  • For changes >500 LOC: I have filed an RFC issue

AI Assistance Disclosure

  • AI tools were used (specify below)

Prepared with AI assistance (Kimi Code): it drafted the patch and the tests. I reviewed every changed line and ran the verification described above myself.

…of dying on SIGPIPE

The handshake daemon answers peers it did not vet. If the peer resets
the connection while the daemon is mid-reply, writeString() reached a
plain write() on the dead socket and the resulting SIGPIPE killed the
whole process, host application included.

Add writeFullySocket() next to writeFully(): same loop, but sends with
MSG_NOSIGNAL on Linux and sets SO_NOSIGPIPE on the socket on macOS, so
a reset peer surfaces as a plain EPIPE error. writeFully() stays as is
because its remaining callers write to regular files, where send()
would fail with ENOTSOCK. writeString() switches over; every one of
its call sites is an accepted handshake socket.

Tests cover both ends: bytes arrive intact on a healthy socketpair,
and a closed peer yields -1/EPIPE with the process still alive (exit
status 141 without the suppression).
@he-yufeng

Copy link
Copy Markdown
Collaborator Author

The two red legs are the repo-wide gap tracked in #4439, not this PR: scripts/test_installation.sh installs unpinned torch (now 2.14.1) while the wheel only ships PG builds up to 2.14.0, so mooncake.pg_2_14_1 is missing on every PR's test-wheel run (same signature on codex/local-disk-retaining-clients and te-rdma-inline-single-write). The pin fix is #4440, green end to end there. This PR's own CTest and unit legs pass.

alogfans commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

[P1] writeFullySocket retries EAGAIN indefinitely. With #4451's SO_SNDTIMEO, a peer that stops reading still traps the handshake daemon in this loop, undoing that fix. Preserve EAGAIN/EWOULDBLOCK timeout handling here and run the stalled-peer regression with both changes applied.

common.h:476

…ying forever

A blocking socket only reports EAGAIN once SO_SNDTIMEO fires (kvcache-ai#4451), so
retrying it here would spin the handshake daemon indefinitely and undo
that fix. Mirror the writeFully handling: retry EINTR, fail the write on
EAGAIN/EWOULDBLOCK.
@he-yufeng

Copy link
Copy Markdown
Collaborator Author

Good catch, done in c14bf0b. writeFullySocket now mirrors writeFully: only EINTR retries, and EAGAIN/EWOULDBLOCK fails the write with the partial count, so once #4451's SO_SNDTIMEO is in, a peer that stops reading fails the reply instead of trapping the daemon in the retry loop.

Ran the stalled-peer regression on a tree with both changes applied (macOS can't build the engine natively, so this was a standalone harness over common.h): socketpair with a 1s SO_SNDTIMEO and an 8MB payload fails in about a second with Socket write timed out: expected 8388608 bytes, actual 4087 bytes, the reset-peer case returns EPIPE with the process alive, and a draining peer still receives the full payload. Rebuilt the same harness with the old retry-on-EAGAIN behavior as a control and it spins indefinitely, so the regression does catch the interaction.

@alogfans alogfans left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@alogfans
alogfans merged commit 9f4c206 into kvcache-ai:main Oct 8, 2026
51 of 54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Handshake daemon can kill the whole process with SIGPIPE when a peer resets mid-reply

2 participants