[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M69AH6D20F70FK9DB32PW23M":3,"skill-same:01M69AH6D20F70FK9DB32PW23M":68},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":47,"repositoryHtmlUrl":48,"repositoryHomepageUrl":49,"ownerHtmlUrl":50,"ownerBlogUrl":51,"ownerDescription":52,"updatedAt":53,"install":54,"resources":67},"01M69AH6D20F70FK9DB32PW23M","contrib-op-shape-inference-memory-safety",null,"Audit and fix out-of-range output writes in ONNX Runtime operator shape-inference functions. Use when reviewing or fixing a contrib (or standard) op TypeAndShapeInference where a getNumOutputs() guard precedes a write to a higher output index - optional trailing outputs make a smaller output count schema-valid, so getOutputType(index) can run one past the declared outputs at Graph::Resolve.","microsoft\u002Fonnxruntime","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M69AGY0EW9AJXAH7HK223SQZ",22300,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fonnxruntime\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fcontrib-op-shape-inference-memory-safety\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"memory","contrib","op","shape","Safety","contrib op shape inference memory safety","memory safety","inference","run","graph","audit","operator","onnx","write","fix","valid","past","at","when","audit and fix","2026-11-10 10:12:30",false,"---\nname: contrib-op-shape-inference-memory-safety\ndescription: \"Audit and fix out-of-range output writes in ONNX Runtime operator shape-inference functions. Use when reviewing or fixing a contrib (or standard) op TypeAndShapeInference where a getNumOutputs() guard precedes a write to a higher output index - optional trailing outputs make a smaller output count schema-valid, so getOutputType(index) can run one past the declared outputs at Graph::Resolve.\"\n---\n\n# Contrib-Op Shape-Inference Output-Index Safety\n\nReusable method for finding and fixing the bug class where an operator's\n`TypeAndShapeInference` function guards an output write with `getNumOutputs() > N` but then\nwrites an output index **greater than** `N`. For a node that declares fewer outputs, the\nwritten index is past the end of the inference context's output vector.\n\n> **Scope**: schema-level shape inference in `onnxruntime\u002Fcore\u002Fgraph\u002Fcontrib_ops\u002F*.cc` and\n> `shape_inference_functions.cc`. This runs once during `Graph::Resolve` (model-load time),\n> **EP-agnostic** - there is no per-EP (CPU\u002FCUDA\u002FROCm) kernel duplicate of this code to\n> chase. Op *kernels* allocate outputs via the bounds-safe `OpKernelContext::Output(index)`\n> and are a separate concern.\n\n## 1. The pattern\n\n```cpp\n\u002F\u002F onnxruntime\u002Fcore\u002Fgraph\u002Fcontrib_ops\u002Fbert_defs.cc  (before)\npropagateElemTypeFromInputToOutput(ctx, 0, 0);\nif (ctx.getNumOutputs() > 1) {                       \u002F\u002F guard says \"> 1\"\n  propagateElemTypeFromInputToOutput(ctx, 0, 1);\n  propagateElemTypeFromInputToOutput(ctx, 0, 2);     \u002F\u002F but writes index 2\n}\n```\n\nThe guard `getNumOutputs() > 1` admits a node with **exactly 2 outputs** (indices 0, 1), yet\nthe body writes index **2**. The implication \"`> 1` ⇒ index 2 exists\" is false: `> 1` only\nguarantees indices 0 and 1.\n\n### Why a smaller output count is valid\n\nTrailing outputs declared `OpSchema::Optional` **lower `min_output`**. ONNX derives\n`min_output` = number of required outputs, `max_output` = total declared. The model checker\n(`checker::check_node`) only enforces `min_output \u003C= N \u003C= max_output`.\n\n| Op | Output decls | min \u002F max | A 2-output node? |\n|---|---|---|---|\n| `DecoderAttention` | out (req), new_key_cache (Opt), new_value_cache (Opt) | 1 \u002F 3 | passes checker |\n| `MultiHeadAttention` | out (req), present_key (Opt), present_value (Opt), qk (Opt) | 1 \u002F 4 | passes checker |\n| `DecoderMaskedMultiHeadAttention` | out (req) + 3 Optional | 1 \u002F 4 | passes checker |\n\nSo a node with `output=['out','present_key']` is schema-valid, passes the checker, and then\nreaches the index-2 write. **A passing checker is not a guarantee the index is in range.**\n\n## 2. The sink (why the write is not caught)\n\n```cpp\n\u002F\u002F onnxruntime\u002Fcore\u002Fgraph\u002Fgraph.cc  -  InferenceContextImpl\nconst TypeProto* getInputType(size_t index) const override {\n  return node_.InputDefs().at(index)->TypeAsProto();   \u002F\u002F .at()  -> bounds-checked\n}\nTypeProto* getOutputType(size_t index) override {\n  return &node_output_types_[index];                   \u002F\u002F operator[]  -> NOT bounds-checked\n}\n```\n\n- `node_output_types_` is sized to `node.OutputDefs().size()` in the `InferenceContextImpl`\n  ctor, so for a 2-output node it has 2 elements; `getOutputType(2)` returns one past the end.\n- `getInputType` uses `.at()` (would throw on a bad index); `getOutputType` uses raw\n  `operator[]` (no check) - the asymmetry is the root cause.\n- The call runs at `Graph::Resolve` → `InferAndVerifyTypeMatch` → `RunInferencing`. The\n  surrounding `ORT_TRY\u002FORT_CATCH(const std::exception&)` only catches *thrown*\n  `fail_shape_inference`; a raw out-of-range `operator[]` does not throw, so the catch does\n  not help.\n- Because this is schema-level inference, it is **EP-independent** - no CUDA\u002FROCm copy.\n\n## 3. Audit technique — always sweep siblings\n\nDo not stop at the reported function. Grep **every** shape-inference guard and compare its\nthreshold against the **highest output index written before the next guard**.\n\n```bash\ngit grep -n 'getNumOutputs' -- \\\n  onnxruntime\u002Fcore\u002Fgraph\u002Fcontrib_ops\u002F*.cc \\\n  onnxruntime\u002Fcore\u002Fgraph\u002Fcontrib_ops\u002Fshape_inference_functions.cc\n```\n\nFor each `if (ctx.getNumOutputs() > N)` block, find the largest `index` passed to\n`propagateElemTypeFromInputToOutput(ctx, _, index)` \u002F `updateOutputShape(ctx, index, _)` \u002F\n`getOutputType(index)` inside it. **Rule: the guard must require strictly more outputs than\nthe highest index written** (write index `k` ⇒ guard must ensure `getNumOutputs() > k`).\n\nCorrect exemplars already in the tree to copy:\n\n| Exemplar | Pattern | Why it is safe |\n|---|---|---|\n| `BaseGroupQueryAttention...` | `if (getNumOutputs() >= 3)` then writes idx 2 | guard covers highest index |\n| `PagedAttention...` | nested `> 1` + inner `if (getNumOutputs() != 3) fail_shape_inference` | fails before any write |\n| `EmbedLayerNormalizationShapeInference` | `> 2` then writes idx 2 | fixed by PR #28176 (precedent) |\n| `SkipLayerNormalizationShapeInference` | each idx `k` guarded by `> k` | per-index guard |\n\n> **Gotcha — conditional writes can hide a vacuous audit.** A write may sit behind an inner\n> condition (e.g. `hasInputShape(past_key_index)` before writing index 2). The site is still\n> a bug, but you can only *observe* it when that inner condition is also satisfied. Keep this\n> in mind both for the audit and for tests (§5).\n\n## 4. Fix patterns\n\n**Point fix (required): raise the guard to cover the highest index written.**\n\n```cpp\n\u002F\u002F before\nif (ctx.getNumOutputs() > 1) { ... writes idx 2 ... }\n\u002F\u002F after\nif (ctx.getNumOutputs() > 2) {  \u002F\u002F both present_key (idx 1) AND present_value (idx 2)\n  ...\n}\n```\n\nJustify the threshold with the op's output semantics. For these attention ops the two trailing\noutputs - `present_key` (idx 1) and `present_value` (idx 2) for `MultiHeadAttention`,\n`new_key_cache` \u002F `new_value_cache` for `DecoderAttention` (see the §1 table for each op's\nexact output names) - are a **both-or-neither pair**: there is no valid configuration that\nemits one without the other, so requiring all three outputs before populating indices 1 and 2\nis behavior-preserving. (`PagedAttention` encodes the same invariant via its nested `!= 3`\ncheck.)\n\n**Defense-in-depth (recommended): bound the sink** so a future author cannot reintroduce the\nclass.\n\n```cpp\n\u002F\u002F onnxruntime\u002Fcore\u002Fgraph\u002Fgraph.cc  -  InferenceContextImpl::getOutputType\nTypeProto* getOutputType(size_t index) override {\n  if (index >= node_output_types_.size()) {\n    fail_type_inference(\"output index \", index, \" is out of range; node has \",\n                        node_output_types_.size(), \" outputs\");\n  }\n  return &node_output_types_[index];\n}\n```\n\nThis mirrors `getInputType`'s `.at()` and the existing bounds checks in the sibling\n`DataPropagationContextImpl`. Placing it at the base layer transitively protects the NHWC and\nquantization wrapper contexts. After the point fix this branch is unreachable through a normal\nmodel (the guard already prevents the out-of-range index), so it is pure defense-in-depth. Its\nfailure mode is build-dependent: with exceptions enabled, `fail_type_inference` raises\n`InferenceError` (a `std::exception`), caught by the existing `ORT_CATCH(const std::exception&)`\naround `RunInferencing` and surfaced as a clean load-time error; under `ORT_NO_EXCEPTIONS` it is\n**not** compiled out - ONNX's no-exceptions path prints the message to `std::cerr` and calls\n`abort()`, a deterministic fail-fast (consistent with `getInputType`'s `.at()`, which likewise\nterminates under no-exceptions). Either way the result is a controlled failure rather than an\nout-of-range write.\n\n## 5. Test recipe\n\nTests live in `onnxruntime\u002Ftest\u002Fcontrib_ops\u002F*.cc` and are **auto-globbed** into the\n`onnxruntime_provider_test` target by `cmake\u002Fonnxruntime_unittests.cmake`\n(`test\u002Fcontrib_ops\u002F*.cc` pattern) - **no cmake edit needed** for a new file. See the\n`ort-test` skill for the executable taxonomy (`onnxruntime_provider_test` vs\n`onnxruntime_test_all`).\n\nRules that make the regression test actually guard the fix:\n\n1. **Drive through `Model` + `Graph::Resolve`**, not ONNX's standalone `TestShapeInference`.\n   Only the full resolve path constructs the real `InferenceContextImpl` and hits the\n   `getOutputType` sink described in §2. A standalone ONNX shape-inference helper uses a\n   different context and **bypasses** the sink, so it cannot reproduce the bug.\n2. **Negative tests must be NON-VACUOUS** - they must actually enter the write branch on\n   pre-fix source. If a write is gated by an inner condition (§3 gotcha), satisfy it: e.g. for\n   `MultiHeadAttention`\u002F`DecoderMaskedMultiHeadAttention`, supply a **shaped `past_key`**\n   (and `past_sequence_length` \u002F `past_present_share_buffer` as the op requires) so the\n   index-2 block runs. A negative test that only supplies `query` skips the block and passes\n   even on pre-fix source - regression-proof in name only.\n3. **Add positive (all-outputs) cases**: a node with every output present must still infer the\n   trailing output types - proves the tightened guard did not over-restrict.\n4. **Keep tests throw-free post-fix** so they are valid under `ORT_NO_EXCEPTIONS`. Any case\n   that is *expected* to `fail_shape_inference` (throws) must be excluded with\n   `#ifndef ORT_NO_EXCEPTIONS`. The \"2 outputs must not go out of range\" case is throw-free\n   after the point fix and is safe in all builds.\n\n**Verify the negative test is non-vacuous (sanitizer A\u002FB)** - the most reliable way to prove a\nnegative test enters the previously-out-of-range branch: build the test at the **pre-fix**\ncommit with **AddressSanitizer** and confirm it flags the out-of-range output access; then\nconfirm it is clean after the fix.\n\n```bash\n# Functional run (any Debug build):\ncmake --build build\u002FLinux\u002FDebug --target onnxruntime_provider_test -j\"$(nproc)\"\n.\u002Fbuild\u002FLinux\u002FDebug\u002Fonnxruntime_provider_test \\\n  --gtest_filter='AttentionOptionalOutputsShapeInferenceTest.*'\n\n# A\u002FB proof (isolated worktree at the pre-fix commit, CPU-only Debug + sanitizer):\ngit worktree add --detach ..\u002Fort-prefix-check \u003Cfix_commit>~1\n# copy the new test file in, then:\npython3 tools\u002Fci_build\u002Fbuild.py --build_dir build\u002Fasan --config Debug --parallel \\\n  --skip_tests --enable_address_sanitizer --skip_submodule_sync \\\n  --cmake_generator Ninja --target onnxruntime_provider_test\n# Pre-fix: the negative tests fail (the sanitizer flags the out-of-range output access).\n# Post-fix (cherry-pick the guard fix): all tests pass, no sanitizer report.\n```\n\n## 6. Process \u002F wording conventions\n\n- Run **`lintrunner -a`** before pushing so the `CLANGFORMAT` \u002F Python-format gate passes. See\n  the `ort-lint` skill.\n- Use **correctness\u002Frobustness framing** in code, comments, commit messages, and the PR body\n  - describe the change as fixing an optional-output guard, not as a security fix. This\n  matches repo convention (compare `python-kwargs-setattr-security`) and keeps the PR neutral.\n\n## 7. Audit checklist (per-operator review)\n\nWhen reviewing or hardening any operator implementation or its shape inference:\n\n- [ ] Read the op's spec - ONNX standard op page, or for a contrib op its `OpSchema`\n      registration (`.Input\u002F.Output\u002F.Attr`, and `Optional`\u002F`Variadic` markers). A local ONNX\n      checkout has the standard-op spec pages; contrib ops are defined only in ORT.\n- [ ] Enumerate **all** inputs, attributes, and outputs, noting which are optional and the\n      resulting `min\u002Fmax` input and output counts.\n- [ ] Validate every input\u002Fattribute before indexing into it, to avoid out-of-range reads\n      (which can cascade into worse failures). Match each output-index write to a guard that\n      guarantees the index is in range (§3 rule).\n- [ ] Prefer `ORT_RETURN_IF` \u002F `ORT_RETURN_IF_NOT` for validation; use `ORT_ENFORCE` in\n      constructors. In shape inference use `fail_shape_inference` \u002F `fail_type_inference`.\n- [ ] Use `SafeInt\u003C>` \u002F `narrow\u003C>()` for index and size arithmetic and casts to avoid overflow\n      or truncation that yields a wrong index. See `core\u002Fcommon\u002Fsafeint.h` and\n      `docs\u002FCoding_Conventions_and_Standards.md`.\n- [ ] Ensure tests build and pass under **no-exceptions** builds; `#ifndef ORT_NO_EXCEPTIONS`\n      around any case expected to throw.\n- [ ] Exclude EPs known not to support the op, with a comment explaining why.\n- [ ] Check whether **other EPs (notably CUDA\u002FROCm)** implement the same op and whether the\n      same issue exists there. (For *shape inference* specifically, the logic is EP-agnostic\n      and single-source - confirm there is no kernel-side analogue.)\n\n## References\n\n- **PR #28176** - \"Fix ... in EmbedLayerNormalizationShapeInference\": the precedent that fixed\n  the identical `> 1` → `> 2` primitive in one site; the sibling attention sites were missed,\n  motivating the sweep in §3.\n- **PR #29268** - this fix: guards corrected in `DecoderAttention` \u002F `MultiHeadAttention` \u002F\n  `DecoderMaskedMultiHeadAttention` shape inference, plus the `getOutputType` bounds check and\n  non-vacuous regression tests.\n- Sibling skill: **`ort-test`** (test executables, `--gtest_filter`, contrib-op test layout);\n  **`ort-lint`** (`lintrunner -a`); **`ort-build`** (build flags, ASan).\n","# Contrib-Op Shape-Inference Output-Index Safety\n\nReusable method for finding and fixing the bug class where an operator's\n`TypeAndShapeInference` function guards an output write with `getNumOutputs() > N` but then\nwrites an output index **greater than** `N`. For a node that declares fewer outputs, the\nwritten index is past the end of the inference context's output vector.\n\n> **Scope**: schema-level shape inference in `onnxruntime\u002Fcore\u002Fgraph\u002Fcontrib_ops\u002F*.cc` and\n> `shape_inference_functions.cc`. This runs once during `Graph::Resolve` (model-load time),\n> **EP-agnostic** - there is no per-EP (CPU\u002FCUDA\u002FROCm) kernel duplicate of this code to\n> chase. Op *kernels* allocate outputs via the bounds-safe `OpKernelContext::Output(index)`\n> and are a separate concern.\n\n## 1. The pattern\n\n```cpp\n\u002F\u002F onnxruntime\u002Fcore\u002Fgraph\u002Fcontrib_ops\u002Fbert_defs.cc  (before)\npropagateElemTypeFromInputToOutput(ctx, 0, 0);\nif (ctx.getNumOutputs() > 1) {                       \u002F\u002F guard says \"> 1\"\n  propagateElemTypeFromInputToOutput(ctx, 0, 1);\n  propagateElemTypeFromInputToOutput(ctx, 0, 2);     \u002F\u002F but writes index 2\n}\n```\n\nThe guard `getNumOutputs() > 1` admits a node with **exactly 2 outputs** (indices 0, 1), yet\nthe body writes index **2**. The implication \"`> 1` ⇒ index 2 exists\" is false: `> 1` only\nguarantees indices 0 and 1.\n\n### Why a smaller output count is valid\n\nTrailing outputs declared `OpSchema::Optional` **lower `min_output`**. ONNX derives\n`min_output` = number of required outputs, `max_output` = total declared. The model checker\n(`checker::check_node`) only enforces `min_output \u003C= N \u003C= max_output`.\n\n| Op | Output decls | min \u002F max | A 2-output node? |\n|---|---|---|---|\n| `DecoderAttention` | out (req), new_key_cache (Opt), new_value_cache (Opt) | 1 \u002F 3 | passes checker |\n| `MultiHeadAttention` | out (req), present_key (Opt), present_value (Opt), qk (Opt) | 1 \u002F 4 | passes checker |\n| `DecoderMaskedMultiHeadAttention` | out (req) + 3 Optional | 1 \u002F 4 | passes checker |\n\nSo a node with `output=['out','present_key']` is schema-valid, passes the checker, and then\nreaches the index-2 write. **A passing checker is not a guarantee the index is in range.**\n\n## 2. The sink (why the write is not caught)\n\n```cpp\n\u002F\u002F onnxruntime\u002Fcore\u002Fgraph\u002Fgraph.cc  -  InferenceContextImpl\nconst TypeProto* getInputType(size_t index) const override {\n  return node_.InputDefs().at(index)->TypeAsProto();   \u002F\u002F .at()  -> bounds-checked\n}\nTypeProto* getOutputType(size_t index) override {\n  return &node_output_types_[index];                   \u002F\u002F operator[]  -> NOT bounds-checked\n}\n```\n\n- `node_output_types_` is sized to `node.OutputDefs().size()` in the `InferenceContextImpl`\n  ctor, so for a 2-output node it has 2 elements; `getOutputType(2)` returns one past the end.\n- `getInputType` uses `.at()` (would throw on a bad index); `getOutputType` uses raw\n  `operator[]` (no check) - the asymmetry is the root cause.\n- The call runs at `Graph::Resolve` → `InferAndVerifyTypeMatch` → `RunInferencing`. The\n  surrounding `ORT_TRY\u002FORT_CATCH(const std::exception&)` only catches *thrown*\n  `fail_shape_inference`; a raw out-of-range `operator[]` does not throw, so the catch does\n  not help.\n- Because this is schema-level inference, it is **EP-independent** - no CUDA\u002FROCm copy.\n\n## 3. Audit technique — always sweep siblings\n\nDo not stop at the reported function. Grep **every** shape-inference guard and compare its\nthreshold against the **highest output index written before the next guard**.\n\n```bash\ngit grep -n 'getNumOutputs' -- \\\n  onnxruntime\u002Fcore\u002Fgraph\u002Fcontrib_ops\u002F*.cc \\\n  onnxruntime\u002Fcore\u002Fgraph\u002Fcontrib_ops\u002Fshape_inference_functions.cc\n```\n\nFor each `if (ctx.getNumOutputs() > N)` block, find the largest `index` passed to\n`propagateElemTypeFromInputToOutput(ctx, _, index)` \u002F `updateOutputShape(ctx, index, _)` \u002F\n`getOutputType(index)` inside it. **Rule: the guard must require strictly more outputs than\nthe highest index written** (write index `k` ⇒ guard must ensure `getNumOutputs() > k`).\n\nCorrect exemplars already in the tree to copy:\n\n| Exemplar | Pattern | Why it is safe |\n|---|---|---|\n| `BaseGroupQueryAttention...` | `if (getNumOutputs() >= 3)` then writes idx 2 | guard covers highest index |\n| `PagedAttention...` | nested `> 1` + inner `if (getNumOutputs() != 3) fail_shape_inference` | fails before any write |\n| `EmbedLayerNormalizationShapeInference` | `> 2` then writes idx 2 | fixed by PR #28176 (precedent) |\n| `SkipLayerNormalizationShapeInference` | each idx `k` guarded by `> k` | per-index guard |\n\n> **Gotcha — conditional writes can hide a vacuous audit.** A write may sit behind an inner\n> condition (e.g. `hasInputShape(past_key_index)` before writing index 2). The site is still\n> a bug, but you can only *observe* it when that inner condition is also satisfied. Keep this\n> in mind both for the audit and for tests (§5).\n\n## 4. Fix patterns\n\n**Point fix (required): raise the guard to cover the highest index written.**\n\n```cpp\n\u002F\u002F before\nif (ctx.getNumOutputs() > 1) { ... writes idx 2 ... }\n\u002F\u002F after\nif (ctx.getNumOutputs() > 2) {  \u002F\u002F both present_key (idx 1) AND present_value (idx 2)\n  ...\n}\n```\n\nJustify the threshold with the op's output semantics. For these attention ops the two trailing\noutputs - `present_key` (idx 1) and `present_value` (idx 2) for `MultiHeadAttention`,\n`new_key_cache` \u002F `new_value_cache` for `DecoderAttention` (see the §1 table for each op's\nexact output names) - are a **both-or-neither pair**: there is no valid configuration that\nemits one without the other, so requiring all three outputs before populating indices 1 and 2\nis behavior-preserving. (`PagedAttention` encodes the same invariant via its nested `!= 3`\ncheck.)\n\n**Defense-in-depth (recommended): bound the sink** so a future author cannot reintroduce the\nclass.\n\n```cpp\n\u002F\u002F onnxruntime\u002Fcore\u002Fgraph\u002Fgraph.cc  -  InferenceContextImpl::getOutputType\nTypeProto* getOutputType(size_t index) override {\n  if (index >= node_output_types_.size()) {\n    fail_type_inference(\"output index \", index, \" is out of range; node has \",\n                        node_output_types_.size(), \" outputs\");\n  }\n  return &node_output_types_[index];\n}\n```\n\nThis mirrors `getInputType`'s `.at()` and the existing bounds checks in the sibling\n`DataPropagationContextImpl`. Placing it at the base layer transitively protects the NHWC and\nquantization wrapper contexts. After the point fix this branch is unreachable through a normal\nmodel (the guard already prevents the out-of-range index), so it is pure defense-in-depth. Its\nfailure mode is build-dependent: with exceptions enabled, `fail_type_inference` raises\n`InferenceError` (a `std::exception`), caught by the existing `ORT_CATCH(const std::exception&)`\naround `RunInferencing` and surfaced as a clean load-time error; under `ORT_NO_EXCEPTIONS` it is\n**not** compiled out - ONNX's no-exceptions path prints the message to `std::cerr` and calls\n`abort()`, a deterministic fail-fast (consistent with `getInputType`'s `.at()`, which likewise\nterminates under no-exceptions). Either way the result is a controlled failure rather than an\nout-of-range write.\n\n## 5. Test recipe\n\nTests live in `onnxruntime\u002Ftest\u002Fcontrib_ops\u002F*.cc` and are **auto-globbed** into the\n`onnxruntime_provider_test` target by `cmake\u002Fonnxruntime_unittests.cmake`\n(`test\u002Fcontrib_ops\u002F*.cc` pattern) - **no cmake edit needed** for a new file. See the\n`ort-test` skill for the executable taxonomy (`onnxruntime_provider_test` vs\n`onnxruntime_test_all`).\n\nRules that make the regression test actually guard the fix:\n\n1. **Drive through `Model` + `Graph::Resolve`**, not ONNX's standalone `TestShapeInference`.\n   Only the full resolve path constructs the real `InferenceContextImpl` and hits the\n   `getOutputType` sink described in §2. A standalone ONNX shape-inference helper uses a\n   different context and **bypasses** the sink, so it cannot reproduce the bug.\n2. **Negative tests must be NON-VACUOUS** - they must actually enter the write branch on\n   pre-fix source. If a write is gated by an inner condition (§3 gotcha), satisfy it: e.g. for\n   `MultiHeadAttention`\u002F`DecoderMaskedMultiHeadAttention`, supply a **shaped `past_key`**\n   (and `past_sequence_length` \u002F `past_present_share_buffer` as the op requires) so the\n   index-2 block runs. A negative test that only supplies `query` skips the block and passes\n   even on pre-fix source - regression-proof in name only.\n3. **Add positive (all-outputs) cases**: a node with every output present must still infer the\n   trailing output types - proves the tightened guard did not over-restrict.\n4. **Keep tests throw-free post-fix** so they are valid under `ORT_NO_EXCEPTIONS`. Any case\n   that is *expected* to `fail_shape_inference` (throws) must be excluded with\n   `#ifndef ORT_NO_EXCEPTIONS`. The \"2 outputs must not go out of range\" case is throw-free\n   after the point fix and is safe in all builds.\n\n**Verify the negative test is non-vacuous (sanitizer A\u002FB)** - the most reliable way to prove a\nnegative test enters the previously-out-of-range branch: build the test at the **pre-fix**\ncommit with **AddressSanitizer** and confirm it flags the out-of-range output access; then\nconfirm it is clean after the fix.\n\n```bash\n# Functional run (any Debug build):\ncmake --build build\u002FLinux\u002FDebug --target onnxruntime_provider_test -j\"$(nproc)\"\n.\u002Fbuild\u002FLinux\u002FDebug\u002Fonnxruntime_provider_test \\\n  --gtest_filter='AttentionOptionalOutputsShapeInferenceTest.*'\n\n# A\u002FB proof (isolated worktree at the pre-fix commit, CPU-only Debug + sanitizer):\ngit worktree add --detach ..\u002Fort-prefix-check \u003Cfix_commit>~1\n# copy the new test file in, then:\npython3 tools\u002Fci_build\u002Fbuild.py --build_dir build\u002Fasan --config Debug --parallel \\\n  --skip_tests --enable_address_sanitizer --skip_submodule_sync \\\n  --cmake_generator Ninja --target onnxruntime_provider_test\n# Pre-fix: the negative tests fail (the sanitizer flags the out-of-range output access).\n# Post-fix (cherry-pick the guard fix): all tests pass, no sanitizer report.\n```\n\n## 6. Process \u002F wording conventions\n\n- Run **`lintrunner -a`** before pushing so the `CLANGFORMAT` \u002F Python-format gate passes. See\n  the `ort-lint` skill.\n- Use **correctness\u002Frobustness framing** in code, comments, commit messages, and the PR body\n  - describe the change as fixing an optional-output guard, not as a security fix. This\n  matches repo convention (compare `python-kwargs-setattr-security`) and keeps the PR neutral.\n\n## 7. Audit checklist (per-operator review)\n\nWhen reviewing or hardening any operator implementation or its shape inference:\n\n- [ ] Read the op's spec - ONNX standard op page, or for a contrib op its `OpSchema`\n      registration (`.Input\u002F.Output\u002F.Attr`, and `Optional`\u002F`Variadic` markers). A local ONNX\n      checkout has the standard-op spec pages; contrib ops are defined only in ORT.\n- [ ] Enumerate **all** inputs, attributes, and outputs, noting which are optional and the\n      resulting `min\u002Fmax` input and output counts.\n- [ ] Validate every input\u002Fattribute before indexing into it, to avoid out-of-range reads\n      (which can cascade into worse failures). Match each output-index write to a guard that\n      guarantees the index is in range (§3 rule).\n- [ ] Prefer `ORT_RETURN_IF` \u002F `ORT_RETURN_IF_NOT` for validation; use `ORT_ENFORCE` in\n      constructors. In shape inference use `fail_shape_inference` \u002F `fail_type_inference`.\n- [ ] Use `SafeInt\u003C>` \u002F `narrow\u003C>()` for index and size arithmetic and casts to avoid overflow\n      or truncation that yields a wrong index. See `core\u002Fcommon\u002Fsafeint.h` and\n      `docs\u002FCoding_Conventions_and_Standards.md`.\n- [ ] Ensure tests build and pass under **no-exceptions** builds; `#ifndef ORT_NO_EXCEPTIONS`\n      around any case expected to throw.\n- [ ] Exclude EPs known not to support the op, with a comment explaining why.\n- [ ] Check whether **other EPs (notably CUDA\u002FROCm)** implement the same op and whether the\n      same issue exists there. (For *shape inference* specifically, the logic is EP-agnostic\n      and single-source - confirm there is no kernel-side analogue.)\n\n## References\n\n- **PR #28176** - \"Fix ... in EmbedLayerNormalizationShapeInference\": the precedent that fixed\n  the identical `> 1` → `> 2` primitive in one site; the sibling attention sites were missed,\n  motivating the sweep in §3.\n- **PR #29268** - this fix: guards corrected in `DecoderAttention` \u002F `MultiHeadAttention` \u002F\n  `DecoderMaskedMultiHeadAttention` shape inference, plus the `getOutputType` bounds check and\n  non-vacuous regression tests.\n- Sibling skill: **`ort-test`** (test executables, `--gtest_filter`, contrib-op test layout);\n  **`ort-lint`** (`lintrunner -a`); **`ort-build`** (build flags, ASan).\n",{"name":5,"description":7},"main","4edd78a847b9d7755f32d6c99aa60cab2addd39b",".github\u002Fskills\u002Fcontrib-op-shape-inference-memory-safety",4443,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fonnxruntime","https:\u002F\u002Fonnxruntime.ai","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-14 02:35:07",{"source":8,"skillName":5,"commands":55,"prompt":65,"zipAvailable":66},[56,59,62],{"tool":57,"command":58},"npx","npx skills add microsoft\u002Fonnxruntime --skill contrib-op-shape-inference-memory-safety",{"tool":60,"command":61},"bunx","bunx skills add microsoft\u002Fonnxruntime --skill contrib-op-shape-inference-memory-safety",{"tool":63,"command":64},"pnpm","pnpm dlx skills add microsoft\u002Fonnxruntime --skill contrib-op-shape-inference-memory-safety","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fonnxruntime\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fcontrib-op-shape-inference-memory-safety\u002FSKILL.md",true,[],[69,99,132,156,175,191],{"skillNo":70,"name":71,"nameZh":6,"description":72,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":80,"publishedAt":39},"01M69AJ65WPPT5DS4JA8YWN37B","workspace-estimation-shared-header","Use when editing a boundary-safe shared header meant to be included by both in-tree and shared-provider\u002Fplugin-bridge code (e.g. include\u002Fonnxruntime\u002Fcore\u002Fframework\u002Fworkspace_requirement.h, or any future header supporting OrtKernelWorkspaceEstimateFunc \u002F DeclareWorkspaceRequirements per issue #29775's Phase-A workspace-estimation roadmap), or when a workspace-estimation math helper needs to be callable from a future plugin EP DLL. Covers a Node forward-declaration ODR trap and the math-helper vs. graph-parsing-wrapper reuse boundary discovered while implementing PR #29811 (MatMulNBits pilot).","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fonnxruntime\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fworkspace-estimation-shared-header\u002FSKILL.md",[75],{"categoryNo":76,"categoryName":77,"taxonomyType":78,"taxonomyTypeDesc":79},"01JBSEEDCAT000000000000031","产品经理","PROFESSION","适用职业",[81,82,83,84,85,86,87,88,28,89,90,91,92,93,94,95,96,97,98,37],"workspace estimation shared header","workspace","estimation","shared","safe","pr","code","framework","parsing","node","math","bridge","roadmap","Wrapper","issue","tree","implementing","phase",{"skillNo":100,"name":101,"nameZh":6,"description":102,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":103,"categories":104,"keywords":110,"publishedAt":39},"01M69AJ3F3NXDS7QP76KZ9EX5H","webgpu-local-testing","Build and run ONNX Runtime WebGPU provider tests on Linux WITHOUT a real GPU, using a software Vulkan adapter (Mesa lavapipe). Use when you need to exercise WebGPU EP kernels off-Mac — the Linux webgpu CI leg is build-only, so software Vulkan is how you actually run WebGPU correctness tests locally. SCOPE - lavapipe only validates host-side enforce\u002Fshape bugs and MatMul-free kernels; any graph containing MatMul (including the expanded-Attention node tests) crashes lavapipe and runs ONLY on macOS-arm64 Metal, which is the source of truth for those. Covers install (dnf on Azure Linux), the --use_webgpu build flag, the onnxruntime_provider_test target, VK_ICD_FILENAMES, and the lavapipe MatMul crash gotcha.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fonnxruntime\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fwebgpu-local-testing\u002FSKILL.md",[105],{"categoryNo":106,"categoryName":107,"taxonomyType":108,"taxonomyTypeDesc":109},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[111,112,113,114,115,116,117,118,119,120,31,27,121,122,123,90,124,125,126,127,128,129,130,131,37],"testing","webgpu","webgpu local testing","local testing","local","macos","metal","vulkan","linux","test","ci","exercise","how","install","only","free","host","scope","you","vk","which",{"skillNo":133,"name":134,"nameZh":6,"description":135,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":136,"categories":137,"keywords":138,"publishedAt":39},"01M69AJ1ASRSCJ90HDWN05TW82","python-kwargs-setattr-security","When reviewing or fixing Python code that uses setattr() with user-controlled kwargs to configure C++ extension objects (SessionOptions, RunOptions, etc.) in ONNX Runtime. Use this to apply the allowlist pattern that prevents arbitrary file writes and other attacks via reflected property access.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fonnxruntime\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fpython-kwargs-setattr-security\u002FSKILL.md",[],[139,140,141,31,87,142,143,144,145,146,147,148,149,150,151,152,153,154,155,37],"python","security","python kwargs setattr security","file","access","configure","use","objects","pattern","c++","runtime","extension","via","with","attacks","python code","apply",{"skillNo":157,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":162,"publishedAt":39},"01M69AHZ0M0Q90TP0KYMZ10HEP","ort-transformers-gpu-pytest","Run the ONNX Runtime transformers Python tests (onnxruntime\u002Ftest\u002Fpython\u002Ftransformers) against a GPU wheel, and confirm real cuDNN\u002Fflash SDPA dispatch. Use when a transformers pytest fails with ModuleNotFoundError onnxruntime.capi, when torch's bundled CUDA\u002FcuDNN libs shadow the ORT-built ones (wrong-version dispatch or load errors), or when you must PROVE a test exercised the cuDNN SDPA decode tier instead of silently skipping\u002Ffalling back to MATH.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fonnxruntime\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fort-transformers-gpu-pytest\u002FSKILL.md",[],[163,164,165,166,139,120,31,27,167,91,145,168,149,169,170,129,171,172,173,174,37],"ort transformers gpu pytest","gpu","Transformers","pytest","flash","back","tests","SDPA","errors","load","prove","decode",{"skillNo":176,"name":177,"nameZh":6,"description":178,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":179,"categories":180,"keywords":182,"publishedAt":39},"01M69AHW3EFVHBHB2S7103YA21","ort-test","Run ONNX Runtime tests. Use this skill when asked to run tests, debug test failures, or find and execute specific test cases in ONNX Runtime.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fonnxruntime\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fort-test\u002FSKILL.md",[181],{"categoryNo":106,"categoryName":107,"taxonomyType":108,"taxonomyTypeDesc":109},[120,183,184,185,186,31,187,27,188,189,145,149,169,190,37],"ort test","run tests","debug","debug test","debug test failures","find","execute","test cases",{"skillNo":192,"name":193,"nameZh":6,"description":194,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":195,"categories":196,"keywords":197,"publishedAt":39},"01M69AHSKXBSERP5NGAJB5E6XD","ort-release-notes","Draft ONNX Runtime release notes using preset configurations for full ORT or scoped component releases. Use when generating highlights with PR links, compiling human contributor acknowledgments from compile_contributors.py output, and applying preset path filtering.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fonnxruntime\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fort-release-notes\u002FSKILL.md",[],[198,199,200,201,202,203,31,86,204,205,206,207,208,209,149,210,211,212,213,214,37],"notes","ort release notes","release notes","release","component","preset","compile","compiling","contributors","releases","human","full","links","path","for","scoped","contributor"]