You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

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 .pte targeting the Ethos-U backend (on-device model loaders, model-serving that accepts user-supplied models, CI pipelines that validate models) hits this path during delegate init().
  • 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 valid return 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().

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support