YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
ExecuTorch Vulkan delegate: unchecked VkTensor.constant_id vector index β OOB read at delegate init
PRIVATE security research artifact. Vulnerability packaged for coordinated disclosure via huntr. Do not redistribute.
Summary
Loading a malicious .pte model whose delegate resolves to the Vulkan backend triggers an
out-of-bounds heap read (and, with larger indices, a wild read / SEGV) during delegate
initialization β before any GPU work and after the FlatBuffer structural verifier passes.
A constant tensor's index is taken directly from the attacker-controlled FlatBuffer field
VkTensor.constant_id and used as a flatbuffers::Vector::Get() index with no bounds check
against constants()->size().
- Target:
pytorch/executorch - Version / commit:
v1.4.0a0@a6d812a082df57898b8608f56c867140cc9da32c(2026-07-10) - Component:
backends/vulkan/runtime/VulkanBackend.cppβGraphBuilder::add_tensor_to_graph() - Vulnerability class: CWE-125 (Out-of-bounds Read) via unvalidated array index (CWE-129)
- Trigger: untrusted model file (
.pte), no user interaction beyond loading the model - Impact: OOB read of process memory β crash / DoS; potential info-disclosure of heap contents
reachable through the fabricated
VkBytes*table (offset,named_key).
Root cause
schema.fbs defines the index as a plain signed scalar:
// backends/vulkan/serialization/schema.fbs
table VkTensor { // line 52
...
constant_id:int; // line 58 (int32, attacker-controlled)
}
...
constants:[VkBytes]; // line 147 (optional vector)
GraphBuilder::add_tensor_to_graph() uses that field directly as a vector index:
// backends/vulkan/runtime/VulkanBackend.cpp (lines ~285-287)
ValueRef ref;
if (tensor_fb->constant_id() >= 0) {
VkBytesPtr constant_bytes =
flatbuffer_->constants()->Get(tensor_fb->constant_id()); // <-- OOB: no i < size() check
if (constant_bytes->named_key() != nullptr && // first deref of bogus pointer
constant_bytes->offset() == UINT64_MAX && ...) { ... }
The only guard is constant_id() >= 0. There is no check that
constant_id < constants()->size(). flatbuffers::Vector::Get() guards only with
FLATBUFFERS_ASSERT(i < size()) β a plain assert that is compiled out under -DNDEBUG,
which is the Release/wheel build configuration. Under -DNDEBUG,
IndirectHelper::Read computes Data() + constant_id * 4 and reads a 4-byte offset far outside
the buffer, then the code dereferences the resulting bogus VkBytes*
(constant_bytes->named_key() / ->offset()).
Why the verifier does not stop it
compileModel() runs vkgraph::VerifyVkGraphBuffer(verifier) (VulkanBackend.cpp:657) before
builder.build_graph() (line 671). The FlatBuffer structural verifier only validates buffer
structure/offsets β it does not validate scalar table fields used as indices. So a buffer with
constant_id = 40 (or 0x02000000) passes verification cleanly, and the OOB happens post-verify.
Adjacent hardening does not cover this
Lines ~304-329 bound-check the constant DATA region extent (offset < constant_data_size_,
tensor byte-extent vs. constant_data_size_). That hardening operates on constant_bytes after
it has already been fetched via the unchecked Get() β it protects the data payload, not the
vector index itself.
Secondary variant (null deref)
constants is an optional vector. If constant_id >= 0 but the FlatBuffer omits the constants
vector, flatbuffer_->constants() is nullptr and ->Get(...) is a null-pointer dereference.
Reachability / PoC
Reachable purely by loading a malicious .pte:
init() β compileModel() β (verifier passes) β build_graph() β
add_tensor_to_graph() β constants()->Get(constant_id) β all before any GPU dispatch.
The PoC builds a valid 30-byte VulkanDelegateHeader wrapping a vk_graph FlatBuffer containing:
- one
VkValue = VkTensor(datatype=FLOAT32, dims=[1], constant_id=N, mem_obj_id=-1) - a 1-entry
constantsvector
Verification harness (exercises the REAL target code)
A faithful harness compiles and links the real target code and replicates VulkanBackend.cpp:287 byte-for-byte:
- Real
backends/vulkan/runtime/VulkanDelegateHeader.cppβ header parse - Real
flatc-generatedschema_generated.hβVkGraphBufferHasIdentifier+VerifyVkGraphBuffer+GetVkGraph - Real bundled flatbuffers library (
third-party/flatbuffers) βVector::Get/IndirectHelper::Read/ReadScalar
Only the GraphBuilder wrapper method is replicated inline (it otherwise pulls in the Vulkan GPU
API / ComputeGraph); the vulnerable expression constants()->Get(tensor_fb->constant_id()) is
identical.
Artifacts: harness.cpp, stubs.cpp, gen/schema_generated.h, harness (Debug),
harness_ndebug (Release / -DNDEBUG + ASan).
Captured evidence (verbatim)
Malicious constant_id=40 β Release build (-DNDEBUG), ASan heap-buffer-overflow READ
[*] mode=MALICIOUS processed blob = 162 bytes, flatbuffer = 132 bytes, constant_id=40
[*] header OK: fb_off=30 fb_size=132 bytes_off=162
[*] VerifyVkGraphBuffer PASSED (verifier does NOT validate constant_id as an index)
[*] values=1 constants=1
[*] about to index constants()->Get(40) (constants size=1)
=================================================================
==311346==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c48a53e012a at pc 0x5562b59d9f05 bp 0x7ffc45968e50 sp 0x7ffc45968e48
READ of size 4 at 0x7c48a53e012a thread T0
#0 ... in unsigned int flatbuffers::ReadScalar<unsigned int>(void const*) .../flatbuffers/base.h:428:23
#1 ... in flatbuffers::IndirectHelper<flatbuffers::Offset<vkgraph::VkBytes>>::Read(unsigned char const*, unsigned int) .../flatbuffers/buffer.h:123:27
#2 ... in flatbuffers::Vector<flatbuffers::Offset<vkgraph::VkBytes>, unsigned int>::Get(unsigned int) const .../flatbuffers/vector.h:177:12
#3 ... in main .../vulkan_bug/harness.cpp:159:59 (== VulkanBackend.cpp:287 constants()->Get(constant_id))
0x7c48a53e012a is located 72 bytes after 162-byte region [0x7c48a53e0040,0x7c48a53e00e2)
SUMMARY: AddressSanitizer: heap-buffer-overflow .../flatbuffers/base.h:428:23 in unsigned int flatbuffers::ReadScalar<unsigned int>(void const*)
Wild-read variant constant_id=0x02000000 (~128 MB past buffer) β Release, ASan SEGV
[*] about to index constants()->Get(33554432) (constants size=1)
==311501==ERROR: AddressSanitizer: SEGV on unknown address 0x7cd8573e008a (pc 0x55b96dd2a612 ...)
SUMMARY: AddressSanitizer: SEGV .../flatbuffers/base.h:428:23 in unsigned int flatbuffers::ReadScalar<unsigned int>(void const*)
Debug build β assert(i < size()) abort (DoS)
[*] about to index constants()->Get(33554432) (constants size=1)
harness: .../third-party/flatbuffers/include/flatbuffers/vector.h:176: ...
Vector<...>::Get(SizeT) const [...]: Assertion `i < size()' failed.
Negative control β benign constant_id=0 (valid index) β clean, both builds
[*] mode=benign processed blob = 158 bytes, flatbuffer = 128 bytes, constant_id=0
[*] header OK: fb_off=30 fb_size=128 bytes_off=158
[*] VerifyVkGraphBuffer PASSED (verifier does NOT validate constant_id as an index)
[*] values=1 constants=1
[*] about to index constants()->Get(0) (constants size=1)
[*] constant_bytes->offset() = 0 (no crash)
[*] done, no crash
exit=0
The verifier PASSED line prints in every malicious run, proving the flaw is post-verification.
Suggested fix
Bounds-check the index before use, and guard the optional vector:
if (tensor_fb->constant_id() >= 0) {
const auto* constants = flatbuffer_->constants();
VK_CHECK_COND(constants != nullptr, "VkTensor.constant_id set but no constants vector");
VK_CHECK_COND(
static_cast<uint32_t>(tensor_fb->constant_id()) < constants->size(),
"VkTensor.constant_id %d out of range (constants size %u)",
tensor_fb->constant_id(), constants->size());
VkBytesPtr constant_bytes = constants->Get(tensor_fb->constant_id());
...
Dedup note
- No published CVE or GitHub advisory currently covers
VkTensor.constant_idvector-index validation in the ExecuTorch Vulkan backend as of the target commit. - Distinct from the existing constant-data-region extent hardening at VulkanBackend.cpp:304-329 (bounds the payload, not the vector index).
- Distinct from prior ExecuTorch findings in this workspace (WebGPU delegate header OOB, XNNPACK
const-data/weight OOB,
minverifyOOB, compile-spec / delegate-data null-deref, flat_tensor null-deref) β different backend, different field, different code path.