Skip to content

UCP/RNDV: keep the rkey when a put completion restarts on failover - #4

Merged
wilsonliu-b10 merged 2 commits into
masterfrom
wilson/rndv-put-failover-rkey
Aug 13, 2026
Merged

UCP/RNDV: keep the rkey when a put completion restarts on failover#4
wilsonliu-b10 merged 2 commits into
masterfrom
wilson/rndv-put-failover-rkey

Conversation

@wilsonliu-b10

@wilsonliu-b10 wilsonliu-b10 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

A production worker segfaulted inside the UCXX progress thread during a RoCE fabric event: a rail send failed, the rendezvous PUT completed with an error status, and the FAILOVER restart dereferenced a released remote key.

The bug

ucp_proto_rndv_put_common_complete()
  → ucp_proto_rndv_rkey_destroy(req)          // ends with req->send.rndv.rkey = NULL
  → ucp_proto_rndv_request_zcopy_complete()
      → ucp_proto_request_zcopy_complete_cb()
          status != OK && FAILOVER && !FAILED → ucp_proto_request_restart(req)
              → ucp_proto_rndv_put_common_send() → ucp_rkey_get_tl_rkey(NULL) → segfault

Release builds compile out the ucs_assert at the top of ucp_proto_rndv_rkey_destroy, so this surfaces as a clean NULL dereference rather than an assert.

The put scheme was admitted to failover on the reasoning that its completion already routed through the hook carrying the restart, so no new restart code was needed. That skipped checking what the caller does before invoking the hook. The get scheme had it right and documents why the rkey must survive a restart — but the condition was open-coded in each scheme, so the put path could be written without it.

The fix

Keep the rkey when the completion is going to restart rather than complete.

The guard has to match the restart condition exactly: status != OK && FAILOVER && !(ep->flags & UCP_EP_FLAG_FAILED). Guarding on only status != OK && FAILOVER would stop the crash but skip the release when the endpoint is failed — where the hook completes rather than restarts, and nothing else frees the key (ucp_request_rndv_flush_complete only decrements the flush count, ucp_request_complete_send does not touch it). That would leak the rkey on precisely the endpoint-death path that produces these incidents.

So the predicate is hoisted into ucp_proto_request_is_failover_restart() and asked by all three sites: the hook that performs the restart, and both schemes that must not release what a restart still needs. One predicate means they cannot drift apart again — which is the actual reason this bug existed.

A backstop, because the invariant is subtle

ucp_proto_request_restart re-enters protocol selection, so the request must still own whatever the newly selected protocol will use. The rule is not "never clean up before restarting" — the get scheme deliberately deregisters its datatype iterator first, and reset re-registers it. The rule is release only what the restart can re-acquire, and the peer's rkey, which only the RTS could supply, is the one thing it cannot.

Nothing stated or enforced that. So reset() may now return a status other than UCS_OK/UCS_ERR_CANCELED to mean "cannot resume", and restart aborts the request instead of tripping ucs_assertv_always. Both rndv zcopy protocols answer it by refusing to restart without their remote key.

That assert fired in every build, so this trades a crash for a failed transfer the caller already knows how to retry. With the guard above in place the refusal should never fire; it exists for the next protocol admitted to failover.

Adjacent sites, deliberately unchanged

  • rndv_rkey_ptr.c:320 releases the rkey and then calls the restart-capable completion — safe only because the status is hardcoded UCS_OK. One refactor away from the identical crash; the new backstop would catch it.
  • proto_rndv.c:995 releases the request ID and then passes a live status through. Restart allocates a fresh ID, so I believe it is fine, but it is the same shape and I have not proven it.
  • The RTR paths NULL-guard their release and complete through ucp_proto_rndv_recv_complete rather than the restart hook, so they were never exposed.

Test

libucp builds clean on this base and on the b300 bed's tree. Not yet exercised against a live NIC kill on the put scheme: the failover gtest needs a separate --enable-gtest build, and kill_put_send/kill_put_recv on the two-pod harness is the real validation. Happy to run that before merge if you would rather gate on it than on review.

Admitting the put scheme to failover routed its completion through
ucp_proto_request_zcopy_complete_cb, which restarts the request when the
status is an error and the endpoint is still recovering. But
ucp_proto_rndv_put_common_complete released the remote key before calling
it, and ucp_proto_rndv_rkey_destroy ends with rkey = NULL, so the
restarted write dereferenced a NULL rkey in ucp_rkey_get_tl_rkey and
segfaulted. In a debug build it trips the assert at the top of
ucp_proto_rndv_rkey_destroy instead.

The get scheme already had this right - it keeps the rkey across a
restart because the remote buffer stays registered until ATS - but the
condition was open-coded in each place, so the put path could be written
without it. Hoist the predicate into
ucp_proto_request_is_failover_restart() and have all three sites ask it:
the completion hook that performs the restart, and the two schemes that
must not release resources the restarted send still needs.

The RTR paths already guard their rkey release with a NULL check and
complete through ucp_proto_rndv_recv_complete rather than the restart
hook, so they were never exposed.
ucp_proto_request_restart() re-enters protocol selection, so the request
must still own everything the newly selected protocol will use. reset()
can rewind a datatype iterator but cannot resurrect a released remote
key, and nothing said so - which is how the put scheme came to release
its rkey before a completion that restarts.

Give the protocol a way to say it cannot resume: a reset status other
than UCS_OK or UCS_ERR_CANCELED now aborts the request rather than
tripping ucs_assertv_always. The assert fired in every build, so this
replaces a crash with a failed transfer that the caller already knows
how to retry. Both rndv zcopy protocols answer it by refusing to restart
without their remote key.

This is the backstop, not the fix - the rkey is now kept across a
restart, so the refusal should never fire. It exists because the
invariant is subtle enough to be missed again: a restart REQUIRES some
cleanup to have happened (the get scheme deregisters its datatype
iterator on purpose, and reset re-registers) while forbidding other
cleanup (the peer's rkey, which only the RTS could provide). Release
only what the restart re-acquires is not something the next protocol
admitted to failover can infer, and asserts are no help in the release
builds where this surfaced as a segfault.
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.

1 participant