Skip to content

UCT/TCP: Validate AM receive length - #12081

Merged
roiedanino merged 3 commits into
openucx:masterfrom
roiedanino:fix/tcp-am-rx-length-6069355
Oct 18, 2026
Merged

roiedanino merged 3 commits into
openucx:masterfrom
roiedanino:fix/tcp-am-rx-length-6069355

Conversation

@roiedanino

Copy link
Copy Markdown
Contributor

What?

Validate UCT TCP active-message lengths at runtime and disconnect peers that
advertise a payload larger than the receive segment. Add focused regression
coverage for an oversized wire header.

Why?

Release builds disable assertions. A malformed length could therefore be
retained and used to size a later socket receive beyond the RX buffer.

How?

Perform the length check as soon as a complete active-message header is
available, reset the RX context, and use the existing TCP disconnect path on
failure.

Validate wire-provided AM lengths at runtime before retaining the header for a later receive. Disconnect peers that advertise a payload larger than the configured receive segment.\n\nAdd a raw TCP regression that sends the protocol magic and an oversized AM header.
@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

@roiedanino roiedanino self-assigned this Oct 13, 2026
@roiedanino
roiedanino requested a review from gleon99 October 13, 2026 14:41
Comment thread src/uct/tcp/tcp_ep.c Outdated
uct_tcp_ep_handle_invalid_am_length(uct_tcp_ep_t *ep, uint32_t length,
size_t max_length)
{
ucs_debug("tcp_ep %p (state %s): received AM length %u exceeds maximum %zu",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ucs_debug hides this entirely in release runs. Peers do not exchange segment sizes, so a peer configured with a larger RX_SEG_SIZE will legitimately hit this and the user only sees a connection reset with no hint. Can we use ucs_diag here? The connection is reset right after, so there is at most one message per connection.

Comment thread src/uct/tcp/tcp_ep.c Outdated
"tcp_ep %p (conn state - %s): %u vs %zu",
ep, uct_tcp_ep_cm_state[ep->conn_state].name, hdr->length,
(iface->config.rx_seg_size - sizeof(*hdr)));
if (ucs_unlikely(hdr->length >

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

hdr->length is still only ucs_assert-ed for the internal AM IDs further down (UCT_TCP_EP_PUT_REQ_AM_ID, UCT_TCP_EP_PUT_ACK_AM_ID, uct_tcp_cm_handle_conn_pkt). A crafted header passes this new check and in release builds uct_tcp_ep_handle_put_req() then memcpy's to put_req->addr. Should those lengths be validated here as well, or is that intentionally out of scope?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intentionally out of scope for this change. This PR prevents an oversized AM length from being retained and used to size a later receive beyond the RX buffer. Exact semantic validation of internal PUT and CM messages is a separate parser-hardening issue that needs event-specific checks and focused tests.

@@ -259,6 +259,30 @@ UCS_TEST_P(test_uct_tcp, listener_flood_connect_and_close) {
test_listener_flood(*m_ent, max_conn, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Residual coverage gap: the new test only covers the single-header case in ACCEPTING state. The partial-message path (remaining < sizeof(*hdr) + hdr->length followed by a second uct_tcp_ep_recv()), which is the one that actually overflowed before this fix, is not directly exercised; it is only covered indirectly because the check now runs before that exit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The test intentionally sends only the oversized header. Without the fix it enters the partial-message path and subsequent progress calls attempt the second receive, but no payload is sent, so it fails safely by leaving the endpoint active instead of corrupting memory. The exact second-receive overwrite was reproduced separately under ASan. With the fix, every complete header is rejected before it can be retained, independent of TCP segmentation.

@@ -259,6 +259,30 @@ UCS_TEST_P(test_uct_tcp, listener_flood_connect_and_close) {
test_listener_flood(*m_ent, max_conn, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Residual coverage gap: no coverage for a TX-capable EP hitting this path (error-handler flow via uct_iface_handle_ep_err), only the RX-only destroy-from-progress flow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The new check only selects the existing uct_tcp_ep_handle_disconnected() path. The TX-capable branch and uct_iface_handle_ep_err() are existing peer-failure plumbing covered by the generic peer-failure tests, while this focused regression covers the new oversized-header decision and the RX-only destruction path. I do not think a duplicate TX-capable fixture is needed here.

Comment thread test/gtest/uct/tcp/test_tcp.cc Outdated

UCS_TEST_P(test_uct_tcp, reject_oversized_am)
{
const uint64_t magic_number = UCT_TCP_MAGIC_NUMBER;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

constexpr?

@svc-ucx

svc-ucx commented Oct 13, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Tests gpu on worker 0) · commit 2a7a1c8c

TL;DR: One gtest failed — shm_ib/test_ucp_proto_am_rndv.am_explicit_scheme_disables_force_provenance/0 — because at 1 MB CUDA the selected AM protocol was am/egr/multi/bcopy instead of am/rndv, so no rndv remote proto config existed; this is a CUDA/rndv protocol-selection test that is environment-dependent and cannot be affected by this PR's TCP-only change, so re-run and harden/guard the test rather than chase the TCP patch.

Full analysis

Summary: make test in Tests gpu on worker 0 exited 1 with 1 failed test out of 10428: shm_ib/test_ucp_proto_am_rndv.am_explicit_scheme_disables_force_provenance/0 (GetParam() = shm,ib,cuda_copy,rocm_copy).

Root cause: From the log:

test/gtest/ucp/test_ucp_proto.cc:439: Expected equality of these values:
  "am/rndv"  vs  thresh->proto_config.proto->name  Which is: "am/egr/multi/bcopy"
test/gtest/ucp/test_ucp_proto.cc:455: Expected: (nullptr) != (remote_proto_config)

The test runs with RNDV_PIPELINE_SHM_CUDA_STAGING_FORCE=y + RNDV_SCHEME=get_zcopy, then asks protocol selection for UCP_OP_ID_AM_SEND, CUDA memory, sys_dev=UNKNOWN, flags=REGISTRABLE only, at 1 MB (test_ucp_proto.cc:404-446). Because RNDV_SCHEME is explicit, every am/rndv ctrl variant whose remote proto isn't rndv/get/zcopy reports UCS_MEMUNITS_INF (src/ucp/rndv/proto_rndv.c:505-518, proto_rndv.inl:168-182). On this worker the rndv/get/zcopy remote variant was evidently not viable for CUDA with those estimated memory flags / sys_dev, so all am/rndv variants were INF and the unrestricted eager protocol (am/egr/multi/bcopy) won — the ASSERT_NE(nullptr, remote_proto_config) at line 455 is just the cascade. The test has no guard for "am/rndv not available on this machine", unlike its RMA sibling which restricts PROTOS to put/rndv,get/rndv,rndv/* (test_ucp_proto.cc:315) so eager can never win, and unlike rndv_force_cuda_send_child_uses_tag_provenance which skips on a stub protocol (:724-726).

This is not caused by PR #12081: commit 2a7a1c8c ("UCT/TCP: Validate AM receive length") touches only src/uct/tcp/tcp_ep.c, and the failing variant's transport list is shm,ib,cuda_copy,rocm_copy — TCP is not loaded. All TCP tests in the run passed. The likely real influencers are the recent CUDA/rndv-selection changes: af8174b8 (Thomas Vegas, gdr_copy required_mem_flags = UCS_MEM_FLAG_GDR_CAPABLE, gdr_copy_md.c:126), abf206ae (Guy Ealey Morag, GPU‑NIC assignment applied to RMA/rndv lanes), deaa328e (Thomas Vegas, dmabuf MR-flag probing in ib_md.c) and [REDACTED:Hex High Entropy String] (#12041). These same test_ucp_proto CUDA-selection assertions already have a history of Azure-only selection discrepancies (PRs #11858/#11861/#11864 were temp branches created to diagnose exactly that).

Implicated commit: Not the PR commit 2a7a1c8c (Roie Danino) — that change is TCP-only. Most plausible contributors to CUDA rndv selection on this worker: af8174b8 (Thomas Vegas), abf206ae (Guy Ealey Morag), deaa328e (Thomas Vegas).

File: test/gtest/ucp/test_ucp_proto.cc:439 (and :455); selection logic at src/ucp/rndv/proto_rndv.c:505-518 and src/ucp/rndv/proto_rndv.inl:168-182

Suggested fix:

  1. Re-trigger the GPU job — this failure is unrelated to the TCP patch and should not block PR UCT/TCP: Validate AM receive length #12081.
  2. Make the test deterministic instead of relying on ambient protocol availability: in test_ucp_proto_am_rndv, either restrict protocols the way the RMA sibling does (modify_config("PROTOS", "am/rndv,rndv/*")) and/or add "RNDV_THRESH=0" so eager cannot beat am/rndv; and in select_am_rndv_remote_proto_config() replace the hard EXPECT_STREQ("am/rndv", ...) at line 439 with UCS_TEST_SKIP_R("am/rndv not selected (rndv/get/zcopy unavailable for CUDA)") when the selected proto is an eager protocol, mirroring the existing stub-protocol skip at :724-726.
  3. Separately, have the rndv owners confirm whether rndv/get/zcopy should still be reachable for CUDA with mem_flags = REGISTRABLE and sys_dev = UNKNOWN after af8174b8/abf206ae; if not, the test's mem_info needs UCS_MEM_FLAG_GDR_CAPABLE/a real sys_dev.

Related: PR #12081 (this PR, TCP-only); #12039, #11985, #12032, #12041 (recent CUDA/rndv selection changes); #11858, #11861, #11864 (prior Azure-specific test_ucp_proto CUDA selection diagnosis branches)

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 2078c2d3-2e3b-47a9-b149-d52655c8e62f in the triage console for the audit trail.

Report rejected oversized messages at diagnostic level and use a compile-time constant for the TCP magic value in the regression test.
@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/uct/tcp/tcp_ep.c Outdated
(iface->config.rx_seg_size - sizeof(*hdr)));
if (ucs_unlikely(hdr->length >
(iface->config.rx_seg_size - sizeof(*hdr)))) {
uct_tcp_ep_handle_invalid_am_length(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

minor: iface->config.rx_seg_size - sizeof(*hdr) is computed twice here; maybe derive it inside the helper from the ep's iface and drop the max_length parameter.

@svc-nvidia-pr-review

Copy link
Copy Markdown

Residual coverage gap: the new test only exercises the path where the EP is in ACCEPTING state with no TX capability, i.e. uct_tcp_ep_set_failed() takes the FAILED + oneshot-destroy branch. The TX-capable branch (uct_iface_handle_ep_err() with UCS_ERR_CONNECTION_RESET, plus uct_tcp_ep_purge()) is not covered, since reaching it from a raw socket requires driving the CM handshake. Worth noting, not worth blocking on.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

@roiedanino
roiedanino requested a review from gleon99 October 15, 2026 10:22
Comment thread src/uct/tcp/tcp_ep.c
uct_tcp_ep_handle_invalid_am_length(uct_tcp_ep_t *ep, uint32_t length,
size_t max_length)
{
ucs_diag("tcp_ep %p (state %s): received AM length %u exceeds maximum %zu",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

maybe use ucs_error() here? diag is below the default warn log level so it is not printed by default, and this path also triggers for a legit peer configured with a bigger UCX_TCP_RX_SEG_SIZE (the sender bounds AM zcopy by its own rx_seg_size), so the user would see only a connection reset without the reason.

Comment thread src/uct/tcp/tcp_ep.c
"tcp_ep %p (conn state - %s): %u vs %zu",
ep, uct_tcp_ep_cm_state[ep->conn_state].name, hdr->length,
(iface->config.rx_seg_size - sizeof(*hdr)));
if (ucs_unlikely(hdr->length > max_am_length)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

can we validate the length of the service AM ids here as well? hdr->length for PUT_REQ/PUT_ACK/CM is still only checked by ucs_assert, e.g. a PUT_REQ with length != sizeof(uct_tcp_ep_put_req_hdr_t) makes uct_tcp_ep_handle_put_req() read put_req->addr from stale buffer bytes and memcpy() to a garbage address in release builds.

@gleon99 gleon99 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍🏼

@roiedanino
roiedanino enabled auto-merge (squash) October 15, 2026 10:57
@svc-ucx

svc-ucx commented Oct 15, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Tests roce on worker 0) · commit aa7024ed

TL;DR: The gtest shard hung for the full 900 s watchdog in rc_mlx5/test_uct_event.am_with_fc/1 — an AM was sent on the RoCE loopback but never received, leaving the unbounded while (m_am_recv_count < m_am_send_count) progress(); loop spinning until the watchdog SIGABRT'd it. This is unrelated to PR #12081 (TCP-only changes); re-run the job, and harden measure_am_loop with a deadline so a lost AM fails fast instead of as a 15-minute watchdog abort.

Full analysis

Summary: "Tests roce on worker 0" (Azure build 139128) aborted: make -C test/gtest test died with Connection timed out - abort testing / signal 6 after the first test of shard 1, rc_mlx5/test_uct_event.am_with_fc/1 <rc_mlx5/mlx5_1:1>, hung.

Root cause: Hang, not slowness. The log shows [ RUN ] rc_mlx5/test_uct_event.am_with_fc/1 at 10:44:11.102 and then zero output until 10:59:11.079 — a single 15:00 gap that is exactly watchdog_timeout = 900. in test/gtest/common/test_helpers.cc:31. The watchdog fired (ADD_FAILURE() << strerror(ETIMEDOUT) << " - abort testing" at test_helpers.cc:57), then pthread_killed the watched thread, producing the backtrace.

The backtrace pins the hung spot precisely:

  • frame 10: test_uct_event::measure_am_loop() at test/gtest/uct/test_event.cc:105
  • frames 1–5: uct_rc_mlx5_iface_progress → uct_ib_mlx5_poll_cq

Line 105 is inside while (m_am_recv_count < m_am_send_count) { progress(); } — an unbounded busy-wait with no deadline. uct_ep_am_bcopy had already returned success (ASSERT_EQ at test_event.cc:76 passed), so an AM was posted on the rc_mlx5 (RoCE, mlx5_1:1, loopback between m_e1/m_e2) endpoint and its receive callback never ran — a dropped/never-completed AM, or an RC flow-control window that never got re-granted (this is the RC_FC_ENABLE~=y variant). The test then spins forever instead of failing.

This is almost certainly not caused by the PR: commit aa7024ed ("UCT/TCP: Reuse maximum AM length") and the other two commits on this branch (c6192742, 2a7a1c8c) touch only src/uct/tcp/*, while the hanging test runs over rc_mlx5/IB-mlx5 code paths that the branch does not modify.

Implicated commit: none for the hang itself — branch commits aa7024ed, c6192742, 2a7a1c8c (Roie Danino) are TCP-only and cannot reach uct_rc_mlx5_iface_progress. Infrastructure/flaky RoCE FC behaviour on agent swx-rain04.

File: test/gtest/uct/test_event.cc:104-106 (unbounded wait loop); watchdog at test/gtest/common/test_helpers.cc:31,57

Suggested fix:

  1. Re-run the "roce on worker 0" job — the failure is independent of this PR's diff and should be treated as flaky until reproduced.
  2. Harden the test so a lost AM reports a useful error instead of a 15-min watchdog abort: bound the loop in measure_am_loop, e.g.
    ucs_time_t deadline = ucs_get_time() + ucs_time_from_sec(60);
    while (m_am_recv_count < m_am_send_count) {
        progress();
        ASSERT_LT(ucs_get_time(), deadline) << "AM not received: sent="
            << m_am_send_count << " recv=" << m_am_recv_count;
    }
    (the same pattern already used elsewhere via uct_test::flush(deadline) in test/gtest/uct/uct_test.cc:775).
  3. If it recurs on rc_mlx5 with RC_FC_ENABLE=y, capture UCX_LOG_LEVEL=debug plus RC FC window state (fc_wnd, pending FC_HARD_REQ) on the hung run — note 409b3a4c "TEST/UCT/EVENT: Fix flow-control deadlock" shows this test has had FC deadlocks before.
  4. Do not raise the gtest/job time limit — the 900 s silence is a hang, so more wall time would only delay the same abort.

Related: PR #12081 (this PR, TCP AM receive length validation — unrelated to the failing transport); prior FC deadlock fix in this same test: commit 409b3a4c.

@roiedanino
roiedanino merged commit 413775a into openucx:master Oct 18, 2026
165 checks passed
roiedanino added a commit to roiedanino/ucx that referenced this pull request Oct 20, 2026
* UCT/TCP: Validate AM receive length

Validate wire-provided AM lengths at runtime before retaining the header for a later receive. Disconnect peers that advertise a payload larger than the configured receive segment.\n\nAdd a raw TCP regression that sends the protocol magic and an oversized AM header.

* UCT/TCP: Improve oversized AM diagnostics

Report rejected oversized messages at diagnostic level and use a compile-time constant for the TCP magic value in the regression test.

* UCT/TCP: Reuse maximum AM length

(cherry picked from commit 413775a)
@roiedanino
roiedanino deleted the fix/tcp-am-rx-length-6069355 branch October 20, 2026 12:15
roiedanino added a commit that referenced this pull request Oct 20, 2026
UCT/TCP: Validate AM receive length (#12081)

* UCT/TCP: Validate AM receive length

Validate wire-provided AM lengths at runtime before retaining the header for a later receive. Disconnect peers that advertise a payload larger than the configured receive segment.\n\nAdd a raw TCP regression that sends the protocol magic and an oversized AM header.

* UCT/TCP: Improve oversized AM diagnostics

Report rejected oversized messages at diagnostic level and use a compile-time constant for the TCP magic value in the regression test.

* UCT/TCP: Reuse maximum AM length

(cherry picked from commit 413775a)
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.

4 participants