Repository navigation
fix(serve): decouple the SMD orchestrator entrypoint from the container's sagemaker (#6200) - #6534
Conversation
…er's sagemaker Deploying a CustomOrchestrator with ModelBuilder fails its container health check at startup with `AttributeError: 'NoneType' object has no attribute 'encode'`, so the Inference Component never reaches InService. The generated `code/inference.py` did `from sagemaker.serve.validations. check_integrity import perform_integrity_check`, which resolves against the *container's* installed sagemaker rather than the SDK that produced the artifacts. `sagemaker-distribution.json` maps SMD inference to 3.2.0 only, and that image bundles sagemaker 2.245.0, whose `perform_integrity_check` still reads `SAGEMAKER_SERVE_SECRET_KEY` and calls `.encode()` on it. The host no longer sets that variable, and must not: the secret key was removed on purpose in aws#5654 / aws#5656 as the fix for CVE-2026-8596, where it was exposed in plaintext through the Describe APIs. So the missing `return` in `prepare_for_smd()` is not the defect, and restoring it would reintroduce the vulnerability -- the version coupling is the defect. Inline the digest comparison in the entrypoint instead. It is the same plain SHA-256 check against `metadata.json` that the host writes, so the integrity posture is unchanged, but the script no longer imports anything from sagemaker and therefore cannot drift against whatever version the image carries. A second, independent mismatch sat behind the first: `_save_model_inference_ spec` pickled `(inference_spec, schema_builder)`, and a v3 SchemaBuilder pickles with references to `sagemaker.core.*` serializer modules that a 2.x install cannot import, so `cloudpickle.load` failed even once the integrity check passed. The orchestrator entrypoint discards the second element of that tuple, so pickle `(inference_spec, None)` for custom orchestrators. Also drop the six `self.secret_key = prepare_for_*(...)` assignments and their `# fix out of scope` / pylint-disable comments, and correct the `-> str` annotations to `-> None` on all four `prepare_for_*` functions. Those functions record a digest and have had nothing to return since the CVE fix; the lying annotation is what led two earlier PRs (aws#6221, aws#6222) to try to resurrect the secret key. Every builder already initializes `self.secret_key = ""` before calling them, so this is not a behavior change. Verified by generating the artifacts with this branch and executing the resulting `inference.py` under a sagemaker 2.245.0 interpreter -- the version bundled in the image the SDK selects. Before: the reported traceback. After: the orchestrator loads and serves. Tampered `serve.pkl` and missing `metadata.json` are still rejected. Closes aws#6200
There was a problem hiding this comment.
🤖 AI code review
This PR fixes the CustomOrchestrator SMD health-check failure (issue #6200) by decoupling the container entrypoint from the container's bundled sagemaker: it inlines a plain SHA-256 integrity check into custom_execution_inference.py (no more from sagemaker... import), pickles (inference_spec, None) for custom orchestrators so a v3 SchemaBuilder can't drag in unimportable sagemaker.core modules, and corrects the prepare_for_* return annotations to -> None while dropping the dead self.secret_key = prepare_*(...) assignments. I verified the inlined check matches perform_integrity_check (reads sha256_hash from _MetaData.to_json(), uses hmac.compare_digest), that SAGEMAKER_INFERENCE_CODE_DIRECTORY is always set to /opt/ml/model/code matching the new default, that every builder initializes self.secret_key = "" before the prepare call (so behaviour is unchanged since the functions always returned None), and that the SMD entrypoint is the only consumer of the custom-orchestrator serve.pkl and discards the second tuple element. The change is correct, consistent with the team precedent in #6200 (do not reintroduce SAGEMAKER_SERVE_SECRET_KEY), and well covered by new unit tests. No blocking issues found.
Reviewed commit 5f0a651. Automated review; verify before acting.
Coverage could not attribute the entrypoint's lines when the test exec'd its source text, so the new code read as unhit (codecov/patch 64%) despite every line being exercised. The module reads its code directory from SAGEMAKER_INFERENCE_CODE_DIRECTORY, so it can be imported for real against a prepared temp dir and reloaded per case, which is both measurable and closer to how the model server loads it. Also cover the awaited branch of handler() with an AsyncCustomOrchestrator; custom_execution_inference.py is now at 100%.
|
Pushed a follow-up commit addressing
|
There was a problem hiding this comment.
🤖 AI code review
This PR fixes the CustomOrchestrator/SMD container health-check failure (#6200) by decoupling the generated code/inference.py from the serving container's bundled sagemaker. It inlines the plain SHA-256 integrity check (identical to the current check_integrity.perform_integrity_check), routes both file reads through SAGEMAKER_INFERENCE_CODE_DIRECTORY with the prior default, pickles custom orchestrators as (inference_spec, None) (the entrypoint already discards the second element), and corrects the prepare_for_* annotations to -> None while dropping the dead self.secret_key = prepare_*(...) assignments. I verified the integrity semantics match, that self.secret_key = "" is set before every affected prepare call (so no behavior change), that CustomOrchestrators only reach the SMD path, and that the new tests cover the key regressions. The change is correct, backward-compatible, and well-tested; no blocking issues found. The PR body explains the author's reasoning but does not attempt to subvert the review.
Reviewed commit a6be1b7. Automated review; verify before acting.
Description
Deploying a
CustomOrchestratorwithModelBuilderfails its container health check at startup withAttributeError: 'NoneType' object has no attribute 'encode', so the Inference Component never reachesInService.The missing
returninprepare_for_smd()is not the defect. The secret key was removed on purpose in #5654 / #5656 as the fix for CVE-2026-8596, whereSAGEMAKER_SERVE_SECRET_KEYwas exposed in plaintext through the Describe APIs. The host side is consistent with that today —check_integrity.compute_hash(buffer)is a plain SHA-256 and no key is set — so restoring the return value would reintroduce the vulnerability. This is why I did not take the approach of the two earlier attempts at this issue (#6221, #6222).The actual defect is a version coupling between the host and the container. The generated
code/inference.pydid:which resolves against the container's installed
sagemaker, not the SDK that produced the artifacts.sagemaker/core/image_uri_config/sagemaker-distribution.jsonmaps SMD inference to3.2.0only, and that image bundles sagemaker 2.245.0, whoseperform_integrity_checkstill doesos.environ.get("SAGEMAKER_SERVE_SECRET_KEY").encode().A second, independent mismatch sits behind the first:
_save_model_inference_specpickled(inference_spec, schema_builder), and a v3SchemaBuilderpickles with references tosagemaker.core.*serializer modules that a 2.x install cannot import, socloudpickle.loadin the entrypoint failed even once the integrity check passed.Changes Made
model_server/smd/custom_execution_inference.py— inlined the digest comparison. It is the same plain SHA-256 check against themetadata.jsonthe host writes, so the integrity posture is unchanged, but the script no longer imports anything fromsagemakerand therefore cannot drift against whatever version the image carries. Both file reads now go throughSAGEMAKER_INFERENCE_CODE_DIRECTORY(always set by_upload_smd_artifacts) with the previous/opt/ml/model/codeas the default, which also makes the module testable.model_builder.py— custom orchestrators pickle as(inference_spec, None). The entrypoint already discards the second element of that tuple, so theSchemaBuilderwas dead weight that coupledserve.pklto the host SDK's module layout.prepare_for_smd/prepare_for_torchserve/prepare_for_mms/prepare_for_tf_serving— corrected the-> strannotations to-> None, and dropped the sixself.secret_key = prepare_for_*(...)assignments along with their# NOTE: ... fix out of scope/pylint: disable-next=assignment-from-no-returncomments. These functions record a digest and have had nothing to return since the CVE fix; the lying annotation is what led both earlier PRs to try to resurrect the secret key. Every builder already initializesself.secret_key = ""before calling them, and bothNoneand""are falsey at every consumer, so this is not a behavior change.Files
sagemaker-serve/src/sagemaker/serve/model_server/smd/custom_execution_inference.pysagemaker-serve/src/sagemaker/serve/model_server/smd/prepare.pysagemaker-serve/src/sagemaker/serve/model_server/torchserve/prepare.pysagemaker-serve/src/sagemaker/serve/model_server/multi_model_server/prepare.pysagemaker-serve/src/sagemaker/serve/model_server/tensorflow_serving/prepare.pysagemaker-serve/src/sagemaker/serve/model_builder.pysagemaker-serve/src/sagemaker/serve/model_builder_servers.pysagemaker-serve/tests/unit/model_server/test_smd_custom_execution_inference.py(new)sagemaker-serve/tests/unit/model_server/test_smd_prepare.pysagemaker-serve/tests/unit/test_model_builder_build.pysagemaker-serve/tests/unit/servers/test_model_builder_servers.pyTesting
End-to-end against the image the SDK actually selects. I generated the artifacts with this branch (
_save_model_inference_spec+prepare_for_smd, no AWS calls) and then executed the resultingcode/inference.pyunder a Python 3.12 interpreter withsagemaker==2.245.0installed — the version bundled insagemaker-distribution-prod:3.2.0-cpu— the way the model server imports it:/pingnever becomes healthy.import-time preflight + unpickle: OK, andhandler(b'hello') -> {'echo': b'hello'}, with noSAGEMAKER_SERVE_SECRET_KEYpresent.SchemaBuilderraisesModuleNotFoundError: No module named 'sagemaker.core'.serve.pkl→Integrity check for the serialized function or data failed.; removingmetadata.json→Path to metadata.json does not exist.Unit tests. 15 new tests, including an AST-level guard asserting the entrypoint imports nothing from
sagemaker— all 11 tests in the new file fail against the unfixed source. Fullsagemaker-serve/tests/uniton this branch: identical failure set to themasterbaseline (49 → 49, zero new); the pre-existing failures are credential/network-dependent.black,flake8andpydocstyleare clean, andpylinton the changed modules reports only the pre-existingno-else-returninhandler.Follow-ups not in this PR
sagemaker-distribution.jsonmaps SMD inference to3.2.0only, while public ECR servessagemaker-distributionup to4.4.2today. Bumping it is worth doing on its own merits, but it needs the per-regionsagemaker-distribution-prodregistry account IDs for the new version, which I cannot verify from outside. It is not a prerequisite for this fix — the point of inlining the check is that the currently-mapped image now works.Mode.LOCAL_CONTAINERcannot exercise SMD.LocalContainerMode.create_serverhas no branch forModelServer.SMDandLocalEndpoint.invokeraises "Unsupported model server", so none of this class of failure is catchable before deploying.Thanks to @deburky, whose analysis on #6200 correctly identified the host/container skew and ruled out restoring the secret key.
Related Issue
Closes #6200
Merge Checklist
prefix: descriptionformat