Skip to content

Commit 00e9ef3

Browse files
vrasparCopilot
andauthored
Fix input validation and null-pointer dereference in STFTDecomposition graph transformer (microsoft#28465)
### Description Validate that `frame_step` and `dft_size` (derived from `frame_length`) are positive before they are used in buffer sizing and loop arithmetic. Use SafeInt for the weight buffer allocation to guard against overflow. Also fix an unconditional dereference of `window_recipient` which is nullptr when the STFT node has no window input. ### Motivation and Context A model with non-positive initializer values for frame_length or frame_step causes signed-to-unsigned wrapping in size computations, leading to out-of-bounds writes during graph optimization. The nullptr dereference is a crash on any windowless STFT node. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ddd1eae commit 00e9ef3

6 files changed

Lines changed: 132 additions & 7 deletions

File tree

onnxruntime/core/optimizer/stft_decomposition.cc

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "core/graph/graph_utils.h"
1010
#include "core/optimizer/optimizer_execution_frame.h"
1111
#include "core/optimizer/utils.h"
12+
#include "core/common/safeint.h"
1213
#include "core/framework/op_kernel.h"
1314
#include "core/framework/tensorprotoutils.h"
1415
#include <numbers>
@@ -210,6 +211,14 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve
210211
dft_size = window_length_dim.dim_value();
211212
}
212213

