From 06cc688f001d914e9f72e47115a8f32ba78e571d Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Wed, 26 Aug 2026 23:43:52 -0700 Subject: [PATCH 1/4] Qualcomm: bounds-check the delegate argument walk instead of running off the end execute() binds delegate arguments positionally. It walks the input and output tensor lists recovered from the context binary and, for every tensor the name prefixes mark as bindable, consumes one entry from args with a running counter. Nothing relates that counter to args.size(). So when the binary and the program disagree on the delegate signature -- a stale binary, or an AOT bug that publishes extra graph I/O -- the walk indexes past the end of the Span and dereferences whatever is there. In the case that prompted this, a context binary declaring 54 graph inputs and 56 graph outputs met a program passing 4 tensors, and the result was a null dereference at 0x8 with the two counts sitting in registers. Reading that back to a cause took days. Count the bindable tensors with the same prefix rules the loops use, then check once before either loop runs. A shortfall is the memory-safety case and is fatal; a surplus is not unsafe, so it warns rather than failing, since a trailing unused argument is not obviously wrong. Deliberately not included: a matching "input_" prefix filter on the input loop, for symmetry with the output loop. Inputs of a model built by from_context_binary carry names straight from the QNN converter with no such prefix, and the runtime only renames outputs (QnnManager.cpp SetName("output_" + tensor_name)). Filtering on it would skip every input of those models and leave the counter at zero when the output loop starts, writing outputs into input buffers. The count check gives the same protection without that risk. Authored with assistance from Claude Code. --- .../qualcomm/runtime/QnnExecuTorchBackend.cpp | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp b/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp index 8bbe047a967..f5458fbba54 100644 --- a/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp +++ b/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp @@ -139,6 +139,46 @@ Error QnnExecuTorchBackend::execute( std::vector input_tensor_structs; std::vector output_tensor_structs; + // The loops below walk the tensor lists recovered from the context binary and + // index args[] with a running counter, so the number of bindable tensors the + // binary declares has to agree with what the program passes. When it does not + // -- a stale binary, or an AOT bug that publishes extra graph I/O -- the walk + // runs off the end of the Span. Count first and fail with both numbers rather + // than reading out of bounds. + size_t bindable_inputs = 0; + for (const auto& input_tensor : input_tensors) { + if (input_tensor->GetName().find("mutbuf_") == std::string::npos) { + ++bindable_inputs; + } + } + size_t bindable_outputs = 0; + for (const auto& output_tensor : output_tensors) { + if (output_tensor->GetName().rfind("output_", 0) == 0 && + output_tensor->GetName().find("mutbuf_") == std::string::npos) { + ++bindable_outputs; + } + } + ET_CHECK_OR_RETURN_ERROR( + bindable_inputs + bindable_outputs <= args.size(), + Internal, + "Method %s: the QNN context binary binds %zu tensors (%zu graph inputs, " + "%zu graph outputs) but ExecuTorch passed %zu arguments. The binary and " + "the program disagree on the delegate signature; the model has to be " + "re-exported.", + method_name.c_str(), + bindable_inputs + bindable_outputs, + bindable_inputs, + bindable_outputs, + args.size()); + if (bindable_inputs + bindable_outputs != args.size()) { + QNN_EXECUTORCH_LOG_WARN( + "Method %s: ExecuTorch passed %zu arguments but the QNN graph binds " + "only %zu; the trailing arguments are unused.", + method_name.c_str(), + args.size(), + bindable_inputs + bindable_outputs); + } + int args_index = 0; input_tensor_structs.reserve(input_tensors.size()); for (const auto& input_tensor : input_tensors) { From 5042dd46a871cc77179b08040cd15e90e7fd11db Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Tue, 1 Sep 2026 18:11:46 -0700 Subject: [PATCH 2/4] Qualcomm: make an argument-count surplus an error too Review follow-up. A surplus is not a memory-safety problem, which is why it warned, but if the check exists to keep QNN graph I/O and the delegate signature aligned then both directions are misalignment. Verified against 14 lowerings -- single I/O, multi-input, multi-output, partially-consumed multi-output, topk with both outputs used, a mutable buffer and a conv, each under an fp16 and a quantized spec -- with no false positives. Mutable buffers are excluded from the count by the same rule the binding loops use, so they do not create a surplus. --- backends/qualcomm/runtime/QnnExecuTorchBackend.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp b/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp index f5458fbba54..f5b1800775e 100644 --- a/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp +++ b/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp @@ -159,7 +159,7 @@ Error QnnExecuTorchBackend::execute( } } ET_CHECK_OR_RETURN_ERROR( - bindable_inputs + bindable_outputs <= args.size(), + bindable_inputs + bindable_outputs == args.size(), Internal, "Method %s: the QNN context binary binds %zu tensors (%zu graph inputs, " "%zu graph outputs) but ExecuTorch passed %zu arguments. The binary and " @@ -170,14 +170,6 @@ Error QnnExecuTorchBackend::execute( bindable_inputs, bindable_outputs, args.size()); - if (bindable_inputs + bindable_outputs != args.size()) { - QNN_EXECUTORCH_LOG_WARN( - "Method %s: ExecuTorch passed %zu arguments but the QNN graph binds " - "only %zu; the trailing arguments are unused.", - method_name.c_str(), - args.size(), - bindable_inputs + bindable_outputs); - } int args_index = 0; input_tensor_structs.reserve(input_tensors.size()); From 2b4144a89ff9dfa8b0dc549edbb3bc34491c0c2b Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Thu, 3 Sep 2026 13:53:46 -0700 Subject: [PATCH 3/4] Update error message for tensor binding mismatch Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- backends/qualcomm/runtime/QnnExecuTorchBackend.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp b/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp index f5b1800775e..f1de44ef4dd 100644 --- a/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp +++ b/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp @@ -161,8 +161,8 @@ Error QnnExecuTorchBackend::execute( ET_CHECK_OR_RETURN_ERROR( bindable_inputs + bindable_outputs == args.size(), Internal, - "Method %s: the QNN context binary binds %zu tensors (%zu graph inputs, " - "%zu graph outputs) but ExecuTorch passed %zu arguments. The binary and " + "Method %s: the QNN context binary binds %zu tensors (%zu bindable inputs, " + "%zu bindable outputs) but ExecuTorch passed %zu arguments. The binary and " "the program disagree on the delegate signature; the model has to be " "re-exported.", method_name.c_str(), From 25d20f11cde59fbc513597db3e80322960464c3a Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Thu, 3 Sep 2026 14:03:37 -0700 Subject: [PATCH 4/4] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- backends/qualcomm/runtime/QnnExecuTorchBackend.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp b/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp index f1de44ef4dd..76b67f911cd 100644 --- a/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp +++ b/backends/qualcomm/runtime/QnnExecuTorchBackend.cpp @@ -147,14 +147,16 @@ Error QnnExecuTorchBackend::execute( // than reading out of bounds. size_t bindable_inputs = 0; for (const auto& input_tensor : input_tensors) { - if (input_tensor->GetName().find("mutbuf_") == std::string::npos) { + const auto& name = input_tensor->GetName(); + if (name.find("mutbuf_") == std::string::npos) { ++bindable_inputs; } } size_t bindable_outputs = 0; for (const auto& output_tensor : output_tensors) { - if (output_tensor->GetName().rfind("output_", 0) == 0 && - output_tensor->GetName().find("mutbuf_") == std::string::npos) { + const auto& name = output_tensor->GetName(); + if (name.rfind("output_", 0) == 0 && + name.find("mutbuf_") == std::string::npos) { ++bindable_outputs; } } @@ -171,7 +173,7 @@ Error QnnExecuTorchBackend::execute( bindable_outputs, args.size()); - int args_index = 0; + size_t args_index = 0; input_tensor_structs.reserve(input_tensors.size()); for (const auto& input_tensor : input_tensors) { if (input_tensor->GetName().find("mutbuf_") == std::string::npos) {