Skip to content

fix(replication): pin kGetObj response host to the requesting peer - #113

Closed
g-husam wants to merge 2 commits into
mainfrom
fix/get-obj-ssrf-dest-address
Closed

g-husam wants to merge 2 commits into
mainfrom
fix/get-obj-ssrf-dest-address

Conversation

@g-husam

@g-husam g-husam commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The kGetObj handler in TransferService used the client-supplied dest_address from the ObjInfoHeader directly as the target for the kRespondToGetObj connection. An unauthenticated client could therefore make the service connect() to an arbitrary host:port and stream the contents of an arbitrary local file (source_obj_id) to it — a Server-Side Request Forgery with file exfiltration.

This change pins the response host to the peer that actually sent the request:

  • HandleGetObjRequest now resolves the callback address via a new file-local helper, ResolveGetCallbackAddress(), which takes the host from getpeername(client_fd) and only the port from header.dest_address.
  • The port is parsed with absl::SimpleAtoi and range-checked (1..65535); a malformed dest_address is rejected with a kError response before the ACK is sent, so the service never attempts a connection for a bad request.
  • No wire-format, AsyncGet/AsyncPut API, or Python changes. For legitimate peers (which always advertise their own local_address_), behaviour is unchanged.

Also fixed: shutdown crash with an in-flight RespondToGetTask

While stress-testing, a pre-existing crash surfaced: HandleGetObjRequest registers RespondToGetTasks in pending_tasks_ with a nullptr promise, and Shutdown() unconditionally called promise->set_exception(...) on every pending task. Shutting down while a RespondToGetTask was still connecting back segfaulted. Shutdown() now skips promise-less entries (2 lines). Covered by ShutdownWithInFlightRespondToGetTaskIsSafe, which was confirmed to segfault without the guard.

Tests

New tests in transfer_service_p2p_test.cpp (raw-socket helpers drive the service directly):

Test What it checks
SpoofedDestHostTest.GetResponseGoesBackToRequester (×8) Spoofed hosts (203.0.113.1, 10.0.0.1, 169.254.169.254, 0.0.0.0, 255.255.255.255, not-a-host, 127.0.0.1, empty) are ignored; the kRespondToGetObj arrives at the requester's real address. Fails without the fix.
InvalidDestAddressTest.GetRequestIsRejected (×10) Missing/empty/zero/out-of-range/non-numeric ports → kError, no ACK, no connection attempt.
GetRequestRejectionKeepsConnectionUsable A rejected request doesn't poison the socket; a following valid request is served.
GetRequestWithUnterminatedDestAddressIsRejected 64 non-NUL bytes in dest_address are handled safely and rejected; service stays responsive.
ConcurrentSpoofedGetRequestsAllReturnToRequester 4 concurrent clients spoofing different hosts all get responses at the real requester address.
GetSucceedsWhenRequesterAdvertisesUnreachableIp Public-API happy path: a requester advertising an unroutable IP still completes a Get over loopback.
ShutdownWithInFlightRespondToGetTaskIsSafe Regression test for the shutdown crash above.

Flake check

  • New tests: 10× shuffled → 220/220 pass; concurrent test 50× → 50/50.

  • Full transfer_service_test binary (excluding MLFLogSinkTest, which has a pre-existing in-process-only absl SetTimeZone abort and passes under ctest): 5× shuffled → 470/470 pass.

  • pytest tests/replication/test_transer_service.py → 6/6 pass against the rebuilt extension.

  • Tests pass

  • Appropriate changes to documentation are included in the PR (N/A — no user-facing behaviour change)

The kGetObj handler used the client-supplied dest_address verbatim as
the target for the kRespondToGetObj connection, letting an unauthenticated
client make the service connect to an arbitrary host:port and send it the
contents of an arbitrary local file (SSRF / file exfiltration).

The response host is now always taken from getpeername() of the socket the
request arrived on; only the port is read from the header, and it is
validated before the request is ACKed. Add regression tests.
…n crash

Expand the dest_address tests into parametrized spoofed-host and invalid-
port suites, plus edge cases: rejection keeps the connection usable, an
unterminated 64-byte dest_address, concurrent spoofed requests, and a
public-API happy path where the requester advertises an unreachable IP.

Also guard Shutdown() against RespondToGetTasks, which are registered in
pending_tasks_ without a promise; shutting down while one was in flight
dereferenced a nullptr. Covered by a new regression test.
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Python Code Coverage Summary

Code Coverage

Package Line Rate Branch Rate Health
src.ml_flashpoint 100% 100% ✔
src.ml_flashpoint.adapter 100% 100% ✔
src.ml_flashpoint.adapter.megatron 97% 95% ✔
src.ml_flashpoint.adapter.nemo 98% 94% ✔
src.ml_flashpoint.adapter.pytorch 99% 92% ✔
src.ml_flashpoint.checkpoint_object_manager 93% 93% ➖
src.ml_flashpoint.core 95% 92% ✔
src.ml_flashpoint.replication 83% 83% ❌
Summary 95% (2396 / 2524) 92% (573 / 624) ➖

Minimum allowed line rate is 90%

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

C++ Code Coverage Summary

Code Coverage

Package Line Rate Branch Rate Health
src.ml_flashpoint.checkpoint_object_manager.buffer_object 93% 54% ✔
src.ml_flashpoint.replication.transfer_service 79% 42% ❌
Summary 82% (916 / 1116) 44% (698 / 1573) ➖

Minimum allowed line rate is 80%

@g-husam g-husam closed this Oct 6, 2026
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