YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Heap OOB read (buffer underflow) in ExecuTorch Ethos-U delegate init
vela_bin_validate() computes a footer pointer without a minimum-size check on the attacker-controlled delegate-blob size.
Target
- Project: pytorch/executorch
- Commit:
a6d812a082df57898b8608f56c867140cc9da32c(2026-07-10) - Files:
backends/arm/runtime/VelaBinStream.cpp(the vulnerable parser)backends/arm/runtime/EthosUBackend.cpp(the caller / entry point)
- Component: Arm Ethos-U backend delegate (
EthosUBackend) - Vulnerability class: Heap out-of-bounds read / buffer underflow (CWE-125 / CWE-127)
Summary
A malicious .pte (ExecuTorch program) that contains an EthosUBackend delegate
segment shorter than sizeof(VelaBinBlock) (== 32 bytes) triggers an out-of-bounds
heap read the moment the delegate is initialized at load time. The delegate blob is
opaque [ubyte] processed data; the .pte flatbuffer program verifier does not
length-check it, so the bug is reachable post-verifier, at delegate init(),
before any inference runs.
Root cause
EthosUBackend::init() hands the delegate blob straight to vela_bin_validate(data, size)
with no minimum-size guard:
backends/arm/runtime/EthosUBackend.cpp (lines 83-96):
Result<DelegateHandle*> init(
BackendInitContext& context,
FreeableBuffer* processed,
ArrayRef<CompileSpec> compile_specs) const override {
ET_LOG(Info, "data:%p", processed->data());
const char* data = static_cast<const char*>(processed->data());
size_t size = processed->size();
// Verify format of vela_bin
if (vela_bin_validate(data, size) == false) { // <-- size is attacker-controlled, unchecked
ET_LOG(Error, "Malformed vela_bin_stream found");
return Error::InvalidProgram;
}
...
vela_bin_validate then computes the footer pointer by subtracting a fixed 32-byte
block size from data + size, and later reads from it โ regardless of whether size
is even large enough for that footer to lie inside the buffer:
backends/arm/runtime/VelaBinStream.cpp (lines 28-52):
bool vela_bin_validate(const char* data, int size) {
const char* foot = data + size - sizeof(VelaBinBlock); // sizeof(VelaBinBlock)==32
// Check 16 byte alignment
bool valid = true;
if ((uintptr_t)data != next_mul_16((uintptr_t)data)) {
ET_LOG(Error, "Vela bin ptr not aligned to 16 bytes: %p", data);
valid = false;
}
if ((uintptr_t)foot != next_mul_16((uintptr_t)foot)) {
ET_LOG(Error, "End of vela bin not aligned to 16 bytes: %p", foot);
valid = false; // <-- sets flag but does NOT early-return
}
// Check header and footer blocks are the right format
if (strncmp(data, "vela_bin_stream", strlen("vela_bin_stream")) != 0) { // OOB if size < 15
ET_LOG(Error, "Incorrect header in vela_bin_stream");
valid = false;
}
if (strncmp(foot, "vela_end_stream", strlen("vela_end_stream")) != 0) { // <-- OOB read here
ET_LOG(Error, "Incorrect footer in vela_bin_stream");
valid = false;
}
return valid;
}
When size < 32, foot = data + size - 32 points before the start of the heap
buffer. Critically, the function only accumulates a valid = false flag; it never
early-returns. So even though the footer-alignment check fails, execution still reaches
line 46, strncmp(foot, "vela_end_stream", 15), which dereferences the underflowed
pointer โ an out-of-bounds read of up to 15 bytes before the allocation.
(The line-42 header strncmp(data, "vela_bin_stream", 15) is a second OOB read when
size < 15. The footer underflow is the cleaner and more general primitive because it
fires for any size < 32.)
PoC
An ASan harness (harness.cpp) compiles the real, unmodified
backends/arm/runtime/VelaBinStream.cpp at commit a6d812a and calls
vela_bin_validate(data, size) exactly as EthosUBackend::init() does. The only build
flag is -DET_LOG_ENABLED=0, which compiles out the ET_LOG logging macro โ none of
those log statements fire before the crash, so the parsing logic (pointer arithmetic +
strncmp) is byte-for-byte the shipped code.
Attack input (mode 0): a 16-byte, 16-byte-aligned heap buffer whose first 16 bytes
are the valid magic "vela_bin_stream\0" (so the header check matches and control
reaches the footer check), passed with size = 16 (< 32). foot = data + 16 - 32 = data - 16 lands in the heap redzone; strncmp(foot, ...) reads OOB.
Negative control (mode 1): a well-formed 64-byte blob (a 32-byte header block +
a "vela_end_stream" footer block at data + 32) returns cleanly (validate == 1)
with zero ASan reports.
Build
clang++ -g -O0 -fsanitize=address -DET_LOG_ENABLED=0 \
-I <repo-parent> \
vela_oob/harness.cpp \
executorch/backends/arm/runtime/VelaBinStream.cpp \
-o vela_harness
Run
./vela_harness 0 # attack -> ASan heap-buffer-overflow READ
./vela_harness 1 # control -> clean, validate==1, no ASan report
Captured evidence (verbatim)
===== ATTACK (mode 0) =====
[attack] data=0x7b3d051e0040 size=16 foot=0x7b3d051e0030 (data-16)
=================================================================
==568158==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b3d051e0030 at pc 0x560626169de5 bp 0x7fff3794c2c0 sp 0x7fff3794ba68
READ of size 1 at 0x7b3d051e0030 thread T0
#0 0x560626169de4 in strncmp
#1 0x56062623945a in executorch::backends::arm::vela_bin_validate(char const*, int) backends/arm/runtime/VelaBinStream.cpp:46:7
#2 0x5606262390ba in main vela_oob/harness.cpp:46:14
#3 0x7f0d06003f76 (libc.so.6+0x29f76)
#4 0x7f0d06004026 in __libc_start_main (libc.so.6+0x2a026)
#5 0x56062614e380 in _start
0x7b3d051e0030 is located 8 bytes after 24-byte region [0x7b3d051e0010,0x7b3d051e0028)
allocated by thread T0 here:
#0 0x5606261f3928 in malloc
#1 0x7f0d0606be20 (libc.so.6+0x91e20)
SUMMARY: AddressSanitizer: heap-buffer-overflow in strncmp
==568158==ABORTING
===== NEGATIVE CONTROL (mode 1) =====
[control] data=0x7ba4887e0020 size=64 foot=0x7ba4887e0040 (data+32)
[control] vela_bin_validate returned 1 (clean, no OOB)
Impact
- Reachability: load-time, pre-inference. Any consumer that loads an untrusted
.ptetargeting the Ethos-U backend (on-device model loaders, model-serving that accepts user-supplied models, CI pipelines that validate models) hits this path during delegateinit(). - Effect: out-of-bounds heap read of up to 15 bytes immediately before the delegate
buffer. In hardened allocators this can crash the process (DoS). Depending on heap
layout the read result influences the
validreturn and thus control flow, and can leak adjacent heap contents via observable accept/reject behavior.
Suggested fix
Add a minimum-size guard before computing foot:
bool vela_bin_validate(const char* data, int size) {
if (size < (int)(2 * sizeof(VelaBinBlock)) || size < 0) { // need header + footer blocks
ET_LOG(Error, "vela_bin too small: %d", size);
return false;
}
const char* foot = data + size - sizeof(VelaBinBlock);
...
and/or have EthosUBackend::init() reject processed->size() < 2 * sizeof(VelaBinBlock)
before calling the validator.
Dedup / prior-art note
No CVE or public advisory is known for vela_bin_validate / the Ethos-U VelaBinStream
parser. This is distinct from other ExecuTorch delegate-parsing issues (XNNPACK
constant-data / weight bounds, Vulkan constant-id, QNN custom-proto header, WebGPU
delegate header), which live in different backends and different parsers. The finding is
specific to backends/arm/runtime/VelaBinStream.cpp:46 (footer) and :42 (header) and
the missing minimum-size check in EthosUBackend::init().