ExecuTorch WebGPU delegate header: missing length/bounds validation (memory-unsafe .pte model load)
Status: gated, access on request (huntr / ProtectAI triage). Do not use against production systems you do not own.
Summary
WebGPUDelegateHeader::parse() in
backends/webgpu/runtime/WebGPUDelegateHeader.cpp (pytorch/executorch,
commit fa49f8ff28983fb820ca86a31cc0fbd2257c1c5b, main branch) parses the
30-byte binary header that precedes the WebGPU delegate's flatbuffer +
constant-data blob inside a .pte model file's delegate "processed" data
segment.
Unlike its sibling VulkanDelegateHeader::parse(data, buffer_size) (same
repo, backends/vulkan/runtime/VulkanDelegateHeader.cpp), the WebGPU
version:
- Takes no
buffer_sizeparameter at all βparse(const void* data)β so it can never check that the buffer it was handed is at leastkExpectedSize(30) bytes before reading the magic and the five header fields at fixed offsets. is_valid()only cross-checks the header fields against each other (flatbuffer_offset >= header_size,bytes_offset >= flatbuffer_offset + flatbuffer_size, β¦). It never checks them against the real buffer length, becauseparse()was never given that length. The Vulkan sibling has an explicit extra check for exactly this ("Validate that header offsets do not extend beyond the buffer").
The sole caller, WebGPUBackend::init()
(backends/webgpu/runtime/WebGPUBackend.cpp), calls
WebGPUDelegateHeader::parse(processed->data()) β again passing no size β
and then unconditionally computes
buffer_start + header->flatbuffer_offset and
buffer_start + header->bytes_offset and dereferences them. Unlike
VulkanBackend::compileModel() (the analogous Vulkan code path), it also
never runs the flatbuffer through a flatbuffers::Verifier before use β it
goes straight from the unchecked offset to
vkgraph::VkGraphBufferHasIdentifier(flatbuffer_data) and
graph->build(flatbuffer_data, ...). The WebGPU backend appears to be an
incomplete port of the Vulkan delegate (same vkgraph/VH00 on-disk
format) that dropped the buffer-size plumbing and the FlatBuffer
verification step along the way.
processed is the delegate's serialized blob taken straight from the
.pte file (BackendDelegate::Init() β
method.cpp:GetProcessedData()), so every byte of it, including its
length, is attacker-controlled. BackendDelegate::Init() runs for every
delegate listed in a program's execution plan simply while loading
the model (Method::init), before any tensor is ever executed and before
any real WebGPU/GPU context is required.
This gives two distinct, independently reachable memory-safety bugs from a
single missing check, both triggerable just by loading a crafted .pte
file that names a WebGPU/Vulkan-schema delegate:
- Root cause A β no minimum-length check in
parse(). Any delegate blob shorter than 30 bytes makesparse()read past the end of the buffer while checking the magic (memcmp) or decoding the header fields (getUInt32LE/getUInt16LE/getUInt64LE). A 0-byte blob SEGVs; a 4-byte blob heap-buffer-overflows. - Root cause B β no offset/size validation against the real buffer. A
header that is internally consistent (passes
is_valid()) but whoseflatbuffer_offset/bytes_offsetpoint far past the end of the actual (short) buffer sails throughparse()asError::Ok. The caller then dereferencesbuffer_start + header->bytes_offset, an arbitrary, attacker-chosen offset, causing an out-of-bounds read (SEGV or heap-buffer-overflow depending on offset magnitude).
Both were reproduced with ASan+UBSan on the real, unmodified source and
independently rediscovered by an AFL++ persistent-mode campaign
(~2.2M execs, ~4 min to full saturation of a 44/278-edge target β the
header parser is small, so AFL exhausts its state space almost instantly;
no new crash classes appeared after the first ~2000 execs) driving the
harness in fuzz_webgpu_header.cpp.
Impact
Loading an untrusted/downloaded .pte model file that specifies a
WebGPU-schema delegate with a truncated or offset-crafted "processed" blob
causes the ExecuTorch runtime to read out of bounds during model load
(no inference required), landing on ASan as heap-buffer-overflow or SEGV.
Depending on heap layout this is a crash (DoS) at minimum; an
out-of-bounds read whose result is subsequently treated as a flatbuffer/
constant-data pointer is also a plausible route to information disclosure
or further memory corruption once the "flatbuffer" bytes it dereferences
are parsed by the vkgraph flatbuffer reader. This is exactly the "loading
an ExecuTorch model file can cause the runtime to crash / undefined
behavior" bug class ExecuTorch has fixed for cash before (see Dedup
section) β just in a different module (WebGPU delegate header, not
tensor-parser/prim-ops).
Dedup / prior-art check
pytorch/executorchGitHub issues/PRs: no hits forWebGPUDelegateHeader(searched 2026-07-06).- CVE-2025-54950 (critical OOB in "ExecuTorch models loading", fixed in
commit
b6b7a16df5e7852d976d8c34c8a7e9a1b6f7d005) is unrelated: that fix only toucheskernels/prim_ops/{et_copy_index.cpp,et_view.cpp, register_prim_ops.cpp}(missing argument-count validation on primitive kernels), nothing inbackends/webgpuorbackends/vulkan. - The flat_tensor/tensor-accessor path (
extension/flat_tensor,runtime/executor/tensor_parser*) was audited and reported in an earlier pass of this operation; this finding is in a structurally different module (delegate/backend metadata header parsing) with a different root cause (parser given no size vs. tensor bounds-check gap). schema/extended_header.cpp(the outer.ptefile header) andVulkanDelegateHeader.cppwere read as comparators; both correctly take and check abuffer_size. WebGPUDelegateHeader is the outlier.
Files
fuzz_webgpu_header.cppβ AFL++ persistent-mode harness. Compiles the real, unmodifiedWebGPUDelegateHeader.cppand drivesWebGPUDelegateHeader::parse()exactly the wayWebGPUBackend::init()does (including dereferencing the returned offsets), on an attacker-sized buffer.poc_A_truncated_header.binβ 4-byte input. Minimal repro for root cause A (no min-length check). ASan: heap-buffer-overflow inMemcmpInterceptorCommonfromWebGPUDelegateHeader.cpp:78(magic check) /WebGPUDelegateHeader.cpp:48-49(getUInt32LE).poc_B_oob_offset.binβ 34-byte input with a well-formed, internally consistent header whosebytes_offsetis0x7FFFFFFF. Minimal repro for root cause B. ASan: SEGV when the harness (mirroringWebGPUBackend::init) dereferencesbuffer_start + header->bytes_offset.crashes/β raw AFL++ crashing testcases from the campaign (5 files, reduce to the same 2 root causes above).
Build / repro
git clone --depth 1 https://github.com/pytorch/executorch.git
clang++ -std=c++17 -g -O1 -fsanitize=address,undefined -I. \
fuzz_webgpu_header.cpp \
executorch/backends/webgpu/runtime/WebGPUDelegateHeader.cpp \
executorch/runtime/platform/log.cpp \
executorch/runtime/platform/platform.cpp \
executorch/runtime/platform/default/posix.cpp \
executorch/runtime/platform/abort.cpp \
-o fuzz_webgpu_header_plain
./fuzz_webgpu_header_plain poc_A_truncated_header.bin # heap-buffer-overflow
./fuzz_webgpu_header_plain poc_B_oob_offset.bin # SEGV
Suggested fix
Give WebGPUDelegateHeader::parse() the same signature and checks as
VulkanDelegateHeader::parse(data, buffer_size): take buffer_size,
reject inputs shorter than kExpectedSize before touching any bytes, and
after is_valid() passes, verify
flatbuffer_offset + flatbuffer_size <= buffer_size and
bytes_offset + bytes_size <= buffer_size before returning. Update
WebGPUBackend::init() to pass processed->size() through.