UCP/RNDV: keep the rkey when a put completion restarts on failover - #4
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Release builds compile out the
ucs_assertat the top ofucp_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 onlystatus != OK && FAILOVERwould 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_completeonly decrements the flush count,ucp_request_complete_senddoes 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_restartre-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, andresetre-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 thanUCS_OK/UCS_ERR_CANCELEDto mean "cannot resume", and restart aborts the request instead of trippingucs_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:320releases the rkey and then calls the restart-capable completion — safe only because the status is hardcodedUCS_OK. One refactor away from the identical crash; the new backstop would catch it.proto_rndv.c:995releases 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.ucp_proto_rndv_recv_completerather than the restart hook, so they were never exposed.Test
libucpbuilds 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-gtestbuild, andkill_put_send/kill_put_recvon the two-pod harness is the real validation. Happy to run that before merge if you would rather gate on it than on review.