Fix SageMaker MME cross-model invoke/unload via X-Amzn-SageMaker-Target-Model header (TRI-1563) - #8923
Conversation
…ader (TRI-1563) Validate X-Amzn-SageMaker-Target-Model against the target_model registered for the URL hash and reject mismatches with 400.
Asserts invoke and unload reject a mismatched X-Amzn-SageMaker-Target-Model header with 400 and leave the other model loaded.
Greptile SummaryThe PR binds SageMaker MME invoke and unload operations to the model identity recorded during load, rejecting contradictory target-model headers.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[MME request URL hash] --> B[Lookup registered model]
B --> C{Target-Model header present?}
C -- No --> E[Use registered target model]
C -- Yes --> D{Header matches registered identity?}
D -- No --> F[Return HTTP 400]
D -- Yes --> E
E --> G[Invoke or unload registered model]
Reviews (5): Last reviewed commit: "review: address MME target-model review ..." | Re-trigger Greptile |
…TRI-1563) Per review: the PoC accepted any status >= 400; assert the specific 400 and the mismatch error payload so the test proves the identity-check branch ran.
… year The sys.path-append-before-imports pattern (pre-existing since 2022) trips flake8 E402 once the PR touches the file; suppress with # noqa: E402 and update the copyright year to 2026.
…psirt-trtllm-multi-model-identity-confusion-sagemaker # Conflicts: # qa/L0_sagemaker/test.sh
| EVBufferAddErrorJson(req->buffer_out, err); | ||
| evhtp_send_reply(req, EVHTP_RES_BADREQ); /* 400 */ | ||
| TRITONSERVER_ErrorDelete(err); |
There was a problem hiding this comment.
Can use macro HTTP_RESPOND_IF_ERR
There was a problem hiding this comment.
Good call — switched both the invoke and unload rejection paths to HTTP_RESPOND_IF_ERR. Same 400 + JSON body, a lot less boilerplate. Done.
| /* Use model name hash as the key, as expected in the SageMaker MME | ||
| * contract, and remember the target_model the repository was actually | ||
| * loaded under so invoke/unload can validate the request header against it | ||
| * (TRI-1563). */ |
There was a problem hiding this comment.
I wouldn't put ticket number unless there is future work.
There was a problem hiding this comment.
Removed the ticket number here and in the two related comments. Done.
| struct SageMakerModelInfo { | ||
| // Parent path of the registered model repository | ||
| // (e.g. /opt/ml/models/<hash>). Needed to unregister the repo on unload. | ||
| std::string repo_path; |
There was a problem hiding this comment.
Made both repo_path and target_model const — they're set once at load and only read after. Done.
| "Expected status code 404, received {}".format(r.status_code), | ||
| ) | ||
|
|
||
| def test_sm_5b_identity_confusion_poc(self): |
There was a problem hiding this comment.
sm = sagemaker; 5b just orders it right after test_sm_5_model_unload (unittest runs tests lexicographically). Renamed to test_sm_5b_target_model_header_mismatch with a docstring so it's self-explanatory, and dropped "poc" — it's a permanent regression test now, not a one-off.
| import tritonclient.http as httpclient | ||
| import numpy as np # noqa: E402 | ||
| import requests # noqa: E402 | ||
| import test_util as tu # noqa: E402 |
There was a problem hiding this comment.
Move sys.path.append("../common") before this line for fewer # noqa: E402.
There was a problem hiding this comment.
Done — moved the stdlib/third-party imports above sys.path.append("../common") so only test_util needs # noqa: E402 now (8 → 1). Matches the existing pattern in e.g. L0_cuda_graph/trt_cuda_graph_test.py.
Use HTTP_RESPOND_IF_ERR macro, const struct members, drop ticket refs, rename test_sm_5b, reduce noqa. No behavior change.
Summary
The SageMaker multi-model endpoint (MME) adapter authorized invoke and unload
requests using the model identity in the URL (
/models/<hash>), but performedthe actual inference / unload using the
X-Amzn-SageMaker-Target-Modelheader,without verifying the two referred to the same model. A request scoped to one
loaded model could therefore invoke or unload a different loaded model, and the
unload path erased the URL-hash registry entry while unloading the header-selected
model — leaving runtime and repository state inconsistent.
This is a confused-deputy / incorrect-authorization issue (CWE-441 / CWE-863). In
multi-tenant or shared-model deployments it breaks tenant isolation (cross-model
inference, cross-model unload/DoS) and corrupts model lifecycle bookkeeping.
Root cause
The registry (
sagemaker_models_list_) stored onlyhash → repo_path, discardingwhich model each hash was actually loaded as. At invoke/unload time there was
nothing to validate the header against, so the header was trusted as the action
target.
Fix — bind identity at load, validate on use
repo_pathto{ repo_path, target_model }(new
SageMakerModelInfo), recording the model each hash was loaded under.target_model, never on theheader. If the request carries an
X-Amzn-SageMaker-Target-Modelheader thatdoes not match the registered value, reject with
400.and repository state stay consistent.
Legitimate traffic is unaffected: SageMaker sets the header consistently with the
loaded model (or omits it), so the mismatch path only triggers on a contradictory
request.
Changes
src/sagemaker_server.{h,cc}— the fixqa/L0_sagemaker/sagemaker_multi_model_test.py— regression test(
test_sm_5b_identity_confusion_poc): mismatched header on invoke and unloadmust return
400, and the other model must remain loadedqa/L0_sagemaker/test.sh— bump expected MME test count 7 → 8Testing
L0_sagemakerpasses (8/8), including the new regression test. Verified end-to-endon the internal CI pipeline (build +
L0_sagemakergreen).Pipeline ID: 61910130