214+
// Validate model-provided scalar values before using them in size calculations.
215+
// These come from untrusted model initializers/shapes and must be positive.
216+
if (dft_size <= 0 || frame_step_value <= 0) {
217+
LOGS(logger, WARNING) << "STFT decomposition skipped: invalid dft_size (" << dft_size
218+
<< ") or frame_step_value (" << frame_step_value << ")";
219+
continue;
220+
}
221+
213222
bool is_onesided = true;
214223
auto& attrs = stft.GetAttributes();
215224
if (attrs.find("onesided") != attrs.end()) {
@@ -227,14 +236,22 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve
227236
if (is_real) {
228237
auto output_num_frames = stft.MutableOutputDefs()[0]->Shape()->dim(1).dim_value();
229238
auto output_frame_length = stft.MutableOutputDefs()[0]->Shape()->dim(2).dim_value();
230-
auto weight_size = static_cast<size_t>(dft_unique_bins * dft_size);
239+
240+
size_t dft_size_sz, dft_unique_bins_sz, weight_size;
241+
if (!SafeCast(dft_unique_bins, dft_unique_bins_sz) ||
242+
!SafeCast(dft_size, dft_size_sz) ||
243+
!SafeMultiply(dft_unique_bins_sz, dft_size_sz, weight_size)) {
244+
LOGS(logger, WARNING) << "STFT decomposition skipped: weight size overflow";
245+
continue;
246+
}
247+
231248
auto real_weights_data = std::vector<float>(weight_size);
232249
auto imag_weights_data = std::vector<float>(weight_size);
233250

234251
// Populate weights
235-
for (size_t k = 0; k < static_cast<size_t>(dft_unique_bins); k++) {
236-
for (size_t n = 0; n < static_cast<size_t>(dft_size); n++) {
237-
auto index = static_cast<size_t>(k * dft_size + n);
252+
for (size_t k = 0; k < dft_unique_bins_sz; k++) {
253+
for (size_t n = 0; n < dft_size_sz; n++) {
254+
auto index = k * dft_size_sz + n;
238255
auto theta = -2 * std::numbers::pi_v<float> * k * n / static_cast<float>(dft_size);
239256
real_weights_data[index] = static_cast<float>(cos(theta));
240257
imag_weights_data[index] = static_cast<float>(sin(theta));
@@ -356,7 +373,6 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve
356373

357374
// Copy inputs
358375
auto signal_target_idx = signal_recipient->Index();
359-
auto window_target_idx = window_recipient->Index();
360376
for (auto cur = input_edges.cbegin(), end = input_edges.cend(); cur != end; ++cur) {
361377
const graph_utils::GraphEdge& edge = *cur;
362378
NodeIndex target_idx = 0;
@@ -367,8 +383,10 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve
367383
recipient = signal_recipient;
368384
break;
369385
case 2:
370-
target_idx = window_target_idx;
371-
recipient = window_recipient;
386+
if (window_recipient) {
387+
target_idx = window_recipient->Index();
388+
recipient = window_recipient;
389+
}
372390
break;
373391
}
374392

onnxruntime/test/optimizer/graph_transform_test.cc

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
#include "core/optimizer/rule_based_graph_transformer.h"
7777
#include "core/optimizer/slice_concat_to_space_to_depth_fusion.h"
7878
#include "core/optimizer/slice_elimination.h"
79+
#include "core/optimizer/stft_decomposition.h"
7980
#include "core/optimizer/unsqueeze_elimination.h"
8081
#include "core/optimizer/utils.h"
8182
#include "core/platform/env.h"
@@ -10425,5 +10426,65 @@ TEST_F(GraphTransformationTests, DivMulFusion_MultiElementInitializer) {
1042510426
// `dropout_elimination.cc` remains as pure defense-in-depth against future
1042610427
// internal callers that may bypass shape inference.
1042710428

10429+
// These tests verify that STFTDecomposition skips malformed models
10430+
// instead of crashing with OOB writes from negative initializer values.
10431+
TEST_F(GraphTransformationTests, STFTDecomposition_NegativeFrameLength) {
10432+
constexpr const ORTCHAR_T* model_uri = MODEL_FOLDER "stft_negative_frame_length.onnx";
10433+
std::shared_ptr<Model> model;
10434+
ASSERT_STATUS_OK(Model::Load(model_uri, model, nullptr, *logger_));
10435+
Graph& graph = model->MainGraph();
10436+
std::map<std::string, int> op_to_count = CountOpsInGraph(graph);
10437+
ASSERT_EQ(op_to_count["STFT"], 1);
10438+
10439+
const InlinedHashSet<std::string_view> empty_ep = {};
10440+
auto stft_transformer = std::make_unique<STFTDecomposition>(empty_ep);
10441+
onnxruntime::GraphTransformerManager graph_transformation_mgr{5};
10442+
ASSERT_STATUS_OK(graph_transformation_mgr.Register(std::move(stft_transformer), TransformerLevel::Level1));
10443+
ASSERT_STATUS_OK(graph_transformation_mgr.ApplyTransformers(graph, TransformerLevel::Level1, *logger_));
10444+
10445+
// STFT node should NOT be decomposed — transformer skips invalid models
10446+
op_to_count = CountOpsInGraph(graph);
10447+
ASSERT_EQ(op_to_count["STFT"], 1);
10448+
}
10449+
10450+
TEST_F(GraphTransformationTests, STFTDecomposition_NegativeFrameStep) {
10451+
constexpr const ORTCHAR_T* model_uri = MODEL_FOLDER "stft_negative_frame_step.onnx";
10452+
std::shared_ptr<Model> model;
10453+
ASSERT_STATUS_OK(Model::Load(model_uri, model, nullptr, *logger_));
10454+
Graph& graph = model->MainGraph();
10455+
std::map<std::string, int> op_to_count = CountOpsInGraph(graph);
10456+
ASSERT_EQ(op_to_count["STFT"], 1);
10457+
10458+
const InlinedHashSet<std::string_view> empty_ep = {};
10459+
auto stft_transformer = std::make_unique<STFTDecomposition>(empty_ep);
10460+
onnxruntime::GraphTransformerManager graph_transformation_mgr{5};
10461+
ASSERT_STATUS_OK(graph_transformation_mgr.Register(std::move(stft_transformer), TransformerLevel::Level1));
10462+
ASSERT_STATUS_OK(graph_transformation_mgr.ApplyTransformers(graph, TransformerLevel::Level1, *logger_));
10463+
10464+
// STFT node should NOT be decomposed — transformer skips invalid models
10465+
op_to_count = CountOpsInGraph(graph);
10466+
ASSERT_EQ(op_to_count["STFT"], 1);
10467+
}
10468+
10469+
TEST_F(GraphTransformationTests, STFTDecomposition_NoWindowInput) {
10470+
constexpr const ORTCHAR_T* model_uri = MODEL_FOLDER "stft_no_window.onnx";
10471+
std::shared_ptr<Model> model;
10472+
ASSERT_STATUS_OK(Model::Load(model_uri, model, nullptr, *logger_));
10473+
Graph& graph = model->MainGraph();
10474+
std::map<std::string, int> op_to_count = CountOpsInGraph(graph);
10475+
ASSERT_EQ(op_to_count["STFT"], 1);
10476+
10477+
const InlinedHashSet<std::string_view> empty_ep = {};
10478+
auto stft_transformer = std::make_unique<STFTDecomposition>(empty_ep);
10479+
onnxruntime::GraphTransformerManager graph_transformation_mgr{5};
10480+
ASSERT_STATUS_OK(graph_transformation_mgr.Register(std::move(stft_transformer), TransformerLevel::Level1));
10481+
// Should not crash (previously dereferenced nullptr window_recipient)
10482+
ASSERT_STATUS_OK(graph_transformation_mgr.ApplyTransformers(graph, TransformerLevel::Level1, *logger_));
10483+
10484+
// Valid windowless STFT should be successfully decomposed
10485+
op_to_count = CountOpsInGraph(graph);
10486+
ASSERT_EQ(op_to_count["STFT"], 0);
10487+
}
10488+
1042810489
} // namespace test
1042910490
} // namespace onnxruntime
204 Bytes
Binary file not shown.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from onnx import TensorProto, helper, save
2+
3+
4+
def make_stft_model(frame_length_value, frame_step_value, has_window, filename):
5+
batch = 1
6+
signal_length = 16
7+
dft_size = abs(frame_length_value) if frame_length_value != 0 else 4
8+
onesided_bins = dft_size // 2 + 1
9+
num_frames = max(1, (signal_length - dft_size) // abs(frame_step_value) + 1) if frame_step_value != 0 else 1
10+
11+
signal = helper.make_tensor_value_info("signal", TensorProto.FLOAT, [batch, signal_length, 1])
12+
output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [batch, num_frames, onesided_bins, 2])
13+
14+
frame_step_init = helper.make_tensor("frame_step", TensorProto.INT64, [], [frame_step_value])
15+
frame_length_init = helper.make_tensor("frame_length", TensorProto.INT64, [], [frame_length_value])
16+
17+
window_input = "window" if has_window else ""
18+
initializers = [frame_step_init, frame_length_init]
19+
if has_window:
20+
window_init = helper.make_tensor("window", TensorProto.FLOAT, [dft_size], [1.0] * dft_size)
21+
initializers.append(window_init)
22+
23+
stft_node = helper.make_node(
24+
"STFT",
25+
inputs=["signal", "frame_step", window_input, "frame_length"],
26+
outputs=["output"],
27+
onesided=1,
28+
)
29+
30+
graph = helper.make_graph(
31+
[stft_node],
32+
"stft_test",
33+
[signal],
34+
[output],
35+
initializer=initializers,
36+
)
37+
38+
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)])
39+
model.ir_version = 8
40+
save(model, filename)
41+
42+
43+
if __name__ == "__main__":
44+
make_stft_model(-2, 4, False, "stft_negative_frame_length.onnx")
45+
make_stft_model(4, -2, False, "stft_negative_frame_step.onnx")
46+
make_stft_model(4, 4, False, "stft_no_window.onnx")
204 Bytes
Binary file not shown.
195 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)