From 06cc688f001d914e9f72e47115a8f32ba78e571d Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Wed, 26 Aug 2026 23:43:52 -0700 Subject: [PATCH] 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) {